diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-10 12:10:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-10 12:10:28 +0300 |
commit | d5f14b5e2cef173b377917829b8a494c9975af03 (patch) | |
tree | 0c8fc0258579a94743d151db56e7239f90800b35 | |
parent | 9d485c177e9404da5ba53702042fdb9c25d459f2 (diff) |
Add latest changes from gitlab-org/gitlab@master
27 files changed, 349 insertions, 238 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index ea8d3d5236d..f2f3c67b121 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -6941c499e077fe2303dd5c31a08807d14ad7a616 +529ef59e73a21d1abc54833c4edbe92cbcc9fb64 diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 19285a1dc85..5158e82c320 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -40,11 +40,6 @@ import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.g import listsIssuesQuery from '../graphql/lists_issues.query.graphql'; import * as types from './mutation_types'; -const notImplemented = () => { - /* eslint-disable-next-line @gitlab/require-i18n-strings */ - throw new Error('Not implemented!'); -}; - export const gqlClient = createGqClient( {}, { @@ -737,28 +732,4 @@ export default { unsetError: ({ commit }) => { commit(types.SET_ERROR, undefined); }, - - fetchBacklog: () => { - notImplemented(); - }, - - bulkUpdateIssues: () => { - notImplemented(); - }, - - fetchIssue: () => { - notImplemented(); - }, - - toggleIssueSubscription: () => { - notImplemented(); - }, - - showPage: () => { - notImplemented(); - }, - - toggleEmptyState: () => { - notImplemented(); - }, }; diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 22b9905ee62..ccea2917c2c 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -9,9 +9,7 @@ export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE'; export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE'; export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST'; -export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST'; export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; -export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR'; export const MOVE_LIST = 'MOVE_LIST'; export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE'; export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED'; @@ -20,19 +18,11 @@ export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE'; export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST'; export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE'; export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS'; -export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE'; -export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS'; -export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR'; export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM'; export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM'; -export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE'; export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS'; -export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS'; -export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR'; export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST'; export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST'; -export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; -export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID'; export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 4f89d7e9d1f..667628b2998 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -6,11 +6,6 @@ import { formatIssue } from '../boards_util'; import { issuableTypes } from '../constants'; import * as mutationTypes from './mutation_types'; -const notImplemented = () => { - /* eslint-disable-next-line @gitlab/require-i18n-strings */ - throw new Error('Not implemented!'); -}; - const updateListItemsCount = ({ state, listId, value }) => { const list = state.boardLists[listId]; if (state.issuableType === issuableTypes.epic) { @@ -94,18 +89,10 @@ export default { state.error = s__('Boards|An error occurred while generating lists. Please reload the page.'); }, - [mutationTypes.REQUEST_ADD_LIST]: () => { - notImplemented(); - }, - [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: (state, list) => { Vue.set(state.boardLists, list.id, list); }, - [mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => { - notImplemented(); - }, - [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => { const { boardLists } = state; Vue.set(boardLists, movedList.id, movedList); @@ -172,35 +159,11 @@ export default { state.isSettingAssignees = isLoading; }, - [mutationTypes.REQUEST_ADD_ISSUE]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_ADD_ISSUE_SUCCESS]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_ADD_ISSUE_ERROR]: () => { - notImplemented(); - }, - [mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => { const issueId = getIdFromGraphQLId(issue.id); Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId })); }, - [mutationTypes.REQUEST_UPDATE_ISSUE]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_UPDATE_ISSUE_SUCCESS]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_UPDATE_ISSUE_ERROR]: () => { - notImplemented(); - }, - [mutationTypes.ADD_BOARD_ITEM_TO_LIST]: ( state, { itemId, listId, moveBeforeId, moveAfterId, atIndex }, @@ -220,14 +183,6 @@ export default { Vue.delete(state.boardItems, itemId); }, - [mutationTypes.SET_CURRENT_PAGE]: () => { - notImplemented(); - }, - - [mutationTypes.TOGGLE_EMPTY_STATE]: () => { - notImplemented(); - }, - [mutationTypes.REQUEST_GROUP_PROJECTS]: (state, fetchNext) => { Vue.set(state, 'groupProjectsFlags', { [fetchNext ? 'isLoadingMore' : 'isLoading']: true, diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js index 176954891e9..df853a7c5b5 100644 --- a/app/assets/javascripts/vue_shared/directives/validation.js +++ b/app/assets/javascripts/vue_shared/directives/validation.js @@ -33,6 +33,10 @@ const focusFirstInvalidInput = (e) => { } }; +const getInputElement = (el) => { + return el.querySelector('input') || el; +}; + const isEveryFieldValid = (form) => Object.values(form.fields).every(({ state }) => state === true); const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = false }) => { @@ -91,8 +95,9 @@ export default function initValidation(customFeedbackMap = {}) { const elDataMap = new WeakMap(); return { - inserted(el, binding, { context }) { + inserted(element, binding, { context }) { const { arg: showGlobalValidation } = binding; + const el = getInputElement(element); const { form: formEl } = el; const validate = createValidator(context, feedbackMap); @@ -121,7 +126,8 @@ export default function initValidation(customFeedbackMap = {}) { validate({ el, reportInvalidInput: showGlobalValidation }); }, - update(el, binding) { + update(element, binding) { + const el = getInputElement(element); const { arg: showGlobalValidation } = binding; const { validate, isTouched, isBlurred } = elDataMap.get(el); const showValidationFeedback = showGlobalValidation || (isTouched && isBlurred); diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 1e44321e148..3c377a2bb35 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -103,7 +103,7 @@ module Issuable end scope :assigned_to, ->(u) do assignees_table = Arel::Table.new("#{to_ability_name}_assignees") - sql = assignees_table.project('true').where(assignees_table[:user_id].in(u)).where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id")) + sql = assignees_table.project('true').where(assignees_table[:user_id].in(u.id)).where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id")) where("EXISTS (#{sql.to_sql})") end # rubocop:enable GitlabSecurity/SqlInjection diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 3002ad27e91..b93aff063bb 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -381,7 +381,7 @@ class MergeRequest < ApplicationRecord scope :review_requested_to, ->(user) do where( reviewers_subquery - .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user)) + .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id)) .exists ) end @@ -389,7 +389,7 @@ class MergeRequest < ApplicationRecord scope :no_review_requested_to, ->(user) do where( reviewers_subquery - .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user)) + .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id)) .exists .not ) diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml deleted file mode 100644 index 719f3a3de33..00000000000 --- a/app/views/layouts/nav/sidebar/_project_menus.html.haml +++ /dev/null @@ -1,34 +0,0 @@ --# Shortcut to Project > Activity -%li.hidden - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do - %span - = _('Activity') - --# Shortcut to Repository > Graph (formerly, Network) -- if project_nav_tab? :network - %li.hidden - = link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do - = _('Graph') - --# Shortcut to Issues > New Issue -- if project_nav_tab?(:issues) - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - = _('Create a new issue') - --# Shortcut to Pipelines > Jobs -- if project_nav_tab? :builds - %li.hidden - = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do - = _('Jobs') - --# Shortcut to commits page -- if project_nav_tab? :commits - %li.hidden - = link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do - = _('Commits') - --# Shortcut to issue boards -- if project_nav_tab?(:issues) - %li.hidden - = link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards' diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml index 1c06fc9eebf..8e388f2826d 100644 --- a/app/views/shared/nav/_sidebar.html.haml +++ b/app/views/shared/nav/_sidebar.html.haml @@ -11,4 +11,5 @@ - if sidebar.render_raw_menus_partial = render sidebar.render_raw_menus_partial + = render partial: 'shared/nav/sidebar_hidden_menu_item', collection: sidebar.hidden_menu&.items = render 'shared/sidebar_toggle_button' diff --git a/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml b/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml new file mode 100644 index 00000000000..953f7a8ae60 --- /dev/null +++ b/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml @@ -0,0 +1,3 @@ +%li.hidden + = link_to sidebar_hidden_menu_item.link, **sidebar_hidden_menu_item.container_html_options do + = sidebar_hidden_menu_item.title diff --git a/db/post_migrate/20200305082754_remove_duplicate_labels_from_project.rb b/db/post_migrate/20200305082754_remove_duplicate_labels_from_project.rb index 4bb43da43bb..f7df9f28187 100644 --- a/db/post_migrate/20200305082754_remove_duplicate_labels_from_project.rb +++ b/db/post_migrate/20200305082754_remove_duplicate_labels_from_project.rb @@ -9,6 +9,8 @@ class RemoveDuplicateLabelsFromProject < ActiveRecord::Migration[6.0] disable_ddl_transaction! class BackupLabel < Label + self.inheritance_column = :_type_disabled + self.table_name = 'backup_labels' end diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md index 6b1cf2d1194..6a510900a41 100644 --- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md +++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md @@ -537,7 +537,7 @@ inactive_users.each do |user| end ``` -### Find Max permissions for project/group +### Find a user's max permissions for project/group ```ruby user = User.find_by_username 'username' diff --git a/doc/api/users.md b/doc/api/users.md index 228194f10fa..ac8fbe8492f 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -190,7 +190,7 @@ GET /users ] ``` -Users on GitLab [Premium or higher](https://about.gitlab.com/pricing/) also see the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit`, and `using_license_seat` parameters. +Users on GitLab [Premium or higher](https://about.gitlab.com/pricing/) also see the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit`, `is_auditor`, and `using_license_seat` parameters. ```json [ @@ -199,6 +199,7 @@ Users on GitLab [Premium or higher](https://about.gitlab.com/pricing/) also see ... "shared_runners_minutes_limit": 133, "extra_shared_runners_minutes_limit": 133, + "is_auditor": false, "using_license_seat": true ... } @@ -359,12 +360,13 @@ NOTE: The `plan` and `trial` parameters are only available on GitLab Enterprise Edition. Users on GitLab [Premium or higher](https://about.gitlab.com/pricing/) also see -the `shared_runners_minutes_limit`, and `extra_shared_runners_minutes_limit` parameters. +the `shared_runners_minutes_limit`, `is_auditor`, and `extra_shared_runners_minutes_limit` parameters. ```json { "id": 1, "username": "john_smith", + "is_auditor": false, "shared_runners_minutes_limit": 133, "extra_shared_runners_minutes_limit": 133, ... @@ -628,6 +630,8 @@ GET /user } ``` +Users on GitLab [Premium or higher](https://about.gitlab.com/pricing/) also see the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit`, `is_auditor`, and `using_license_seat` parameters. + ## User status Get the status of the currently signed in user. diff --git a/doc/development/changelog.md b/doc/development/changelog.md index f77f918d2a1..6412c303735 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -52,11 +52,9 @@ the `author` field. GitLab team members **should not**. a changelog entry regardless of these guidelines if the contributor wants one. Example: "Fixed a typo on the search results page." - Any docs-only changes **should not** have a changelog entry. -- Any change behind a feature flag **disabled** by default **should not** have a changelog entry. -- Any change behind a feature flag that is **enabled** by default **should** have a changelog entry. +- For changes related to feature flags, use [feature flag guide](feature_flags/index.md#changelog) to determine the changelog entry. - Any change that adds new Usage Data metrics, sets the status of existing ones to `removed`, and changes that need to be documented in Product Intelligence [Metrics Dictionary](usage_ping/dictionary.md) **should** have a changelog entry. - A change that adds snowplow events **should** have a changelog entry - -- A change that [removes a feature flag, or removes a feature and its feature flag](feature_flags/index.md) **must** have a changelog entry. - A fix for a regression introduced and then fixed in the same release (i.e., fixing a bug introduced during a monthly release candidate) **should not** have a changelog entry. diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md index 560e4f8cb90..e18bcaa1f4e 100644 --- a/doc/development/feature_flags/index.md +++ b/doc/development/feature_flags/index.md @@ -15,12 +15,6 @@ This document provides guidelines on how to use feature flags in the GitLab codebase to conditionally enable features and test them. -Features that are developed and merged behind a feature flag -should not include a changelog entry. A changelog entry with `type: added` should be included in the merge -request removing the feature flag or the merge request where the default value of -the feature flag is set to enabled. If the feature contains any database migrations, it -*should* include a changelog entry for the database changes. - WARNING: All newly-introduced feature flags should be [disabled by default](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#feature-flags-in-gitlab-development). @@ -55,7 +49,7 @@ should be leveraged: a specific project and ensure that there are no issues with the implementation. 1. When the feature is ready to be announced, create a merge request that adds documentation about the feature, including [documentation for the feature flag itself](../documentation/feature_flags.md), - and a changelog entry. In the same merge request either flip the feature flag to + and a [changelog entry](#changelog). In the same merge request either flip the feature flag to be **on by default** or remove it entirely in order to enable the new behavior. One might be tempted to think that feature flags will delay the release of a @@ -461,6 +455,24 @@ as follows: Feature.remove(:feature_flag_name) ``` +## Changelog + +- Any change behind a feature flag **disabled** by default **should not** have a changelog entry. + - **Exception:** database migrations **should** have a changelog entry. +- Any change related to a feature flag itself (flag removal, default-on setting) **should** have a changelog entry. + Use the flowchart to determine the changelog entry type. + + ```mermaid + graph LR + A[flag: default off] -->|'added' / 'changed'| B(flag: default on) + B -->|'other'| C(remove flag, keep new code) + B -->|'removed' / 'changed'| D(remove flag, keep old code) + A -->|'added' / 'changed'| C + A -->|no changelog| D + ``` + +- Any change behind a feature flag that is **enabled** by default **should** have a changelog entry. + ## Feature flags in tests Introducing a feature flag into the codebase creates an additional code path that should be tested. diff --git a/doc/user/application_security/api_fuzzing/index.md b/doc/user/application_security/api_fuzzing/index.md index a1193715018..bc6ce132566 100644 --- a/doc/user/application_security/api_fuzzing/index.md +++ b/doc/user/application_security/api_fuzzing/index.md @@ -569,7 +569,10 @@ To get you started quickly, GitLab provides the configuration file [`gitlab-api-fuzzing-config.yml`](https://gitlab.com/gitlab-org/security-products/analyzers/api-fuzzing/-/blob/master/gitlab-api-fuzzing-config.yml). This file has several testing profiles that perform various numbers of tests. The run time of each profile increases as the test numbers go up. To use a configuration file, add it to your -repository's root as `.gitlab-api-fuzzing.yml`. +repository as `.gitlab/gitlab-api-fuzzing-config.yml`. + +NOTE: ++In GitLab 13.11 and earlier, the configuration file was `.gitlab-api-fuzzing.yml` in the repository's root. In GitLab 13.12 and later, it is `.gitlab/gitlab-api-fuzzing-config.yml` in the repository's root. | Profile | Fuzz Tests (per parameter) | |:---------|:-----------| diff --git a/doc/user/application_security/security_dashboard/img/security_center_dashboard_link_v12_4.png b/doc/user/application_security/security_dashboard/img/security_center_dashboard_link_v12_4.png Binary files differdeleted file mode 100644 index e0e80810b08..00000000000 --- a/doc/user/application_security/security_dashboard/img/security_center_dashboard_link_v12_4.png +++ /dev/null diff --git a/doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.png b/doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.png Binary files differindex 4223494c294..74592e2cea5 100644 --- a/doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.png +++ b/doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.png diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 6c03a6aea25..f0b3d895df5 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -150,10 +150,8 @@ the following: ![Security Center Dashboard with projects](img/security_center_dashboard_v13_4.png) -You can access the Security Center from the menu -bar at the top of the page. Under **More**, select **Security**. - -![Security Center navigation link](img/security_center_dashboard_link_v12_4.png) +To view the Security Center, from the navigation bar at the top of the page, select +**More > Security**. ### Adding projects to the Security Center diff --git a/lib/sidebars/projects/menus/hidden_menu.rb b/lib/sidebars/projects/menus/hidden_menu.rb new file mode 100644 index 00000000000..19c77c67e1a --- /dev/null +++ b/lib/sidebars/projects/menus/hidden_menu.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + module Menus + class HiddenMenu < ::Sidebars::Menu + override :configure_menu_items + def configure_menu_items + add_item(activity_menu_item) + add_item(graph_menu_item) + add_item(new_issue_menu_item) + add_item(jobs_menu_item) + add_item(commits_menu_item) + add_item(issue_boards_menu_item) + + true + end + + private + + def activity_menu_item + ::Sidebars::MenuItem.new( + title: _('Activity'), + link: activity_project_path(context.project), + active_routes: {}, + container_html_options: { class: 'shortcuts-project-activity' }, + item_id: :activity + ) + end + + def graph_menu_item + return unless can?(context.current_user, :download_code, context.project) + return if context.project.empty_repo? + + ::Sidebars::MenuItem.new( + title: _('Graph'), + link: project_network_path(context.project, context.current_ref), + active_routes: {}, + container_html_options: { class: 'shortcuts-network' }, + item_id: :graph + ) + end + + def new_issue_menu_item + return unless can?(context.current_user, :read_issue, context.project) + + ::Sidebars::MenuItem.new( + title: _('Create a new issue'), + link: new_project_issue_path(context.project), + active_routes: {}, + container_html_options: { class: 'shortcuts-new-issue' }, + item_id: :new_issue + ) + end + + def jobs_menu_item + return unless can?(context.current_user, :read_build, context.project) + + ::Sidebars::MenuItem.new( + title: _('Jobs'), + link: project_jobs_path(context.project), + active_routes: {}, + container_html_options: { class: 'shortcuts-builds' }, + item_id: :jobs + ) + end + + def commits_menu_item + return unless can?(context.current_user, :download_code, context.project) + return if context.project.empty_repo? + + ::Sidebars::MenuItem.new( + title: _('Commits'), + link: project_commits_path(context.project), + active_routes: {}, + container_html_options: { class: 'shortcuts-commits' }, + item_id: :commits + ) + end + + def issue_boards_menu_item + return unless can?(context.current_user, :read_issue, context.project) + + ::Sidebars::MenuItem.new( + title: _('Issue Boards'), + link: project_boards_path(context.project), + active_routes: {}, + container_html_options: { class: 'shortcuts-issue-boards' }, + item_id: :issue_boards + ) + end + end + end + end +end diff --git a/lib/sidebars/projects/panel.rb b/lib/sidebars/projects/panel.rb index dbf2977df01..7a35c224cbf 100644 --- a/lib/sidebars/projects/panel.rb +++ b/lib/sidebars/projects/panel.rb @@ -6,6 +6,7 @@ module Sidebars override :configure_menus def configure_menus set_scope_menu(Sidebars::Projects::Menus::ScopeMenu.new(context)) + set_hidden_menu(Sidebars::Projects::Menus::HiddenMenu.new(context)) add_menu(Sidebars::Projects::Menus::ProjectInformationMenu.new(context)) add_menu(Sidebars::Projects::Menus::LearnGitlabMenu.new(context)) @@ -27,11 +28,6 @@ module Sidebars add_menu(Sidebars::Projects::Menus::SettingsMenu.new(context)) end - override :render_raw_menus_partial - def render_raw_menus_partial - 'layouts/nav/sidebar/project_menus' - end - override :aria_label def aria_label _('Project navigation') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 557cf5525b6..9b638aa1a63 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10199,9 +10199,6 @@ msgstr "" msgid "DastProfiles|Password form field" msgstr "" -msgid "DastProfiles|Please enter a valid timeout value" -msgstr "" - msgid "DastProfiles|Profile name" msgstr "" diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 34e24fe0a1c..09343b5704f 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -40,12 +40,6 @@ import { jest.mock('~/flash'); -const expectNotImplemented = (action) => { - it('is not implemented', () => { - expect(action).toThrow(new Error('Not implemented!')); - }); -}; - // We need this helper to make sure projectPath is including // subgroups when the movIssue action is called. const getProjectPath = (path) => path.split('#')[0]; @@ -1825,27 +1819,3 @@ describe('unsetError', () => { }); }); }); - -describe('fetchBacklog', () => { - expectNotImplemented(actions.fetchBacklog); -}); - -describe('bulkUpdateIssues', () => { - expectNotImplemented(actions.bulkUpdateIssues); -}); - -describe('fetchIssue', () => { - expectNotImplemented(actions.fetchIssue); -}); - -describe('toggleIssueSubscription', () => { - expectNotImplemented(actions.toggleIssueSubscription); -}); - -describe('showPage', () => { - expectNotImplemented(actions.showPage); -}); - -describe('toggleEmptyState', () => { - expectNotImplemented(actions.toggleEmptyState); -}); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index af6d439e294..d89abcc79ae 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -13,12 +13,6 @@ import { mockList, } from '../mock_data'; -const expectNotImplemented = (action) => { - it('is not implemented', () => { - expect(action).toThrow(new Error('Not implemented!')); - }); -}; - describe('Board Store Mutations', () => { let state; @@ -158,10 +152,6 @@ describe('Board Store Mutations', () => { }); }); - describe('REQUEST_ADD_LIST', () => { - expectNotImplemented(mutations.REQUEST_ADD_LIST); - }); - describe('RECEIVE_ADD_LIST_SUCCESS', () => { it('adds list to boardLists state', () => { mutations.RECEIVE_ADD_LIST_SUCCESS(state, mockLists[0]); @@ -172,10 +162,6 @@ describe('Board Store Mutations', () => { }); }); - describe('RECEIVE_ADD_LIST_ERROR', () => { - expectNotImplemented(mutations.RECEIVE_ADD_LIST_ERROR); - }); - describe('MOVE_LIST', () => { it('updates boardLists state with reordered lists', () => { state = { @@ -341,10 +327,6 @@ describe('Board Store Mutations', () => { }); }); - describe('REQUEST_ADD_ISSUE', () => { - expectNotImplemented(mutations.REQUEST_ADD_ISSUE); - }); - describe('UPDATE_BOARD_ITEM_BY_ID', () => { const issueId = '1'; const prop = 'id'; @@ -386,14 +368,6 @@ describe('Board Store Mutations', () => { }); }); - describe('RECEIVE_ADD_ISSUE_SUCCESS', () => { - expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_SUCCESS); - }); - - describe('RECEIVE_ADD_ISSUE_ERROR', () => { - expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR); - }); - describe('MUTATE_ISSUE_SUCCESS', () => { it('updates issue in issues state', () => { const issues = { @@ -434,18 +408,6 @@ describe('Board Store Mutations', () => { }); }); - describe('REQUEST_UPDATE_ISSUE', () => { - expectNotImplemented(mutations.REQUEST_UPDATE_ISSUE); - }); - - describe('RECEIVE_UPDATE_ISSUE_SUCCESS', () => { - expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_SUCCESS); - }); - - describe('RECEIVE_UPDATE_ISSUE_ERROR', () => { - expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR); - }); - describe('ADD_BOARD_ITEM_TO_LIST', () => { beforeEach(() => { setBoardsListsState(); @@ -540,14 +502,6 @@ describe('Board Store Mutations', () => { }); }); - describe('SET_CURRENT_PAGE', () => { - expectNotImplemented(mutations.SET_CURRENT_PAGE); - }); - - describe('TOGGLE_EMPTY_STATE', () => { - expectNotImplemented(mutations.TOGGLE_EMPTY_STATE); - }); - describe('REQUEST_GROUP_PROJECTS', () => { it('Should set isLoading in groupProjectsFlags to true in state when fetchNext is false', () => { mutations[types.REQUEST_GROUP_PROJECTS](state, false); diff --git a/spec/frontend/vue_shared/directives/validation_spec.js b/spec/frontend/vue_shared/directives/validation_spec.js index 2764a71d204..0c84be9354f 100644 --- a/spec/frontend/vue_shared/directives/validation_spec.js +++ b/spec/frontend/vue_shared/directives/validation_spec.js @@ -4,12 +4,18 @@ import validation from '~/vue_shared/directives/validation'; describe('validation directive', () => { let wrapper; - const createComponent = ({ inputAttributes, showValidation } = {}) => { + const createComponent = ({ inputAttributes, showValidation, template } = {}) => { const defaultInputAttributes = { type: 'text', required: true, }; + const defaultTemplate = ` + <form> + <input v-validation:[showValidation] name="exampleField" v-bind="attributes" /> + </form> + `; + const component = { directives: { validation: validation(), @@ -29,11 +35,7 @@ describe('validation directive', () => { }, }; }, - template: ` - <form> - <input v-validation:[showValidation] name="exampleField" v-bind="attributes" /> - </form> - `, + template: template || defaultTemplate, }; wrapper = shallowMount(component, { attachTo: document.body }); @@ -48,6 +50,12 @@ describe('validation directive', () => { const findForm = () => wrapper.find('form'); const findInput = () => wrapper.find('input'); + const setValueAndTriggerValidation = (value) => { + const input = findInput(); + input.setValue(value); + input.trigger('blur'); + }; + describe.each([true, false])( 'with fields untouched and "showValidation" set to "%s"', (showValidation) => { @@ -78,12 +86,6 @@ describe('validation directive', () => { `( 'with input-attributes set to $inputAttributes', ({ inputAttributes, validValue, invalidValue }) => { - const setValueAndTriggerValidation = (value) => { - const input = findInput(); - input.setValue(value); - input.trigger('blur'); - }; - beforeEach(() => { createComponent({ inputAttributes }); }); @@ -129,4 +131,52 @@ describe('validation directive', () => { }); }, ); + + describe('with group elements', () => { + const template = ` + <form> + <div v-validation:[showValidation]> + <input name="exampleField" v-bind="attributes" /> + </div> + </form> + `; + beforeEach(() => { + createComponent({ + template, + inputAttributes: { + required: true, + }, + }); + }); + + describe('with invalid value', () => { + beforeEach(() => { + setValueAndTriggerValidation(''); + }); + + it('should set correct field state', () => { + expect(getFormData().fields.exampleField).toEqual({ + state: false, + feedback: expect.any(String), + }); + }); + + it('should set correct feedback', () => { + expect(getFormData().fields.exampleField.feedback).toBe('Please fill out this field.'); + }); + }); + + describe('with valid value', () => { + beforeEach(() => { + setValueAndTriggerValidation('hello'); + }); + + it('set the correct state', () => { + expect(getFormData().fields.exampleField).toEqual({ + state: true, + feedback: '', + }); + }); + }); + }); }); diff --git a/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb b/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb new file mode 100644 index 00000000000..f7cb0adbb66 --- /dev/null +++ b/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Projects::Menus::HiddenMenu do + let_it_be(:project) { create(:project, :repository) } + + let(:user) { project.owner } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, current_ref: project.repository.root_ref) } + + describe '#render?' do + subject { described_class.new(context) } + + context 'when menu does not have any menu items' do + it 'returns false' do + allow(subject).to receive(:has_items?).and_return(false) + + expect(subject.render?).to be false + end + end + + context 'when menu has menu items' do + it 'returns true' do + expect(subject.render?).to be true + end + end + end + + describe 'Menu items' do + subject { described_class.new(context).items.index { |e| e.item_id == item_id } } + + shared_examples 'access rights checks' do + specify { is_expected.not_to be_nil } + + describe 'when the user does not have access' do + let(:user) { nil } + + specify { is_expected.to be_nil } + end + end + + describe 'Activity' do + let(:item_id) { :activity } + + context 'when user has access to the project' do + specify { is_expected.not_to be_nil } + + describe 'when the user is not present' do + let(:user) { nil } + + specify { is_expected.not_to be_nil } + end + end + end + + describe 'Graph' do + let(:item_id) { :graph } + + context 'when project repository is empty' do + before do + allow(project).to receive(:empty_repo?).and_return(true) + end + + specify { is_expected.to be_nil } + end + + it_behaves_like 'access rights checks' + end + + describe 'New Issue' do + let(:item_id) { :new_issue } + + it_behaves_like 'access rights checks' + end + + describe 'Jobs' do + let(:item_id) { :jobs } + + it_behaves_like 'access rights checks' + end + + describe 'Commits' do + let(:item_id) { :commits } + + context 'when project repository is empty' do + before do + allow(project).to receive(:empty_repo?).and_return(true) + end + + specify { is_expected.to be_nil } + end + + it_behaves_like 'access rights checks' + end + + describe 'Issue Boards' do + let(:item_id) { :issue_boards } + + it_behaves_like 'access rights checks' + end + end +end diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index 11bb81bcd40..fafd5777f93 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -1090,5 +1090,43 @@ RSpec.describe 'layouts/nav/sidebar/_project' do end end + describe 'Hidden menus' do + it 'has a link to the Activity page' do + render + + expect(rendered).to have_link('Activity', href: activity_project_path(project), class: 'shortcuts-project-activity', visible: false) + end + + it 'has a link to the Graph page' do + render + + expect(rendered).to have_link('Graph', href: project_network_path(project, current_ref), class: 'shortcuts-network', visible: false) + end + + it 'has a link to the New Issue page' do + render + + expect(rendered).to have_link('Create a new issue', href: new_project_issue_path(project), class: 'shortcuts-new-issue', visible: false) + end + + it 'has a link to the Jobs page' do + render + + expect(rendered).to have_link('Jobs', href: project_jobs_path(project), class: 'shortcuts-builds', visible: false) + end + + it 'has a link to the Commits page' do + render + + expect(rendered).to have_link('Commits', href: project_commits_path(project), class: 'shortcuts-commits', visible: false) + end + + it 'has a link to the Issue Boards page' do + render + + expect(rendered).to have_link('Issue Boards', href: project_boards_path(project), class: 'shortcuts-issue-boards', visible: false) + end + end + it_behaves_like 'sidebar includes snowplow attributes', 'render', 'projects_side_navigation', 'projects_side_navigation' end |