diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index c793d86618216466fee5869e6d06b5ade9dbeb3c..c173ae40879f7926ccd7770d8d60e695056d0b7b 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -63,7 +63,8 @@ def preferences_param_names :markdown_surround_selection, :markdown_automatic_lists, :use_new_navigation, - :enabled_following + :enabled_following, + :use_work_items_view ] end end diff --git a/app/graphql/mutations/user_preferences/update.rb b/app/graphql/mutations/user_preferences/update.rb index a87ad0cead0b58c1277360ad8dd2714fd71d21a3..d051ab82171f6ac29ebea1e4a7dcd2f6c384067e 100644 --- a/app/graphql/mutations/user_preferences/update.rb +++ b/app/graphql/mutations/user_preferences/update.rb @@ -9,7 +9,8 @@ class Update < BaseMutation :extensions_marketplace_opt_in_status, :organization_groups_projects_display, :use_web_ide_extension_marketplace, - :visibility_pipeline_id_type + :visibility_pipeline_id_type, + :use_work_items_view ].freeze argument :extensions_marketplace_opt_in_status, Types::ExtensionsMarketplaceOptInStatusEnum, @@ -21,6 +22,9 @@ class Update < BaseMutation argument :use_web_ide_extension_marketplace, GraphQL::Types::Boolean, required: false, description: 'Whether Web IDE Extension Marketplace is enabled for the user.' + argument :use_work_items_view, GraphQL::Types::Boolean, + required: false, + description: 'Use work item view instead of legacy issue view.' argument :visibility_pipeline_id_type, Types::VisibilityPipelineIdTypeEnum, required: false, description: 'Determines whether the pipeline list shows ID or IID.' diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb index 40999aa79c33e0647dce5ca3b23afdfbc8550555..c05e43a85efd948ad8796002a1f048206f5f36b5 100644 --- a/app/graphql/types/user_interface.rb +++ b/app/graphql/types/user_interface.rb @@ -31,7 +31,7 @@ module UserInterface null: false, resolver_method: :redacted_name, description: 'Human-readable name of the user. ' \ - 'Returns `****` if the user is a project bot and the requester does not have permission to view the project.' + 'Returns `****` if the user is a project bot and the requester does not have permission to view the project.' field :state, type: Types::UserStateEnum, @@ -151,6 +151,11 @@ module UserInterface field :gitpod_enabled, GraphQL::Types::Boolean, null: true, description: 'Whether Gitpod is enabled at the user level.' + field :user_preferences, ::Types::UserPreferencesType, + null: true, + description: 'Preferences for the user.', + method: :user_preference + field :preferences_gitpod_path, GraphQL::Types::String, null: true, @@ -164,7 +169,7 @@ module UserInterface null: true, alpha: { milestone: '15.10' }, description: "Achievements for the user. " \ - "Only returns for namespaces where the `achievements` feature flag is enabled.", + "Only returns for namespaces where the `achievements` feature flag is enabled.", extras: [:lookahead], resolver: ::Resolvers::Achievements::UserAchievementsForUserResolver diff --git a/app/graphql/types/user_preferences_type.rb b/app/graphql/types/user_preferences_type.rb index f77bba6b651730f0df6b12f1ff59158f136f0e5c..586e9e302cbd0948648372b9bb3c4e6da6764e06 100644 --- a/app/graphql/types/user_preferences_type.rb +++ b/app/graphql/types/user_preferences_type.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes - # Only used to render the current user's own preferences class UserPreferencesType < BaseObject graphql_name 'UserPreferences' + authorize :read_user_preference + alias_method :user_preference, :object field :extensions_marketplace_opt_in_status, Types::ExtensionsMarketplaceOptInStatusEnum, @@ -20,12 +20,16 @@ class UserPreferencesType < BaseObject description: 'Determines whether the pipeline list shows ID or IID.', null: true + # rubocop:disable GraphQL/ExtractType -- These are stored as user preferences field :use_web_ide_extension_marketplace, GraphQL::Types::Boolean, description: 'Whether Web IDE Extension Marketplace is enabled for the user.', null: false, deprecated: { reason: 'Use `extensions_marketplace_opt_in_status` instead', milestone: '16.11' } - # rubocop:disable GraphQL/ExtractType -- These are stored as user preferences + field :use_work_items_view, GraphQL::Types::Boolean, + description: 'Use work item view instead of legacy issue view.', + null: true + field :organization_groups_projects_sort, Types::Organizations::GroupsProjectsSortEnum, description: 'Sort order for organization groups and projects.', diff --git a/app/models/user.rb b/app/models/user.rb index fc3ab15bb17c898ce2368e5923faa09d32546bb9..7ed601db4715e2a49f8c42cc8a80a4af23cb4421 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -434,6 +434,7 @@ def update_tracked_fields!(request) :enabled_following, :enabled_following=, :home_organization, :home_organization_id, :home_organization_id=, :dpop_enabled, :dpop_enabled=, + :use_work_items_view, :use_work_items_view=, to: :user_preference delegate :path, to: :namespace, allow_nil: true, prefix: true diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index ccab3d9f02d5d3eaabf482fe9df9fc359d6373b2..293fb86f760d56bade095f4d5b74ea788af2613c 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -34,6 +34,7 @@ class UserPolicy < BasePolicy enable :read_user_email_address enable :admin_user_email_address enable :make_profile_private + enable :read_user_preference end rule { default }.enable :read_user_profile diff --git a/app/policies/user_preference_policy.rb b/app/policies/user_preference_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..eef67684122d601b42f3b646d17cd6f2e0a573b4 --- /dev/null +++ b/app/policies/user_preference_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# rubocop:disable Gitlab/BoundedContexts -- Updating this would involve updating multiple dependencies, so should be done with the User module +class UserPreferencePolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass -- Updating this would involve updating multiple dependencies, so should be done with the User module + delegate { @subject.user } +end +# rubocop:enable Gitlab/BoundedContexts diff --git a/db/migrate/20240522141913_add_use_work_items_view_to_user_preferences.rb b/db/migrate/20240522141913_add_use_work_items_view_to_user_preferences.rb new file mode 100644 index 0000000000000000000000000000000000000000..6424d0cdf48104c8f8d78fa376bed1857fa33c7b --- /dev/null +++ b/db/migrate/20240522141913_add_use_work_items_view_to_user_preferences.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddUseWorkItemsViewToUserPreferences < Gitlab::Database::Migration[2.2] + milestone '17.4' + + def change + add_column :user_preferences, :use_work_items_view, :boolean, default: false, null: false + end +end diff --git a/db/schema_migrations/20240522141913 b/db/schema_migrations/20240522141913 new file mode 100644 index 0000000000000000000000000000000000000000..a001dccb7f9b732e7d3b99e55b1da0b934671ccf --- /dev/null +++ b/db/schema_migrations/20240522141913 @@ -0,0 +1 @@ +4d360baf0e12d723ebd34059b3dc44039573d1bfcaf0a3bd1edfab360191f3db \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 6ca4b98c2d638887c4ee4a710b4d1b548a112cbc..9d7ff768f0152328996d56e1d436684e3b895d94 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19290,6 +19290,7 @@ CREATE TABLE user_preferences ( organization_groups_projects_sort text, organization_groups_projects_display smallint DEFAULT 0 NOT NULL, dpop_enabled boolean DEFAULT false NOT NULL, + use_work_items_view boolean DEFAULT false NOT NULL, CONSTRAINT check_1d670edc68 CHECK ((time_display_relative IS NOT NULL)), CONSTRAINT check_89bf269f41 CHECK ((char_length(diffs_deletion_color) <= 7)), CONSTRAINT check_b1306f8875 CHECK ((char_length(organization_groups_projects_sort) <= 64)), diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1e2cdd3d7d1f1c3a96e3ec22a83078d6804339f1..83fff96f28f5343079fae498b21a67d00f4dbe8a 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -10014,6 +10014,7 @@ Input type: `UserPreferencesUpdateInput` | `organizationGroupsProjectsDisplay` **{warning-solid}** | [`OrganizationGroupProjectDisplay`](#organizationgroupprojectdisplay) | **Deprecated:** **Status**: Experiment. Introduced in GitLab 17.2. | | `organizationGroupsProjectsSort` **{warning-solid}** | [`OrganizationGroupProjectSort`](#organizationgroupprojectsort) | **Deprecated:** **Status**: Experiment. Introduced in GitLab 17.2. | | `useWebIdeExtensionMarketplace` | [`Boolean`](#boolean) | Whether Web IDE Extension Marketplace is enabled for the user. | +| `useWorkItemsView` | [`Boolean`](#boolean) | Use work item view instead of legacy issue view. | | `visibilityPipelineIdType` | [`VisibilityPipelineIdType`](#visibilitypipelineidtype) | Determines whether the pipeline list shows ID or IID. | #### Fields @@ -16895,6 +16896,7 @@ A user with add-on data. | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | @@ -17766,6 +17768,7 @@ Core representation of a GitLab user. | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | @@ -20072,6 +20075,7 @@ The currently authenticated GitLab user. | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | @@ -25640,6 +25644,7 @@ A user assigned to a merge request. | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | @@ -26019,6 +26024,7 @@ The author of the merge request. | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | @@ -26444,6 +26450,7 @@ A user participating in a merge request. | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | @@ -26842,6 +26849,7 @@ A user assigned to a merge request as a reviewer. | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | @@ -33201,6 +33209,7 @@ Core representation of a GitLab user. | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | @@ -33574,6 +33583,7 @@ fields relate to interactions between the two entities. | `organizationGroupsProjectsDisplay` **{warning-solid}** | [`OrganizationGroupProjectDisplay!`](#organizationgroupprojectdisplay) | **Introduced** in GitLab 17.2. **Status**: Experiment. Default list view for organization groups and projects. | | `organizationGroupsProjectsSort` **{warning-solid}** | [`OrganizationGroupProjectSort`](#organizationgroupprojectsort) | **Introduced** in GitLab 17.2. **Status**: Experiment. Sort order for organization groups and projects. | | `useWebIdeExtensionMarketplace` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in GitLab 16.11. Use `extensions_marketplace_opt_in_status` instead. | +| `useWorkItemsView` | [`Boolean`](#boolean) | Use work item view instead of legacy issue view. | | `visibilityPipelineIdType` | [`VisibilityPipelineIdType`](#visibilitypipelineidtype) | Determines whether the pipeline list shows ID or IID. | ### `UserStatus` @@ -40058,6 +40068,7 @@ Implementations: | `status` | [`UserStatus`](#userstatus) | User status. | | `twitter` | [`String`](#string) | X (formerly Twitter) username of the user. | | `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. | +| `userPreferences` | [`UserPreferences`](#userpreferences) | Preferences for the user. | | `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. | | `webPath` | [`String!`](#string) | Web path of the user. | | `webUrl` | [`String!`](#string) | Web URL of the user. | diff --git a/spec/graphql/types/user_preferences_type_spec.rb b/spec/graphql/types/user_preferences_type_spec.rb index 622605c35accefb4e2bc717bf4743f59d25e8333..919a8e08e340f6fe1203c39a65619c9ac1546bd8 100644 --- a/spec/graphql/types/user_preferences_type_spec.rb +++ b/spec/graphql/types/user_preferences_type_spec.rb @@ -8,6 +8,7 @@ it 'exposes the expected fields' do expected_fields = %i[ issues_sort + use_work_items_view visibility_pipeline_id_type use_web_ide_extension_marketplace extensions_marketplace_opt_in_status diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb index 3ab7e797231498b249936af9bc149ec343a19ade..3fa592f91605012aed3a37dab4a8e20cb0c34abd 100644 --- a/spec/graphql/types/user_type_spec.rb +++ b/spec/graphql/types/user_type_spec.rb @@ -61,6 +61,7 @@ lastActivityOn pronouns ide + userPreferences ] expect(described_class).to include_graphql_fields(*expected_fields) @@ -94,7 +95,9 @@ GQL end - subject(:user_name) { GitlabSchema.execute(query, context: { current_user: current_user }).as_json.dig('data', 'user', 'name') } + subject(:user_name) do + GitlabSchema.execute(query, context: { current_user: current_user }).as_json.dig('data', 'user', 'name') + end context 'user requests' do let(:current_user) { user } @@ -386,4 +389,12 @@ end end end + + describe 'userPreferences field' do + subject { described_class.fields['userPreferences'] } + + it 'returns userPreferences field' do + is_expected.to have_graphql_type(Types::UserPreferencesType) + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index caa23a5bb638a9c517dd60981154c3128cc43c98..4b470fa0e133c28c224b28fb40cc7d75c8e9ea4f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -111,6 +111,9 @@ it { is_expected.to delegate_method(:dpop_enabled).to(:user_preference) } it { is_expected.to delegate_method(:dpop_enabled=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:use_work_items_view).to(:user_preference) } + it { is_expected.to delegate_method(:use_work_items_view=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil } diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index 200c69484a9234cc825db18ca1a06aa2668054e9..3f0eb09f19c0ecac05ec009e67c4b7fcd28d6b8e 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -90,7 +90,7 @@ subject { described_class.new(current_user, current_user) } context 'when current_user is not blocked' do - it { is_expected.to be_allowed(:get_user_associations_count ) } + it { is_expected.to be_allowed(:get_user_associations_count) } end context 'when current_user is blocked' do @@ -112,7 +112,7 @@ subject { described_class.new(current_user, current_user) } context 'when current_user is not blocked' do - it { is_expected.to be_allowed(:get_user_associations_count ) } + it { is_expected.to be_allowed(:get_user_associations_count) } end context 'when current_user is blocked' do @@ -302,4 +302,30 @@ end end end + + describe ':read_user_preference' do + context 'when user is admin' do + let(:current_user) { admin } + + context 'when admin mode is enabled', :enable_admin_mode do + it { is_expected.to be_allowed(:read_user_preference) } + end + + context 'when admin mode is disabled' do + it { is_expected.not_to be_allowed(:read_user_preference) } + end + end + + context 'when user is not an admin' do + context 'requesting their own' do + subject { described_class.new(current_user, current_user) } + + it { is_expected.to be_allowed(:read_user_preference) } + end + + context "requesting a different user's" do + it { is_expected.not_to be_allowed(:read_user_preference) } + end + end + end end diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb index b0c8fc7e2ba49e842d1da48699f0eda722d999db..d74b788ec78b6ddc7fd97f697b7b17f586979b1a 100644 --- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb +++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb @@ -16,7 +16,8 @@ 'organizationGroupsProjectsDisplay' => 'GROUPS', 'organizationGroupsProjectsSort' => 'NAME_DESC', 'visibilityPipelineIdType' => 'IID', - 'useWebIdeExtensionMarketplace' => true + 'useWebIdeExtensionMarketplace' => true, + 'useWorkItemsView' => true } end @@ -34,12 +35,14 @@ expect(mutation_response['userPreferences']['organizationGroupsProjectsSort']).to eq('NAME_DESC') expect(mutation_response['userPreferences']['visibilityPipelineIdType']).to eq('IID') expect(mutation_response['userPreferences']['useWebIdeExtensionMarketplace']).to eq(true) + expect(mutation_response['userPreferences']['useWorkItemsView']).to eq(true) expect(current_user.user_preference.persisted?).to eq(true) expect(current_user.user_preference.extensions_marketplace_opt_in_status).to eq('enabled') expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid') expect(current_user.user_preference.use_web_ide_extension_marketplace).to eq(false) + expect(current_user.user_preference.use_work_items_view).to eq(true) end end @@ -51,7 +54,8 @@ organization_groups_projects_display: Types::Organizations::GroupsProjectsDisplayEnum.values['GROUPS'].value, organization_groups_projects_sort: 'NAME_DESC', visibility_pipeline_id_type: 'id', - use_web_ide_extension_marketplace: false + use_web_ide_extension_marketplace: false, + use_work_items_view: false } end @@ -72,6 +76,7 @@ expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid') + expect(current_user.user_preference.use_work_items_view).to eq(true) end context 'when input has nil attributes' do @@ -82,7 +87,8 @@ 'organizationGroupsProjectsDisplay' => nil, 'organizationGroupsProjectsSort' => nil, 'visibilityPipelineIdType' => nil, - 'useWebIdeExtensionMarketplace' => nil + 'useWebIdeExtensionMarketplace' => nil, + 'useWorkItemsView' => nil } end @@ -101,7 +107,8 @@ organization_groups_projects_display: init_user_preference[:organization_groups_projects_display], extensions_marketplace_opt_in_status: init_user_preference[:extensions_marketplace_opt_in_status], visibility_pipeline_id_type: init_user_preference[:visibility_pipeline_id_type], - use_web_ide_extension_marketplace: init_user_preference[:use_web_ide_extension_marketplace] + use_web_ide_extension_marketplace: init_user_preference[:use_web_ide_extension_marketplace], + use_work_items_view: init_user_preference[:use_work_items_view] }) end end diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb index 137aff63e9e188142e5b052581d0f1fd78427656..a516e22015d10749164fe426220b82e0bd648e88 100644 --- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb @@ -57,6 +57,7 @@ lastActivityOn pronouns ide + userPreferences ] # TODO: 'workspaces' needs to be included, but only when this spec is run in EE context, to account for the