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