From 62aae3415c1a5e53f35668a84fdafc04be5e0f27 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 31 Jan 2022 06:12:59 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../components/notifications_dropdown.vue | 4 + app/assets/javascripts/notifications/index.js | 2 + .../projects/planning_hierarchy/index.js | 3 + .../work_items_hierarchy/components/app.vue | 95 ++++++++++++ .../components/hierarchy.vue | 119 +++++++++++++++ .../work_items_hierarchy/constants.js | 61 ++++++++ .../work_items_hierarchy/hierarchy_util.js | 10 ++ .../work_items_hierarchy/static_response.js | 142 ++++++++++++++++++ .../work_items_hierarchy_bundle.js | 26 ++++ .../stylesheets/_page_specific_files.scss | 1 + .../stylesheets/framework/typography.scss | 6 - app/assets/stylesheets/pages/hierarchy.scss | 15 ++ .../concerns/planning_hierarchy.rb | 17 +++ app/controllers/projects_controller.rb | 2 + app/policies/project_policy.rb | 2 + app/views/groups/_home_panel.html.haml | 2 +- app/views/projects/_home_panel.html.haml | 2 +- app/views/shared/planning_hierarchy.html.haml | 5 + .../development/work_items_hierarchy.yml | 8 + config/routes/project.rb | 2 + .../14_0/deprecation_manage_access_14_0.yml | 4 +- doc/api/oauth2.md | 18 +-- doc/ci/pipelines/settings.md | 17 ++- doc/development/code_review.md | 3 + doc/development/testing_guide/flaky_tests.md | 1 + doc/update/removals.md | 4 +- doc/user/application_security/dast/index.md | 2 +- ...view-project-work-item-hierarchy_v14_8.png | Bin 0 -> 38783 bytes doc/user/group/planning_hierarchy/index.md | 14 ++ lib/sidebars/concerns/work_item_hierarchy.rb | 27 ++++ .../menus/project_information_menu.rb | 3 + locale/gitlab.pot | 48 ++++++ scripts/rspec_helpers.sh | 2 +- .../members/member_leaves_project_spec.rb | 12 +- .../components/notifications_dropdown_spec.js | 8 + .../components/app_spec.js | 63 ++++++++ .../components/hierarchy_spec.js | 118 +++++++++++++++ .../hierarchy_util_spec.js | 16 ++ .../concerns/work_item_hierarchy_spec.rb | 36 +++++ .../menus/project_information_menu_spec.rb | 20 +++ .../concerns/planning_hierarchy_spec.rb | 34 +++++ .../navbar_structure_context.rb | 1 + .../policies/project_policy_shared_context.rb | 2 +- 43 files changed, 939 insertions(+), 38 deletions(-) create mode 100644 app/assets/javascripts/pages/projects/planning_hierarchy/index.js create mode 100644 app/assets/javascripts/work_items_hierarchy/components/app.vue create mode 100644 app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue create mode 100644 app/assets/javascripts/work_items_hierarchy/constants.js create mode 100644 app/assets/javascripts/work_items_hierarchy/hierarchy_util.js create mode 100644 app/assets/javascripts/work_items_hierarchy/static_response.js create mode 100644 app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js create mode 100644 app/assets/stylesheets/pages/hierarchy.scss create mode 100644 app/controllers/concerns/planning_hierarchy.rb create mode 100644 app/views/shared/planning_hierarchy.html.haml create mode 100644 config/feature_flags/development/work_items_hierarchy.yml create mode 100644 doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_8.png create mode 100644 lib/sidebars/concerns/work_item_hierarchy.rb create mode 100644 spec/frontend/work_items_hierarchy/components/app_spec.js create mode 100644 spec/frontend/work_items_hierarchy/components/hierarchy_spec.js create mode 100644 spec/frontend/work_items_hierarchy/hierarchy_util_spec.js create mode 100644 spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb create mode 100644 spec/requests/concerns/planning_hierarchy_spec.rb diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown.vue b/app/assets/javascripts/notifications/components/notifications_dropdown.vue index 69eb2115bf4..6b450c2b5fd 100644 --- a/app/assets/javascripts/notifications/components/notifications_dropdown.vue +++ b/app/assets/javascripts/notifications/components/notifications_dropdown.vue @@ -42,6 +42,9 @@ export default { showLabel: { default: false, }, + noFlip: { + default: false, + }, }, data() { return { @@ -127,6 +130,7 @@ export default { :disabled="disabled" :split="isCustomNotification" :text="buttonText" + :no-flip="noFlip" @click="openNotificationsModal" > { projectId, groupId, showLabel, + noFlip, } = el.dataset; return new Vue({ @@ -35,6 +36,7 @@ export default () => { projectId, groupId, showLabel: parseBoolean(showLabel), + noFlip: parseBoolean(noFlip), }, render(h) { return h(NotificationsDropdown); diff --git a/app/assets/javascripts/pages/projects/planning_hierarchy/index.js b/app/assets/javascripts/pages/projects/planning_hierarchy/index.js new file mode 100644 index 00000000000..d5dfe2d5f37 --- /dev/null +++ b/app/assets/javascripts/pages/projects/planning_hierarchy/index.js @@ -0,0 +1,3 @@ +import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle'; + +initWorkItemsHierarchy(); diff --git a/app/assets/javascripts/work_items_hierarchy/components/app.vue b/app/assets/javascripts/work_items_hierarchy/components/app.vue new file mode 100644 index 00000000000..cc04ff026ff --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/components/app.vue @@ -0,0 +1,95 @@ + + + diff --git a/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue b/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue new file mode 100644 index 00000000000..9b81218b6e4 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue @@ -0,0 +1,119 @@ + + diff --git a/app/assets/javascripts/work_items_hierarchy/constants.js b/app/assets/javascripts/work_items_hierarchy/constants.js new file mode 100644 index 00000000000..c9d78c300e3 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/constants.js @@ -0,0 +1,61 @@ +import { __ } from '~/locale'; + +export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey'; + +/** + * Hard-coded strings since we're rendering hierarchy + * items from mock responses. Remove this when we + * have a real hierarchy endpoint. + */ +export const LICENSE_PLAN = { + FREE: 'free', + PREMIUM: 'premium', + ULTIMATE: 'ultimate', +}; + +export const workItemTypes = { + EPIC: { + title: __('Epic'), + icon: 'epic', + color: '#694CC0', + backgroundColor: '#E1D8F9', + }, + ISSUE: { + title: __('Issue'), + icon: 'issues', + color: '#1068BF', + backgroundColor: '#CBE2F9', + }, + TASK: { + title: __('Task'), + icon: 'task-done', + color: '#217645', + backgroundColor: '#C3E6CD', + }, + INCIDENT: { + title: __('Incident'), + icon: 'issue-type-incident', + backgroundColor: '#db2a0f', + color: '#FDD4CD', + iconSize: 16, + }, + SUB_EPIC: { + title: __('Child epic'), + icon: 'epic', + color: '#AB6100', + backgroundColor: '#F5D9A8', + }, + REQUIREMENT: { + title: __('Requirement'), + icon: 'requirements', + color: '#0068c5', + backgroundColor: '#c5e3fb', + }, + TEST_CASE: { + title: __('Test case'), + icon: 'issue-type-test-case', + backgroundColor: '#007a3f', + color: '#bae8cb', + iconSize: 16, + }, +}; diff --git a/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js new file mode 100644 index 00000000000..61d93acdb91 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js @@ -0,0 +1,10 @@ +import { LICENSE_PLAN } from './constants'; + +export function inferLicensePlan({ hasSubEpics, hasEpics }) { + if (hasSubEpics) { + return LICENSE_PLAN.ULTIMATE; + } else if (hasEpics) { + return LICENSE_PLAN.PREMIUM; + } + return LICENSE_PLAN.FREE; +} diff --git a/app/assets/javascripts/work_items_hierarchy/static_response.js b/app/assets/javascripts/work_items_hierarchy/static_response.js new file mode 100644 index 00000000000..d1e2e486082 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/static_response.js @@ -0,0 +1,142 @@ +const FREE_TIER = 'free'; +const ULTIMATE_TIER = 'ultimate'; +const PREMIUM_TIER = 'premium'; + +const RESPONSE = { + [FREE_TIER]: [ + { + id: '1', + type: 'ISSUE', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '2', + type: 'TASK', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '3', + type: 'INCIDENT', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '4', + type: 'EPIC', + available: false, + license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '5', + type: 'SUB_EPIC', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '6', + type: 'REQUIREMENT', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '7', + type: 'TEST_CASE', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + ], + + [PREMIUM_TIER]: [ + { + id: '1', + type: 'EPIC', + available: true, + license: null, + nestedTypes: ['ISSUE'], + }, + { + id: '2', + type: 'TASK', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '3', + type: 'INCIDENT', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '5', + type: 'SUB_EPIC', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '6', + type: 'REQUIREMENT', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '7', + type: 'TEST_CASE', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + ], + + [ULTIMATE_TIER]: [ + { + id: '1', + type: 'EPIC', + available: true, + license: null, + nestedTypes: ['SUB_EPIC', 'ISSUE'], + }, + { + id: '2', + type: 'TASK', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '3', + type: 'INCIDENT', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '6', + type: 'REQUIREMENT', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '7', + type: 'TEST_CASE', + available: true, + license: null, + nestedTypes: null, + }, + ], +}; + +export default RESPONSE; diff --git a/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js new file mode 100644 index 00000000000..2258c725301 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import App from './components/app.vue'; +import { inferLicensePlan } from './hierarchy_util'; + +export const initWorkItemsHierarchy = () => { + const el = document.querySelector('#js-work-items-hierarchy'); + + const { illustrationPath, hasEpics, hasSubEpics } = el.dataset; + + const licensePlan = inferLicensePlan({ + hasEpics: parseBoolean(hasEpics), + hasSubEpics: parseBoolean(hasSubEpics), + }); + + return new Vue({ + el, + provide: { + illustrationPath, + licensePlan, + }, + render(createElement) { + return createElement(App); + }, + }); +}; diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index ff2b82d1806..24549a170bd 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -31,3 +31,4 @@ @import './pages/storage_quota'; @import './pages/tree'; @import './pages/users'; +@import './pages/hierarchy'; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 639b07bcc6b..b23b4e7d35d 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -525,32 +525,26 @@ -moz-osx-font-smoothing: grayscale; } - .fa-2x, .admonitionblock td.icon [class^='fa icon-'] { font-size: 2em; } - .fa-exclamation-triangle::before, .admonitionblock td.icon .icon-warning::before { content: '⚠'; } - .fa-exclamation-circle::before, .admonitionblock td.icon .icon-important::before { content: '❗'; } - .fa-lightbulb-o::before, .admonitionblock td.icon .icon-tip::before { content: '💡'; } - .fa-thumb-tack::before, .admonitionblock td.icon .icon-note::before { content: '📌'; } - .fa-fire::before, .admonitionblock td.icon .icon-caution::before { content: '🔥'; } diff --git a/app/assets/stylesheets/pages/hierarchy.scss b/app/assets/stylesheets/pages/hierarchy.scss new file mode 100644 index 00000000000..0812e4cc41e --- /dev/null +++ b/app/assets/stylesheets/pages/hierarchy.scss @@ -0,0 +1,15 @@ +.hierarchy-rounded-arrow-tail { + position: absolute; + top: 4px; + left: 5px; + height: calc(100% - 20px); +} + +.hierarchy-icon-wrapper { + height: $default-icon-size; + width: $default-icon-size; +} + +.hierarchy-rounded-arrow { + transform: scale(1, -1) rotate(90deg); +} diff --git a/app/controllers/concerns/planning_hierarchy.rb b/app/controllers/concerns/planning_hierarchy.rb new file mode 100644 index 00000000000..66f17ee688b --- /dev/null +++ b/app/controllers/concerns/planning_hierarchy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module PlanningHierarchy + extend ActiveSupport::Concern + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def planning_hierarchy + return access_denied! unless can?(current_user, :read_planning_hierarchy, @project) + + return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml) + + render 'shared/planning_hierarchy' + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables +end + +PlanningHierarchy.prepend_mod_with('PlanningHierarchy') diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 64abcd7cc33..c84d6256339 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -10,6 +10,7 @@ class ProjectsController < Projects::ApplicationController include ImportUrlParams include FiltersEvents include SourcegraphDecorator + include PlanningHierarchy prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } @@ -54,6 +55,7 @@ class ProjectsController < Projects::ApplicationController feature_category :team_planning, [:preview_markdown, :new_issuable_address] feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export] feature_category :code_review, [:unfoldered_environment_names] + feature_category :portfolio_management, [:planning_hierarchy] urgency :low, [:refs] urgency :high, [:unfoldered_environment_names] diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 0a3f0c6d424..1c3d6032d63 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy enable :read_wiki enable :read_issue enable :read_label + enable :read_planning_hierarchy enable :read_milestone enable :read_snippet enable :read_project_member @@ -572,6 +573,7 @@ class ProjectPolicy < BasePolicy enable :read_issue_board_list enable :read_wiki enable :read_label + enable :read_planning_hierarchy enable :read_milestone enable :read_snippet enable :read_project_member diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index e5d67831c71..9b3a8c31d54 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -31,7 +31,7 @@ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('admin') - if @notification_setting - .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top' } } + .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top', no_flip: 'true' } } - if can_create_subgroups .gl-px-2.gl-sm-w-auto.gl-w-full = link_to _("New subgroup"), new_group_path(parent_id: @group.id), class: "btn btn-default gl-button gl-mt-3 gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_subgroup_button' } diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 016bdcf2ce6..8e6cc6da65d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -37,7 +37,7 @@ = sprite_icon('admin') .gl-display-flex.gl-align-items-start.gl-mr-3 - if @notification_setting - .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } } + .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, no_flip: 'true' } } .count-buttons.gl-display-flex.gl-align-items-flex-start = render 'projects/buttons/star' diff --git a/app/views/shared/planning_hierarchy.html.haml b/app/views/shared/planning_hierarchy.html.haml new file mode 100644 index 00000000000..7ab5347b33d --- /dev/null +++ b/app/views/shared/planning_hierarchy.html.haml @@ -0,0 +1,5 @@ +- page_title _("Planning hierarchy") +- has_sub_epics = @project&.licensed_feature_available?(:subepics) +- has_epics = @project&.licensed_feature_available?(:epics) + +#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } } diff --git a/config/feature_flags/development/work_items_hierarchy.yml b/config/feature_flags/development/work_items_hierarchy.yml new file mode 100644 index 00000000000..0b3bc1bbe87 --- /dev/null +++ b/config/feature_flags/development/work_items_hierarchy.yml @@ -0,0 +1,8 @@ +--- +name: work_items_hierarchy +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79315 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451 +milestone: '14.8' +type: development +group: group::product planning +default_enabled: false diff --git a/config/routes/project.rb b/config/routes/project.rb index 50e958106d9..e05033587df 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -465,6 +465,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do namespace :integrations do resource :shimo, only: [:show] end + + get :planning_hierarchy end # End of the /-/ scope. diff --git a/data/removals/14_0/deprecation_manage_access_14_0.yml b/data/removals/14_0/deprecation_manage_access_14_0.yml index 224a709085a..30167d23d60 100644 --- a/data/removals/14_0/deprecation_manage_access_14_0.yml +++ b/data/removals/14_0/deprecation_manage_access_14_0.yml @@ -7,7 +7,7 @@ body: | To improve performance, we are limiting the number of projects returned from the `GET /groups/:id/` API call to 100. A complete list of projects can still be retrieved with the `GET /groups/:id/projects` API call. -- name: "GitLab OAuth implicit grant deprecation" +- name: "GitLab OAuth implicit grant" removal_date: "2021-06-22" removal_milestone: "14.0" reporter: ogolowinski @@ -16,4 +16,4 @@ body: | GitLab is deprecating the [OAuth 2 implicit grant flow](https://docs.gitlab.com/ee/api/oauth2.html#implicit-grant-flow) as it has been removed for [OAuth 2.1](https://oauth.net/2.1/). - Beginning in 14.0, new applications can't be created with the OAuth 2 implicit grant flow. Existing OAuth implicit grant flows are no longer supported in 14.4. Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows) before release 14.4. + Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows). diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 91fd8502bc9..7d83607ab28 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -32,7 +32,7 @@ GitLab supports the following authorization flows: hosted, first-party services. GitLab recommends against use of this flow. The draft specification for [OAuth 2.1](https://oauth.net/2.1/) specifically omits both the -Implicit grant and Resource Owner Password Credentials flows. It will be deprecated in the next OAuth specification version. +Implicit grant and Resource Owner Password Credentials flows. Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out how all those flows work and pick the right one for your use case. @@ -239,19 +239,13 @@ You can now make requests to the API with the access token returned. ### Implicit grant flow -NOTE: -For a detailed flow diagram, see the [RFC specification](https://tools.ietf.org/html/rfc6749#section-4.2). - WARNING: Implicit grant flow is inherently insecure and the IETF has removed it in [OAuth 2.1](https://oauth.net/2.1/). -For this reason, [support for it is deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/288516). -In GitLab 14.0, new applications can't be created using it. In GitLab 14.4, support for it is -scheduled to be removed for existing applications. +It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/288516) for use in GitLab 14.0, and is planned for +[removal](https://gitlab.com/gitlab-org/gitlab/-/issues/344609) in GitLab 15.0. -We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce) instead. If you choose to use Implicit flow, be sure to verify the -`application id` (or `client_id`) associated with the access token before granting -access to the data. To learn more, read -[Retrieving the token information](#retrieve-the-token-information)). +We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce) +instead. Unlike the authorization code flow, the client receives an `access token` immediately as a result of the authorization request. The flow does not use the @@ -415,7 +409,7 @@ The following is an example response: The fields `scopes` and `expires_in_seconds` are included in the response. -These are aliases for `scope` and `expires_in` respectively, and have been included to +These fields are aliases for `scope` and `expires_in` respectively, and have been included to prevent breaking changes introduced in [doorkeeper 5.0.2](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions#from-4x-to-5x). Don't rely on these fields as they are slated for removal in a later release. diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md index 8ba84338dfb..38516d83b89 100644 --- a/doc/ci/pipelines/settings.md +++ b/doc/ci/pipelines/settings.md @@ -211,16 +211,17 @@ averaged. To define a coverage-parsing regular expression: -1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > CI/CD**. -1. Expand **General pipelines**. -1. In the **Test coverage parsing** field, enter a regular expression. - Leave blank to disable this feature. +- In the GitLab UI: -Alternatively, provide a regular expression using the [`coverage`](../yaml/index.md#coverage) -keyword in your project's `.gitlab-ci.yml`. + 1. On the top bar, select **Menu > Projects** and find your project. + 1. On the left sidebar, select **Settings > CI/CD**. + 1. Expand **General pipelines**. + 1. In the **Test coverage parsing** field, enter a regular expression. Leave blank to disable this feature. -You can use to test your regex. The regex returns the **last** +- Using the project's `.gitlab-ci.yml`, provide a regular expression using the [`coverage`](../yaml/index.md#coverage) + keyword. + +You can use to test your regular expression. The regular expression returns the **last** match found in the output. ### Test coverage examples diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 29228d56848..bdfda0252a2 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -510,6 +510,9 @@ When reviewing merge requests added by wider community contributors: fetching of malicious packages. - Review links and images, especially in documentation MRs. - When in doubt, ask someone from `@gitlab-com/gl-security/appsec` to review the merge request **before manually starting any merge request pipeline**. +- Only set the milestone when the merge request is likely to be included in + the current milestone. This is to avoid confusion around when it'll be + merged and avoid moving milestone too often when it's not yet ready. If the MR source branch is more than 1,000 commits behind the target branch: diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index 333ebd8370f..09fc8f4d33e 100644 --- a/doc/development/testing_guide/flaky_tests.md +++ b/doc/development/testing_guide/flaky_tests.md @@ -106,6 +106,7 @@ reproduction. - [Lazy loaded images can cause Capybara to mis-click](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18713) - [Triggering JS events before the event handlers are set up](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18742) - [Wait for the image to be lazy-loaded when asserting on a Markdown image's `src` attribute](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25408) +- [Avoid asserting against flash notice banners](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79432) #### Capybara viewport size related issues diff --git a/doc/update/removals.md b/doc/update/removals.md index 550de5ff41a..15acc2c189d 100644 --- a/doc/update/removals.md +++ b/doc/update/removals.md @@ -175,7 +175,7 @@ As [announced in GitLab 13.3](https://about.gitlab.com/releases/2020/08/22/gitla - `geo_postgresql['fdw_external_password']` - `gitlab-_rails['geo_migrated_local_files_clean_up_worker_cron']` -### GitLab OAuth implicit grant deprecation +### GitLab OAuth implicit grant WARNING: This feature was changed or removed in 14.0 @@ -185,7 +185,7 @@ changes to your code, settings, or workflow. GitLab is deprecating the [OAuth 2 implicit grant flow](https://docs.gitlab.com/ee/api/oauth2.html#implicit-grant-flow) as it has been removed for [OAuth 2.1](https://oauth.net/2.1/). -Beginning in 14.0, new applications can't be created with the OAuth 2 implicit grant flow. Existing OAuth implicit grant flows are no longer supported in 14.4. Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows) before release 14.4. +Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows). ### GitLab Runner helper image in GitLab.com Container Registry diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 5d365491e30..4b601f0f877 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -53,7 +53,7 @@ results. On failure, the analyzer outputs an - [GitLab Runner](../../../ci/runners/index.md) available, with the [`docker` executor](https://docs.gitlab.com/runner/executors/docker.html). - Target application deployed. For more details, read [Deployment options](#deployment-options). -- DAST runs in the `test` stage, which is available by default. If you redefine the stages in the `.gitlab-ci.yml` file, the `test` stage is required. +- DAST runs in the `dast` stage, which must be added manually to your `.gitlab-ci.yml`. ### Deployment options diff --git a/doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_8.png b/doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_8.png new file mode 100644 index 0000000000000000000000000000000000000000..2ddd551ee46222230b2171785650b5e9404f0422 GIT binary patch literal 38783 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 [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.8 and is behind the feature flag `work_items_hierarchy`. + +To view the planning hierarchy in a project: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Project information > Planning hierarchy**. + +Under **Current structure**, you can see a hierarchy diagram that matches your current planning hierarchy. +The work items outside your subscription plan show up below **Unavailable structure**. + +![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_8.png) + ## Hierarchies with epics With epics, you can achieve the following hierarchy: diff --git a/lib/sidebars/concerns/work_item_hierarchy.rb b/lib/sidebars/concerns/work_item_hierarchy.rb new file mode 100644 index 00000000000..8e48f004bc7 --- /dev/null +++ b/lib/sidebars/concerns/work_item_hierarchy.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# This module has the necessary methods to render +# work items hierarchy menu +module Sidebars + module Concerns + module WorkItemHierarchy + def hierarchy_menu_item(container, url, path) + unless show_hierarachy_menu_item?(container) + return ::Sidebars::NilMenuItem.new(item_id: :hierarchy) + end + + ::Sidebars::MenuItem.new( + title: _('Planning hierarchy'), + link: url, + active_routes: { path: path }, + item_id: :hierarchy + ) + end + + def show_hierarachy_menu_item?(container) + Feature.enabled?(:work_items_hierarchy, container, default_enabled: :yaml) && + can?(context.current_user, :read_planning_hierarchy, container) + end + end + end +end diff --git a/lib/sidebars/projects/menus/project_information_menu.rb b/lib/sidebars/projects/menus/project_information_menu.rb index 44b94ee3522..4056d50d324 100644 --- a/lib/sidebars/projects/menus/project_information_menu.rb +++ b/lib/sidebars/projects/menus/project_information_menu.rb @@ -4,10 +4,13 @@ module Sidebars module Projects module Menus class ProjectInformationMenu < ::Sidebars::Menu + include ::Sidebars::Concerns::WorkItemHierarchy + override :configure_menu_items def configure_menu_items add_item(activity_menu_item) add_item(labels_menu_item) + add_item(hierarchy_menu_item(context.project, project_planning_hierarchy_path(context.project), 'projects#planning_hierarchy')) add_item(members_menu_item) true diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 25a83ad9cf7..0d7b2f75729 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7113,6 +7113,9 @@ msgstr "" msgid "Child" msgstr "" +msgid "Child epic" +msgstr "" + msgid "Child epic does not exist." msgstr "" @@ -17725,6 +17728,33 @@ msgstr[1] "" msgid "Hide values" msgstr "" +msgid "Hierarchy|Current structure" +msgstr "" + +msgid "Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals." +msgstr "" + +msgid "Hierarchy|Help us improve work items in GitLab!" +msgstr "" + +msgid "Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you." +msgstr "" + +msgid "Hierarchy|Planning hierarchy" +msgstr "" + +msgid "Hierarchy|Take the work items survey" +msgstr "" + +msgid "Hierarchy|These items are unavailable in the current structure." +msgstr "" + +msgid "Hierarchy|Unavailable structure" +msgstr "" + +msgid "Hierarchy|You can start using these items now." +msgstr "" + msgid "High or unknown vulnerabilities present" msgstr "" @@ -26636,6 +26666,9 @@ msgstr "" msgid "Plan:" msgstr "" +msgid "Planning hierarchy" +msgstr "" + msgid "PlantUML" msgstr "" @@ -30373,6 +30406,9 @@ msgstr "" msgid "Required only if you are not using role instance credentials." msgstr "" +msgid "Requirement" +msgstr "" + msgid "Requirement %{reference} has been added" msgstr "" @@ -33033,6 +33069,9 @@ msgstr "" msgid "Show all breadcrumbs" msgstr "" +msgid "Show all epics" +msgstr "" + msgid "Show all issues." msgstr "" @@ -33045,6 +33084,9 @@ msgstr "" msgid "Show archived projects only" msgstr "" +msgid "Show closed epics" +msgstr "" + msgid "Show command" msgstr "" @@ -33081,6 +33123,9 @@ msgstr "" msgid "Show one file at a time" msgstr "" +msgid "Show open epics" +msgstr "" + msgid "Show the Closed list" msgstr "" @@ -35376,6 +35421,9 @@ msgstr "" msgid "Test Cases" msgstr "" +msgid "Test case" +msgstr "" + msgid "Test coverage parsing" msgstr "" diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh index a6d9899bf2a..3bcd87aee2e 100644 --- a/scripts/rspec_helpers.sh +++ b/scripts/rspec_helpers.sh @@ -275,7 +275,7 @@ function rspec_paralellized_job() { export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv" if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then - tooling/bin/parallel_rspec --rspec_args "$(rspec_args)" --filter "tmp/matching_tests.txt" || rspec_run_status=$? + tooling/bin/parallel_rspec --rspec_args "$(rspec_args "${rspec_opts}")" --filter "tmp/matching_tests.txt" || rspec_run_status=$? else tooling/bin/parallel_rspec --rspec_args "$(rspec_args "${rspec_opts}")" || rspec_run_status=$? fi diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb index 09025e56728..c38292f81bf 100644 --- a/spec/features/projects/members/member_leaves_project_spec.rb +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Projects > Members > Member leaves project' do + include Spec::Support::Helpers::Features::MembersHelpers + let(:user) { create(:user) } let(:project) { create(:project, :repository) } @@ -25,10 +27,14 @@ RSpec.describe 'Projects > Members > Member leaves project' do visit project_path(project, leave: 1) page.accept_confirm - wait_for_all_requests - expect(find('.flash-notice')).to have_content "You left the \"#{project.full_name}\" project" + expect(current_path).to eq(dashboard_projects_path) - expect(project.users.exists?(user.id)).to be_falsey + + sign_in(project.first_owner) + + visit project_project_members_path(project) + + expect(members_table).not_to have_content(user.name) end end diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js index e12251ce6d9..7ca6c2052ae 100644 --- a/spec/frontend/notifications/components/notifications_dropdown_spec.js +++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js @@ -195,6 +195,14 @@ describe('NotificationsDropdown', () => { ); }); }); + + it('passes provided `noFlip` value to `GlDropdown`', () => { + wrapper = createComponent({ + noFlip: true, + }); + + expect(findDropdown().attributes('no-flip')).toBe('true'); + }); }); describe('when selecting an item', () => { diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js new file mode 100644 index 00000000000..092e9c90553 --- /dev/null +++ b/spec/frontend/work_items_hierarchy/components/app_spec.js @@ -0,0 +1,63 @@ +import { nextTick } from 'vue'; +import { createLocalVue, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { GlBanner } from '@gitlab/ui'; +import App from '~/work_items_hierarchy/components/app.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('WorkItemsHierarchy App', () => { + let wrapper; + const createComponent = (props = {}, data = {}) => { + wrapper = extendedWrapper( + mount(App, { + localVue, + provide: { + illustrationPath: '/foo.svg', + licensePlan: 'free', + ...props, + }, + data() { + return data; + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('survey banner', () => { + it('shows when the banner is visible', () => { + createComponent({}, { bannerVisible: true }); + + expect(wrapper.find(GlBanner).exists()).toBe(true); + }); + + it('hide when close is called', async () => { + createComponent({}, { bannerVisible: true }); + + wrapper.findByTestId('close-icon').trigger('click'); + + await nextTick(); + + expect(wrapper.find(GlBanner).exists()).toBe(false); + }); + }); + + describe('Unavailable structure', () => { + it.each` + licensePlan | visible + ${'free'} | ${true} + ${'premium'} | ${true} + ${'ultimate'} | ${false} + `('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => { + createComponent({ licensePlan }); + + expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible); + }); + }); +}); diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js new file mode 100644 index 00000000000..74774e38d6b --- /dev/null +++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js @@ -0,0 +1,118 @@ +import { createLocalVue, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { GlBadge } from '@gitlab/ui'; +import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import RESPONSE from '~/work_items_hierarchy/static_response'; +import { workItemTypes } from '~/work_items_hierarchy/constants'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('WorkItemsHierarchy Hierarchy', () => { + let wrapper; + + const workItemsFromResponse = (response) => { + return response.reduce( + (itemTypes, item) => { + const key = item.available ? 'available' : 'unavailable'; + itemTypes[key].push({ + ...item, + ...workItemTypes[item.type], + nestedTypes: item.nestedTypes + ? item.nestedTypes.map((type) => workItemTypes[type]) + : null, + }); + return itemTypes; + }, + { available: [], unavailable: [] }, + ); + }; + + const createComponent = (props = {}) => { + wrapper = extendedWrapper( + mount(Hierarchy, { + localVue, + propsData: { + workItemTypes: props.workItemTypes, + ...props, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('available structure', () => { + let items = []; + + beforeEach(() => { + items = workItemsFromResponse(RESPONSE.ultimate).available; + createComponent({ workItemTypes: items }); + }); + + it('renders all work items', () => { + expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length); + }); + + it('does not render badges', () => { + expect(wrapper.find(GlBadge).exists()).toBe(false); + }); + }); + + describe('unavailable structure', () => { + let items = []; + + beforeEach(() => { + items = workItemsFromResponse(RESPONSE.premium).unavailable; + createComponent({ workItemTypes: items }); + }); + + it('renders all work items', () => { + expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length); + }); + + it('renders license badges for all work items', () => { + expect(wrapper.findAll(GlBadge)).toHaveLength(items.length); + }); + + it('does not render svg icon for linking', () => { + expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false); + expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false); + }); + }); + + describe('nested work items', () => { + describe.each` + licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible + ${'ultimate'} | ${true} | ${true} | ${true} + ${'premium'} | ${false} | ${false} | ${true} + ${'free'} | ${false} | ${false} | ${false} + `( + 'when $licensePlan license', + ({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => { + let items = []; + beforeEach(() => { + items = workItemsFromResponse(RESPONSE[licensePlan]).available; + createComponent({ workItemTypes: items }); + }); + + it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe( + arrowTailVisible, + ); + }); + + it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible); + }); + + it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible); + }); + }, + ); + }); +}); diff --git a/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js new file mode 100644 index 00000000000..9042fa27d16 --- /dev/null +++ b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js @@ -0,0 +1,16 @@ +import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util'; +import { LICENSE_PLAN } from '~/work_items_hierarchy/constants'; + +describe('inferLicensePlan', () => { + it.each` + epics | subEpics | licensePlan + ${true} | ${true} | ${LICENSE_PLAN.ULTIMATE} + ${true} | ${false} | ${LICENSE_PLAN.PREMIUM} + ${false} | ${false} | ${LICENSE_PLAN.FREE} + `( + 'returns $licensePlan when epic is $epics and sub-epic is $subEpics', + ({ epics, subEpics, licensePlan }) => { + expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan); + }, + ); +}); diff --git a/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb new file mode 100644 index 00000000000..f0a5e032764 --- /dev/null +++ b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Concerns::WorkItemHierarchy do + shared_examples 'hierarchy menu' do + let(:item_id) { :hierarchy } + + context 'when the feature is disabled does not render' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + specify { is_expected.to be_nil } + end + + context 'when the feature is enabled does render' do + before do + stub_feature_flags(work_items_hierarchy: true) + end + + specify { is_expected.not_to be_nil } + end + end + + describe 'Project hierarchy menu item' do + let_it_be_with_reload(:project) { create(:project, :repository) } + + let(:user) { project.owner } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } + + subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } } + + it_behaves_like 'hierarchy menu' + end +end diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb index 7ff06ac229e..76367782d68 100644 --- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb @@ -59,5 +59,25 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do specify { is_expected.to be_nil } end end + + describe 'Hierarchy' do + let(:item_id) { :hierarchy } + + context 'when the feature is disabled' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + specify { is_expected.to be_nil } + end + + context 'when the feature is enabled' do + before do + stub_feature_flags(work_items_hierarchy: true) + end + + specify { is_expected.not_to be_nil } + end + end end end diff --git a/spec/requests/concerns/planning_hierarchy_spec.rb b/spec/requests/concerns/planning_hierarchy_spec.rb new file mode 100644 index 00000000000..c8d5a3b6b53 --- /dev/null +++ b/spec/requests/concerns/planning_hierarchy_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe PlanningHierarchy, type: :request do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + describe 'GET #planning_hierarchy' do + it 'renders planning hierarchy' do + stub_feature_flags(work_items_hierarchy: true) + + get project_planning_hierarchy_path(project) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to match(/id="js-work-items-hierarchy"/) + end + + it 'renders 404 page' do + stub_feature_flags(work_items_hierarchy: false) + + get project_planning_hierarchy_path(project) + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).not_to match(/id="js-work-items-hierarchy"/) + end + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 27967850389..576a8aa44fa 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [ _('Activity'), _('Labels'), + _('Planning hierarchy'), _('Members') ] }, diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index c39252cef13..3641edc845a 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do %i[ award_emoji create_issue create_merge_request_in create_note create_project read_issue_board read_issue read_issue_iid read_issue_link - read_label read_issue_board_list read_milestone read_note read_project + read_label read_planning_hierarchy read_issue_board_list read_milestone read_note read_project read_project_for_iids read_project_member read_release read_snippet read_wiki upload_file ]