diff --git a/app/assets/javascripts/access_level/constants.js b/app/assets/javascripts/access_level/constants.js
index c53fa472b36b1e7e057d95c554b1affb8e1799f7..9db628310c51227eea55e490cdb848b58698d50c 100644
--- a/app/assets/javascripts/access_level/constants.js
+++ b/app/assets/javascripts/access_level/constants.js
@@ -4,6 +4,7 @@ import { __, s__ } from '~/locale';
export const ACCESS_LEVEL_NO_ACCESS_INTEGER = 0;
export const ACCESS_LEVEL_MINIMAL_ACCESS_INTEGER = 5;
export const ACCESS_LEVEL_GUEST_INTEGER = 10;
+export const ACCESS_LEVEL_PLANNER_INTEGER = 15;
export const ACCESS_LEVEL_REPORTER_INTEGER = 20;
export const ACCESS_LEVEL_DEVELOPER_INTEGER = 30;
export const ACCESS_LEVEL_MAINTAINER_INTEGER = 40;
@@ -13,6 +14,7 @@ export const ACCESS_LEVEL_ADMIN_INTEGER = 60;
const ACCESS_LEVEL_NO_ACCESS = __('No access');
const ACCESS_LEVEL_MINIMAL_ACCESS = __('Minimal Access');
const ACCESS_LEVEL_GUEST = __('Guest');
+const ACCESS_LEVEL_PLANNER = __('Planner');
const ACCESS_LEVEL_REPORTER = __('Reporter');
const ACCESS_LEVEL_DEVELOPER = __('Developer');
const ACCESS_LEVEL_MAINTAINER = __('Maintainer');
@@ -39,6 +41,14 @@ export const BASE_ROLES = [
'MemberRole|The Guest role is for users who need visibility into a project or group but should not have the ability to make changes, such as external stakeholders.',
),
},
+ {
+ value: 'PLANNER',
+ text: ACCESS_LEVEL_PLANNER,
+ accessLevel: ACCESS_LEVEL_PLANNER_INTEGER,
+ memberRoleId: null,
+ occupiesSeat: true,
+ description: s__('MemberRole|The Planner role is for users who need to manage projects.'),
+ },
{
value: 'REPORTER',
text: ACCESS_LEVEL_REPORTER,
diff --git a/app/graphql/types/access_level_enum.rb b/app/graphql/types/access_level_enum.rb
index d58e7230a8e50febba230cab260d1861b51b7931..750fedb62a6208d1b2bd9937b26cee54ef15282e 100644
--- a/app/graphql/types/access_level_enum.rb
+++ b/app/graphql/types/access_level_enum.rb
@@ -8,6 +8,7 @@ class AccessLevelEnum < BaseEnum
value 'NO_ACCESS', value: Gitlab::Access::NO_ACCESS, description: 'No access.'
value 'MINIMAL_ACCESS', value: Gitlab::Access::MINIMAL_ACCESS, description: 'Minimal access.'
value 'GUEST', value: Gitlab::Access::GUEST, description: 'Guest access.'
+ value 'PLANNER', value: Gitlab::Access::PLANNER, description: 'Planner access.'
value 'REPORTER', value: Gitlab::Access::REPORTER, description: 'Reporter access.'
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: 'Developer access.'
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: 'Maintainer access.'
diff --git a/app/graphql/types/member_access_level_enum.rb b/app/graphql/types/member_access_level_enum.rb
index 8f89b882641cbf278b3d1fadfef1d4c2f5cf41b6..20ebb7497bc32967327d5de50110ed9e3705976e 100644
--- a/app/graphql/types/member_access_level_enum.rb
+++ b/app/graphql/types/member_access_level_enum.rb
@@ -6,6 +6,7 @@ class MemberAccessLevelEnum < BaseEnum
description 'Access level of a group or project member'
value 'GUEST', value: Gitlab::Access::GUEST, description: 'Guest access.'
+ value 'PLANNER', value: Gitlab::Access::PLANNER, description: 'Planner access.'
value 'REPORTER', value: Gitlab::Access::REPORTER, description: 'Reporter access.'
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: 'Developer access.'
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: 'Maintainer access.'
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 3b8de53f3da9613db1f4737e5ba0131acdc9265a..3ea1ffca1e357478b4f7a8e5d300ec7423a72802 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -695,6 +695,7 @@ def localized_access_names
Gitlab::Access::NO_ACCESS => _('No access'),
Gitlab::Access::MINIMAL_ACCESS => _("Minimal Access"),
Gitlab::Access::GUEST => _('Guest'),
+ Gitlab::Access::PLANNER => _('Planner'),
Gitlab::Access::REPORTER => _('Reporter'),
Gitlab::Access::DEVELOPER => _('Developer'),
Gitlab::Access::MAINTAINER => _('Maintainer'),
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7154ed0b29131bb19b5471a6ceb9cab14c294e2d..0c96fcba7b07e074fa771b3ba684a52ab46f03a6 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -783,7 +783,7 @@ def project_level_readable_by?(user)
elsif project.personal? && project.team.owner?(user)
true
elsif confidential? && !assignee_or_author?(user)
- project.member?(user, Gitlab::Access::REPORTER)
+ project.member?(user, Gitlab::Access::PLANNER)
elsif project.public? || (project.internal? && !user.external?)
project.feature_available?(:issues, user)
else
@@ -796,7 +796,7 @@ def group_level_readable_by?(user)
return false unless namespace.is_a?(::Group)
if confidential? && !assignee_or_author?(user)
- namespace.member?(user, Gitlab::Access::REPORTER)
+ namespace.member?(user, Gitlab::Access::PLANNER)
else
namespace.member?(user)
end
diff --git a/app/models/member.rb b/app/models/member.rb
index b2164f307f43e16b87b144e6727b0aa50a630cda..ff4258630e734733fe51ed49e8c44eb3d39bda5b 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -164,6 +164,7 @@ class Member < ApplicationRecord
scope :has_access, -> { active.where('access_level > 0') }
scope :guests, -> { active.where(access_level: GUEST) }
+ scope :planners, -> { active.where(access_level: PLANNER) }
scope :reporters, -> { active.where(access_level: REPORTER) }
scope :developers, -> { active.where(access_level: DEVELOPER) }
scope :maintainers, -> { active.where(access_level: MAINTAINER) }
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 5aac606207883cbcd7cf772da877cd41c56754c3..2ef643e7e188df15a4927f58eda316fb6540ea48 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -11,6 +11,10 @@ def add_guest(user, current_user: nil)
add_member(user, :guest, current_user: current_user)
end
+ def add_planner(user, current_user: nil)
+ add_member(user, :planner, current_user: current_user)
+ end
+
def add_reporter(user, current_user: nil)
add_member(user, :reporter, current_user: current_user)
end
@@ -89,6 +93,10 @@ def guests
@guests ||= fetch_members(Gitlab::Access::GUEST)
end
+ def planners
+ @planners ||= fetch_members(Gitlab::Access::PLANNER)
+ end
+
def reporters
@reporters ||= fetch_members(Gitlab::Access::REPORTER)
end
@@ -152,6 +160,10 @@ def guest?(user)
max_member_access(user.id) == Gitlab::Access::GUEST
end
+ def planner?(user)
+ max_member_access(user.id) == Gitlab::Access::PLANNER
+ end
+
def reporter?(user)
max_member_access(user.id) == Gitlab::Access::REPORTER
end
diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb
index 8bcaf4a1e584a9108a95df4dec6a205ea1bf3c62..28fd9c0422cd8c5bf22019b2463f5cb892f83290 100644
--- a/app/models/system/broadcast_message.rb
+++ b/app/models/system/broadcast_message.rb
@@ -7,6 +7,7 @@ class BroadcastMessage < ApplicationRecord
ALLOWED_TARGET_ACCESS_LEVELS = [
Gitlab::Access::GUEST,
+ Gitlab::Access::PLANNER,
Gitlab::Access::REPORTER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::MAINTAINER,
diff --git a/app/models/user.rb b/app/models/user.rb
index 45c36c5c44ba7f0f27033e3721fb3485c69b0459..76695accaa3e6acedb51c906700b0f62c1c4a055 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1395,7 +1395,7 @@ def owned_projects
#
# This logic is duplicated from `Ability#project_abilities` into a SQL form.
def projects_where_can_admin_issues
- authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
+ authorized_projects(Gitlab::Access::PLANNER).non_archived.with_issues_enabled
end
# rubocop: disable CodeReuse/ServiceClass
diff --git a/app/models/users_statistics.rb b/app/models/users_statistics.rb
index a314ae8920bcc5214c244379d9ae797488c3d481..6b2261a0591aa4f8b6c5b9e8d8e9f90bf77bbbd7 100644
--- a/app/models/users_statistics.rb
+++ b/app/models/users_statistics.rb
@@ -7,6 +7,7 @@ def active
[
without_groups_and_projects,
with_highest_role_guest,
+ with_highest_role_planner,
with_highest_role_reporter,
with_highest_role_developer,
with_highest_role_maintainer,
@@ -34,6 +35,7 @@ def highest_role_stats
{
without_groups_and_projects: without_groups_and_projects_stats,
with_highest_role_guest: batch_count_for_access_level(Gitlab::Access::GUEST),
+ with_highest_role_planner: batch_count_for_access_level(Gitlab::Access::PLANNER),
with_highest_role_reporter: batch_count_for_access_level(Gitlab::Access::REPORTER),
with_highest_role_developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER),
with_highest_role_maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER),
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 6f1fb19277c005058a5a96f5fc72e26f504e3e58..ec2a7d2797ca2e97e1d49349eb8275fe42508c7b 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -13,6 +13,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
condition(:has_access) { access_level != GroupMember::NO_ACCESS }
condition(:guest) { access_level >= GroupMember::GUEST }
+ condition(:planner) { access_level == GroupMember::PLANNER }
condition(:developer) { access_level >= GroupMember::DEVELOPER }
condition(:owner) { access_level >= GroupMember::OWNER }
condition(:maintainer) { access_level >= GroupMember::MAINTAINER }
@@ -196,6 +197,11 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
rule { has_access }.enable :read_namespace_via_membership
+ rule { planner }.policy do
+ enable :planner_access
+ enable :read_confidential_issues
+ end
+
rule { developer }.policy do
enable :admin_metrics_dashboard_annotation
enable :create_custom_emoji
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index f1ec9b155c14b9ef9bd2cabd1cb21c4df7c614dc..b8f5580c7395eaf28821d218c4bf0ecf3e243762 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -9,7 +9,7 @@ class IssuePolicy < IssuablePolicy
desc "User can read confidential issues"
condition(:can_read_confidential) do
- @user && (@user.admin? || can?(:reporter_access) || assignee_or_author?) # rubocop:disable Cop/UserAdmin
+ @user && (@user.admin? || can?(:reporter_access) || can?(:planner_access) || assignee_or_author?) # rubocop:disable Cop/UserAdmin
end
desc "Project belongs to a group, crm is enabled and user can read contacts in the root group"
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 335459958ed93a8870f228540e84b8ba94267a65..16ff0cab7fb352057f7f942674ec8b57686388af 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -15,6 +15,9 @@ class ProjectPolicy < BasePolicy
desc "User has guest access"
condition(:guest) { team_member? }
+ desc "User has planner access"
+ condition(:planner) { team_access_level == Gitlab::Access::PLANNER }
+
desc "User has reporter access"
condition(:reporter) { team_access_level >= Gitlab::Access::REPORTER }
@@ -315,6 +318,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:read_all_resources) }.enable :read_confidential_issues
rule { guest }.enable :guest_access
+ rule { planner }.enable :planner_access
rule { reporter }.enable :reporter_access
rule { developer }.enable :developer_access
rule { maintainer }.enable :maintainer_access
@@ -389,6 +393,32 @@ class ProjectPolicy < BasePolicy
rule { guest & ~public_project }.enable :read_grafana
+ rule { can?(:planner_access) }.policy do
+ enable :guest_access
+ # enable :admin_issue_board
+ enable :admin_issue_board_list
+ enable :admin_issue
+ enable :update_issue
+ enable :destroy_issue
+ enable :read_confidential_issues
+ enable :reopen_issue
+ enable :create_design
+ enable :update_design
+ enable :move_design
+ enable :destroy_design
+ enable :read_merge_request
+ enable :download_code
+
+ # enable(*create_read_update_admin_destroy(:wiki))
+ enable :read_wiki
+ enable :create_wiki
+ # enable :admin_wiki
+ # enable :download_wiki_code
+ # enable :create_snippet
+ # enable :admin_label
+ # enable :admin_milestone
+ end
+
rule { can?(:reporter_access) }.policy do
enable :admin_issue_board
enable :download_code
diff --git a/app/views/admin/dashboard/_stats_users_table.html.haml b/app/views/admin/dashboard/_stats_users_table.html.haml
index a23674c79dbe6a04bc9d1702ecc7f876cf5c2547..c11a1547a0e4bf01b4f028f4a06807d6600a9395 100644
--- a/app/views/admin/dashboard/_stats_users_table.html.haml
+++ b/app/views/admin/dashboard/_stats_users_table.html.haml
@@ -1,49 +1,56 @@
%table.table.gl-text-gray-500.gl-w-full
%tr
- %td.gl-p-5!
+ %td{ class: '!gl-p-5' }
= s_('AdminArea|Users without a Group and Project')
= render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
- %td.gl-text-right{ class: 'gl-p-5!' }
+ %td.gl-text-right{ class: '!gl-p-5' }
= @users_statistics&.without_groups_and_projects
= render_if_exists 'admin/dashboard/minimal_access_stats_row', users_statistics: @users_statistics
+ %tr
+ %td{ class: '!gl-p-5' }
+ = s_('AdminArea|Users with highest role')
+ %strong
+ = s_('AdminArea|Planner')
+ %td.gl-text-right{ class: 'gl-p-5!' }
+ = @users_statistics&.with_highest_role_planner
%tr
%td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Reporter')
- %td.gl-text-right{ class: 'gl-p-5!' }
+ %td.gl-text-right{ class: '!gl-p-5' }
= @users_statistics&.with_highest_role_reporter
%tr
- %td.gl-p-5!
+ %td{ class: '!gl-p-5' }
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Developer')
- %td.gl-text-right{ class: 'gl-p-5!' }
+ %td.gl-text-right{ class: '!gl-p-5' }
= @users_statistics&.with_highest_role_developer
%tr
- %td.gl-p-5!
+ %td{ class: '!gl-p-5' }
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Maintainer')
- %td.gl-text-right{ class: 'gl-p-5!' }
+ %td.gl-text-right{ class: '!gl-p-5' }
= @users_statistics&.with_highest_role_maintainer
%tr
- %td.gl-p-5!
+ %td{ class: '!gl-p-5' }
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Owner')
- %td.gl-text-right{ class: 'gl-p-5!' }
+ %td.gl-text-right{ class: '!gl-p-5' }
= @users_statistics&.with_highest_role_owner
%tr
- %td.gl-p-5!
+ %td{ class: '!gl-p-5' }
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Guest')
= render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
- %td.gl-text-right{ class: 'gl-p-5!' }
+ %td.gl-text-right{ class: '!gl-p-5' }
= @users_statistics&.with_highest_role_guest
%tr
- %td.gl-p-5!
+ %td{ class: '!gl-p-5' }
= s_('AdminArea|Bots')
- %td.gl-text-right{ class: 'gl-p-5!' }
+ %td.gl-text-right{ class: '!gl-p-5' }
= @users_statistics&.bots
diff --git a/db/migrate/20240813114601_add_with_highest_role_planner_to_users_statistics.rb b/db/migrate/20240813114601_add_with_highest_role_planner_to_users_statistics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8e0a52a5cc2ebf927d677cc09025dd6bfe07ae10
--- /dev/null
+++ b/db/migrate/20240813114601_add_with_highest_role_planner_to_users_statistics.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddWithHighestRolePlannerToUsersStatistics < Gitlab::Database::Migration[2.2]
+ milestone '17.4'
+
+ def change
+ add_column :users_statistics, :with_highest_role_planner, :integer, default: 0, null: false
+ end
+end
diff --git a/db/schema_migrations/20240813114601 b/db/schema_migrations/20240813114601
new file mode 100644
index 0000000000000000000000000000000000000000..2c76495e5f83e255a5d5b2120d104165b4619e9f
--- /dev/null
+++ b/db/schema_migrations/20240813114601
@@ -0,0 +1 @@
+5cbebbd84d69b79e5dda55934981cb48f766d7272b44a0857522adbb0c552c71
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6a48955a94fa53a6b9612a23a12a797d59a48ea8..569275b86ff59f6711ebae3eab7e4e2418cd600f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -19178,7 +19178,8 @@ CREATE TABLE users_statistics (
bots integer DEFAULT 0 NOT NULL,
blocked integer DEFAULT 0 NOT NULL,
with_highest_role_minimal_access integer DEFAULT 0 NOT NULL,
- with_highest_role_guest_with_custom_role integer DEFAULT 0 NOT NULL
+ with_highest_role_guest_with_custom_role integer DEFAULT 0 NOT NULL,
+ with_highest_role_planner integer DEFAULT 0 NOT NULL
);
CREATE SEQUENCE users_statistics_id_seq
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index f54ab008a4f76ad9d7626bf8b8ac2a0684c5998a..af04f510671528a16453df278d265b6241e55743 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -34639,6 +34639,7 @@ Access level to a resource.
| `MINIMAL_ACCESS` | Minimal access. |
| `NO_ACCESS` | No access. |
| `OWNER` | Owner access. |
+| `PLANNER` | Planner access. |
| `REPORTER` | Reporter access. |
### `AgentTokenStatus`
@@ -36257,6 +36258,7 @@ Access level of a group or project member.
| `MAINTAINER` | Maintainer access. |
| `MINIMAL_ACCESS` | Minimal access. |
| `OWNER` | Owner access. |
+| `PLANNER` | Planner access. |
| `REPORTER` | Reporter access. |
### `MemberAccessLevelName`
@@ -36269,6 +36271,7 @@ Name of access levels of a group or project member.
| `GUEST` | Guest access. |
| `MAINTAINER` | Maintainer access. |
| `OWNER` | Owner access. |
+| `PLANNER` | Planner access. |
| `REPORTER` | Reporter access. |
### `MemberApprovalStatusType`
diff --git a/ee/app/models/ee/users_statistics.rb b/ee/app/models/ee/users_statistics.rb
index 0b1f4937bbb6ef7653a79c2b157e7312fa6acd71..06c6f8478c923622787dd0edc1409b4a6c873900 100644
--- a/ee/app/models/ee/users_statistics.rb
+++ b/ee/app/models/ee/users_statistics.rb
@@ -26,6 +26,7 @@ def active
def base_billable_users
[
+ with_highest_role_planner,
with_highest_role_reporter,
with_highest_role_developer,
with_highest_role_maintainer,
diff --git a/ee/app/views/admin/dashboard/_stats_users_table.html.haml b/ee/app/views/admin/dashboard/_stats_users_table.html.haml
index 79b63d671e3e44311a9d1c9e00e1b42832e262e7..cc86971c6c843256bf31fd9985c76b2d4dc3cfe5 100644
--- a/ee/app/views/admin/dashboard/_stats_users_table.html.haml
+++ b/ee/app/views/admin/dashboard/_stats_users_table.html.haml
@@ -17,6 +17,13 @@
= s_('AdminArea|Reporter')
%td.gl-text-right{ class: '!gl-p-5' }
= @users_statistics&.with_highest_role_reporter
+ %tr
+ %td{ class: '!gl-p-5' }
+ = s_('AdminArea|Users with highest role')
+ %strong
+ = s_('AdminArea|Planner')
+ %td.gl-text-right{ class: '!gl-p-5' }
+ = @users_statistics&.with_highest_role_planner
%tr
%td{ class: '!gl-p-5' }
= s_('AdminArea|Users with highest role')
diff --git a/ee/bin/custom-ability b/ee/bin/custom-ability
index cf30a725d7389e0ac0f02ab3b1a9825acaedc0a6..8b9f35f215356d830af44f259d8b23d57d7583ee 100755
--- a/ee/bin/custom-ability
+++ b/ee/bin/custom-ability
@@ -21,6 +21,7 @@ require_relative '../../lib/gitlab/popen'
LEVELS = {
'Not applicable' => 0,
'Guest' => 10,
+ 'Planner' => 15,
'Reporter' => 20,
'Developer' => 30,
'Maintainer' => 40,
diff --git a/ee/spec/frontend/members/components/table/drawer/role_details_drawer_spec.js b/ee/spec/frontend/members/components/table/drawer/role_details_drawer_spec.js
index cd7f7c58ebb2ea2ddd34eac7121c56134b0695b6..b21c36eef796ea80d8199fdbf337f36421c277b4 100644
--- a/ee/spec/frontend/members/components/table/drawer/role_details_drawer_spec.js
+++ b/ee/spec/frontend/members/components/table/drawer/role_details_drawer_spec.js
@@ -16,7 +16,9 @@ import {
describe('Role details drawer', () => {
const { permissions } = updateableCustomRoleMember.customRoles[1];
const dropdownItems = roleDropdownItems(updateableCustomRoleMember);
- const currentRole = dropdownItems.flatten[7];
+ const currentRole = dropdownItems.flatten.find(
+ (role) => role.memberRoleId === updateableCustomRoleMember.accessLevel.memberRoleId,
+ );
let wrapper;
const createWrapper = ({ member = updateableCustomRoleMember } = {}) => {
diff --git a/ee/spec/frontend/members/components/table/max_role_spec.js b/ee/spec/frontend/members/components/table/max_role_spec.js
index 46e83e896ca749ab5c5b4ca707a913876ca2636a..4975d65ecfe9e70c27052e18a63c5cdc650d697b 100644
--- a/ee/spec/frontend/members/components/table/max_role_spec.js
+++ b/ee/spec/frontend/members/components/table/max_role_spec.js
@@ -182,9 +182,11 @@ describe('MaxRole', () => {
createComponent();
expect(findListbox().props('items')[0].text).toBe('Standard roles');
- expect(findListbox().props('items')[0].options).toHaveLength(6);
+ expect(findListbox().props('items')[0].options).toHaveLength(
+ Object.keys(member.validRoles).length,
+ );
expect(findListbox().props('items')[1].text).toBe('Custom roles');
- expect(findListbox().props('items')[1].options).toHaveLength(3);
+ expect(findListbox().props('items')[1].options).toHaveLength(member.customRoles.length);
});
it('calls `updateMemberRole` Vuex action', async () => {
diff --git a/ee/spec/models/ee/users_statistics_spec.rb b/ee/spec/models/ee/users_statistics_spec.rb
index 08c8f2fd24b44681de45fcba485ddcd1ffe6bd21..33cb7c3d810e4a37ea1347bedfee8a7d8be3d038 100644
--- a/ee/spec/models/ee/users_statistics_spec.rb
+++ b/ee/spec/models/ee/users_statistics_spec.rb
@@ -9,7 +9,7 @@
describe '#billable' do
it 'sums users statistics values excluding blocked users and bots' do
- expect(users_statistics.billable).to eq(74)
+ expect(users_statistics.billable).to eq(79)
end
context 'when there is an ultimate license' do
@@ -19,14 +19,14 @@
end
it 'excludes blocked users, bots, guest users, users without a group or project and minimal access users' do
- expect(users_statistics.billable).to eq(43)
+ expect(users_statistics.billable).to eq(48)
end
end
end
describe '#active' do
it 'includes minimal access roles' do
- expect(users_statistics.active).to eq(76)
+ expect(users_statistics.active).to eq(81)
end
end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index a95431b47e7b33aa912d8ffdf1fe5c65eb1d3e37..c7eaefc58d3dcc1146143bedd4e46d3dff5dcbc8 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -12,6 +12,7 @@ module Access
NO_ACCESS = 0
MINIMAL_ACCESS = 5
GUEST = 10
+ PLANNER = 15
REPORTER = 20
DEVELOPER = 30
MAINTAINER = 40
@@ -44,6 +45,7 @@ def all_values
def options
{
"Guest" => GUEST,
+ "Planner" => PLANNER,
"Reporter" => REPORTER,
"Developer" => DEVELOPER,
"Maintainer" => MAINTAINER
@@ -65,6 +67,7 @@ def options_with_none
def sym_options
{
guest: GUEST,
+ planner: PLANNER,
reporter: REPORTER,
developer: DEVELOPER,
maintainer: MAINTAINER
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1b3c44b7771a30e3ad84c38aec16d22dea7a1d06..fc50bf7e90de0f064d3493a61cd6fb7eac103723 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3718,6 +3718,9 @@ msgstr ""
msgid "AdminArea|Owner"
msgstr ""
+msgid "AdminArea|Planner"
+msgstr ""
+
msgid "AdminArea|Projects"
msgstr ""
@@ -32746,6 +32749,9 @@ msgstr ""
msgid "MemberRole|The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners."
msgstr ""
+msgid "MemberRole|The Planner role is for users who need to manage projects."
+msgstr ""
+
msgid "MemberRole|The Reporter role is suitable for team members who need to stay informed about a project or group but do not actively contribute code."
msgstr ""
@@ -39885,6 +39891,9 @@ msgstr ""
msgid "Plan:"
msgstr ""
+msgid "Planner"
+msgstr ""
+
msgid "Planning hierarchy"
msgstr ""
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index d9bca85fba629f6c94b599dc8589fdd6e2bb0f17..7a1c4ec35f798bf5fed7296cddd616194c2610ba 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -8,6 +8,7 @@
user
trait(:guest) { access_level { GroupMember::GUEST } }
+ trait(:planner) { access_level { GroupMember::PLANNER } }
trait(:reporter) { access_level { GroupMember::REPORTER } }
trait(:developer) { access_level { GroupMember::DEVELOPER } }
trait(:maintainer) { access_level { GroupMember::MAINTAINER } }
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index a039a626efa9584890559cd8542ebfafb707f031..18cdd01bf2798f7b1e86ebefe23a92dc2b3275ff 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -8,6 +8,7 @@
maintainer
trait(:guest) { access_level { ProjectMember::GUEST } }
+ trait(:planner) { access_level { ProjectMember::PLANNER } }
trait(:reporter) { access_level { ProjectMember::REPORTER } }
trait(:developer) { access_level { ProjectMember::DEVELOPER } }
trait(:maintainer) { access_level { ProjectMember::MAINTAINER } }
diff --git a/spec/factories/user_highest_roles.rb b/spec/factories/user_highest_roles.rb
index ee5794b55fb14a7be1bd6ff94c8fe174f06128a0..04e3e344b4c4e02ef34d58fd960690676708f8e0 100644
--- a/spec/factories/user_highest_roles.rb
+++ b/spec/factories/user_highest_roles.rb
@@ -6,6 +6,7 @@
user
trait(:guest) { highest_access_level { GroupMember::GUEST } }
+ trait(:planner) { highest_access_level { GroupMember::PLANNER } }
trait(:reporter) { highest_access_level { GroupMember::REPORTER } }
trait(:developer) { highest_access_level { GroupMember::DEVELOPER } }
trait(:maintainer) { highest_access_level { GroupMember::MAINTAINER } }
diff --git a/spec/factories/users_statistics.rb b/spec/factories/users_statistics.rb
index 07699dc38b2e820ad1d352dbbaf66343416b7bcb..f829d88684f0d7b6d71c1455878c0f1069e657f2 100644
--- a/spec/factories/users_statistics.rb
+++ b/spec/factories/users_statistics.rb
@@ -4,6 +4,7 @@
factory :users_statistics do
without_groups_and_projects { 23 }
with_highest_role_guest { 5 }
+ with_highest_role_planner { 5 }
with_highest_role_reporter { 9 }
with_highest_role_developer { 21 }
with_highest_role_maintainer { 6 }
diff --git a/spec/frontend/members/components/table/drawer/role_details_drawer_spec.js b/spec/frontend/members/components/table/drawer/role_details_drawer_spec.js
index d81bc13260172e40e431d1b6c564cbb43468c3b0..9cd31b944ad108c21116f70e85b20c6168833780 100644
--- a/spec/frontend/members/components/table/drawer/role_details_drawer_spec.js
+++ b/spec/frontend/members/components/table/drawer/role_details_drawer_spec.js
@@ -16,7 +16,9 @@ jest.mock('~/lib/utils/dom_utils', () => ({
describe('Role details drawer', () => {
const dropdownItems = roleDropdownItems(updateableMember);
- const currentRole = dropdownItems.flatten[5];
+ const currentRole = dropdownItems.flatten.find(
+ (role) => role.accessLevel === updateableMember.accessLevel.integerValue,
+ );
const newRole = dropdownItems.flatten[2];
const saveRoleStub = jest.fn();
let wrapper;
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 416666e2a143b887dc073147c5d738cf61163ff1..eeef01576e484f6d268a9678e10222463d1a622d 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -46,6 +46,7 @@ export const member = {
validRoles: {
'Minimal Access': 5,
Guest: 10,
+ Planner: 15,
Reporter: 20,
Developer: 30,
Maintainer: 40,
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 70f843be0e1789f1fa861eeba31a79367be436f9..891d349b6c465a5b28deec426781db848c8c95b6 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -113,6 +113,7 @@
it 'returns Gitlab::Access.options' do
expect(access_levels).to eq({
"Guest" => 10,
+ "Planner" => 15,
"Reporter" => 20,
"Developer" => 30
})
diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb
index d1ca9ff5125258d0ef39b0a977910175541422ea..794b77812d12613cd7a3a105cf3f2eccda114801 100644
--- a/spec/models/users_statistics_spec.rb
+++ b/spec/models/users_statistics_spec.rb
@@ -36,6 +36,7 @@
before do
create_list(:user_highest_role, 1)
create_list(:user_highest_role, 2, :guest)
+ create_list(:user_highest_role, 2, :planner)
create_list(:user_highest_role, 2, :reporter)
create_list(:user_highest_role, 2, :developer)
create_list(:user_highest_role, 2, :maintainer)
@@ -51,6 +52,7 @@
expect(described_class.create_current_stats!).to have_attributes(
without_groups_and_projects: 1,
with_highest_role_guest: 2,
+ with_highest_role_planner: 2,
with_highest_role_reporter: 2,
with_highest_role_developer: 2,
with_highest_role_maintainer: 2,
@@ -72,13 +74,13 @@
describe '#active' do
it 'sums users statistics values without the value for blocked' do
- expect(users_statistics.active).to eq(71)
+ expect(users_statistics.active).to eq(76)
end
end
describe '#total' do
it 'sums all users statistics values' do
- expect(users_statistics.total).to eq(78)
+ expect(users_statistics.total).to eq(83)
end
end
end