Commit 74d35955 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖
Browse files

Add latest changes from gitlab-org/gitlab@master

parent c9337409
......@@ -22,7 +22,7 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown {
ajaxFilterConfig() {
return {
endpoint: `${gon.relative_url_root || ''}${this.endpoint}`,
endpoint: this.endpoint,
searchKey: 'search',
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
......@@ -33,9 +33,11 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown {
}
itemClicked(e) {
super.itemClicked(e, selected =>
selected.querySelector('.dropdown-light-content').innerText.trim(),
);
super.itemClicked(e, selected => {
const title = selected.querySelector('.dropdown-light-content').innerText.trim();
return DropdownUtils.getEscapedText(title);
});
}
renderContent(forceShowList = false) {
......
......@@ -5,7 +5,7 @@ export default class DropdownUser extends DropdownAjaxFilter {
constructor(options = {}) {
super({
...options,
endpoint: '/-/autocomplete/users.json',
endpoint: `${gon.relative_url_root || ''}/-/autocomplete/users.json`,
symbol: '@',
});
}
......
......@@ -12,6 +12,7 @@ export default class FilteredSearchDropdownManager {
runnerTagsEndpoint = '',
labelsEndpoint = '',
milestonesEndpoint = '',
iterationsEndpoint = '',
releasesEndpoint = '',
environmentsEndpoint = '',
epicsEndpoint = '',
......@@ -28,6 +29,7 @@ export default class FilteredSearchDropdownManager {
this.runnerTagsEndpoint = removeTrailingSlash(runnerTagsEndpoint);
this.labelsEndpoint = removeTrailingSlash(labelsEndpoint);
this.milestonesEndpoint = removeTrailingSlash(milestonesEndpoint);
this.iterationsEndpoint = removeTrailingSlash(iterationsEndpoint);
this.releasesEndpoint = removeTrailingSlash(releasesEndpoint);
this.epicsEndpoint = removeTrailingSlash(epicsEndpoint);
this.environmentsEndpoint = removeTrailingSlash(environmentsEndpoint);
......
......@@ -52,16 +52,24 @@ export default class FilteredSearchManager {
this.placeholder = placeholder;
this.anchor = anchor;
const { multipleAssignees } = this.filteredSearchInput.dataset;
const {
multipleAssignees,
epicsEndpoint,
iterationsEndpoint,
} = this.filteredSearchInput.dataset;
if (multipleAssignees && this.filteredSearchTokenKeys.enableMultipleAssignees) {
this.filteredSearchTokenKeys.enableMultipleAssignees();
}
const { epicsEndpoint } = this.filteredSearchInput.dataset;
if (!epicsEndpoint && this.filteredSearchTokenKeys.removeEpicToken) {
this.filteredSearchTokenKeys.removeEpicToken();
}
if (!iterationsEndpoint && this.filteredSearchTokenKeys.removeIterationToken) {
this.filteredSearchTokenKeys.removeIterationToken();
}
this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
......@@ -112,6 +120,7 @@ export default class FilteredSearchManager {
releasesEndpoint = '',
environmentsEndpoint = '',
epicsEndpoint = '',
iterationsEndpoint = '',
} = this.filteredSearchInput.dataset;
this.dropdownManager = new FilteredSearchDropdownManager({
......@@ -121,6 +130,7 @@ export default class FilteredSearchManager {
releasesEndpoint,
environmentsEndpoint,
epicsEndpoint,
iterationsEndpoint,
tokenizer: this.tokenizer,
page: this.page,
isGroup: this.isGroup,
......
......@@ -9,4 +9,8 @@
export const getIdFromGraphQLId = (gid = '') =>
parseInt((gid || '').replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null;
export default {};
export const MutationOperationMode = {
Append: 'APPEND',
Remove: 'REMOVE',
Replace: 'REPLACE',
};
<script>
import $ from 'jquery';
import { difference, union } from 'lodash';
import flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { camelCase, difference, union } from 'lodash';
import updateIssueLabelsMutation from '~/boards/queries/issue_set_labels.mutation.graphql';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import { toLabelGid } from '~/sidebar/utils';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import { getIdFromGraphQLId, MutationOperationMode } from '~/graphql_shared/utils';
const mutationMap = {
[IssuableType.Issue]: {
mutation: updateIssueLabelsMutation,
mutationName: 'updateIssue',
},
[IssuableType.MergeRequest]: {
mutation: updateMergeRequestLabelsMutation,
mutationName: 'mergeRequestSetLabels',
},
};
export default {
components: {
......@@ -21,7 +36,6 @@ export default {
'issuableType',
'labelsFetchPath',
'labelsManagePath',
'labelsUpdatePath',
'projectIssuesPath',
'projectPath',
],
......@@ -35,37 +49,79 @@ export default {
handleDropdownClose() {
$(this.$el).trigger('hidden.gl.dropdown');
},
handleUpdateSelectedLabels(dropdownLabels) {
getUpdateVariables(dropdownLabels) {
const currentLabelIds = this.selectedLabels.map(label => label.id);
const userAddedLabelIds = dropdownLabels.filter(label => label.set).map(label => label.id);
const userRemovedLabelIds = dropdownLabels.filter(label => !label.set).map(label => label.id);
const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
this.updateSelectedLabels(labelIds);
switch (this.issuableType) {
case IssuableType.Issue:
return {
addLabelIds: userAddedLabelIds,
iid: this.iid,
projectPath: this.projectPath,
removeLabelIds: userRemovedLabelIds,
};
case IssuableType.MergeRequest:
return {
iid: this.iid,
labelIds: labelIds.map(toLabelGid),
operationMode: MutationOperationMode.Replace,
projectPath: this.projectPath,
};
default:
return {};
}
},
handleUpdateSelectedLabels(dropdownLabels) {
this.updateSelectedLabels(this.getUpdateVariables(dropdownLabels));
},
getRemoveVariables(labelId) {
switch (this.issuableType) {
case IssuableType.Issue:
return {
iid: this.iid,
projectPath: this.projectPath,
removeLabelIds: [labelId],
};
case IssuableType.MergeRequest:
return {
iid: this.iid,
labelIds: [toLabelGid(labelId)],
operationMode: MutationOperationMode.Remove,
projectPath: this.projectPath,
};
default:
return {};
}
},
handleLabelRemove(labelId) {
const currentLabelIds = this.selectedLabels.map(label => label.id);
const labelIds = difference(currentLabelIds, [labelId]);
this.updateSelectedLabels(labelIds);
this.updateSelectedLabels(this.getRemoveVariables(labelId));
},
updateSelectedLabels(labelIds) {
updateSelectedLabels(inputVariables) {
this.isLabelsSelectInProgress = true;
axios({
data: {
[this.issuableType]: {
label_ids: labelIds,
},
},
method: 'put',
url: this.labelsUpdatePath,
})
this.$apollo
.mutate({
mutation: mutationMap[this.issuableType].mutation,
variables: { input: inputVariables },
})
.then(({ data }) => {
this.selectedLabels = data.labels;
const { mutationName } = mutationMap[this.issuableType];
if (data[mutationName]?.errors?.length) {
throw new Error();
}
const issuableType = camelCase(this.issuableType);
this.selectedLabels = data[mutationName]?.[issuableType]?.labels?.nodes?.map(label => ({
...label,
id: getIdFromGraphQLId(label.id),
}));
})
.catch(() => flash(__('An error occurred while updating labels.')))
.catch(() => createFlash({ message: __('An error occurred while updating labels.') }))
.finally(() => {
this.isLabelsSelectInProgress = false;
});
......
......@@ -91,8 +91,13 @@ export function mountSidebarLabels() {
return false;
}
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
provide: {
...el.dataset,
allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate),
......
mutation mergeRequestSetLabels($input: MergeRequestSetLabelsInput!) {
mergeRequestSetLabels(input: $input) {
errors
mergeRequest {
labels {
nodes {
color
description
id
title
}
}
}
}
}
export const toLabelGid = id => `gid://gitlab/Label/${id}`;
# frozen_string_literal: true
module Registrations
class WelcomeController < ApplicationController
layout 'welcome'
skip_before_action :authenticate_user!, :required_signup_info, :check_two_factor_requirement, only: [:show, :update]
before_action :require_current_user
feature_category :authentication_and_authorization
def show
return redirect_to path_for_signed_in_user(current_user) if completed_welcome_step?
end
def update
result = ::Users::SignupService.new(current_user, update_params).execute
if result[:status] == :success
process_gitlab_com_tracking
return redirect_to new_users_sign_up_group_path if experiment_enabled?(:onboarding_issues) && show_onboarding_issues_experiment?
redirect_to path_for_signed_in_user(current_user)
else
render :show
end
end
private
def require_current_user
return redirect_to new_user_registration_path unless current_user
end
def completed_welcome_step?
current_user.role.present? && !current_user.setup_for_company.nil?
end
def process_gitlab_com_tracking
return false unless ::Gitlab.com?
return false unless show_onboarding_issues_experiment?
track_experiment_event(:onboarding_issues, 'signed_up')
record_experiment_user(:onboarding_issues)
end
def update_params
params.require(:user).permit(:role, :setup_for_company)
end
def requires_confirmation?(user)
return false if user.confirmed?
return false if Feature.enabled?(:soft_email_confirmation)
return false if experiment_enabled?(:signup_flow)
true
end
def path_for_signed_in_user(user)
return users_almost_there_path if requires_confirmation?(user)
stored_location_for(user) || dashboard_projects_path
end
def show_onboarding_issues_experiment?
!helpers.in_subscription_flow? &&
!helpers.in_invitation_flow? &&
!helpers.in_oauth_flow? &&
!helpers.in_trial_flow?
end
end
end
Registrations::WelcomeController.prepend_if_ee('EE::Registrations::WelcomeController')
......@@ -8,9 +8,8 @@ class RegistrationsController < Devise::RegistrationsController
BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'.freeze
layout :choose_layout
layout 'devise'
skip_before_action :required_signup_info, :check_two_factor_requirement, only: [:welcome, :update_registration]
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, :ensure_destroy_prerequisites_met, only: [:destroy]
before_action :load_recaptcha, only: :new
......@@ -49,30 +48,6 @@ def destroy
end
end
def welcome
return redirect_to new_user_registration_path unless current_user
return redirect_to path_for_signed_in_user(current_user) if current_user.role.present? && !current_user.setup_for_company.nil?
end
def update_registration
return redirect_to new_user_registration_path unless current_user
result = ::Users::SignupService.new(current_user, update_registration_params).execute
if result[:status] == :success
if ::Gitlab.com? && show_onboarding_issues_experiment?
track_experiment_event(:onboarding_issues, 'signed_up')
record_experiment_user(:onboarding_issues)
end
return redirect_to new_users_sign_up_group_path if experiment_enabled?(:onboarding_issues) && show_onboarding_issues_experiment?
redirect_to path_for_signed_in_user(current_user)
else
render :welcome
end
end
protected
def persist_accepted_terms_if_required(new_user)
......@@ -160,10 +135,6 @@ def sign_up_params
params.require(:user).permit(:username, :email, :name, :first_name, :last_name, :password)
end
def update_registration_params
params.require(:user).permit(:role, :setup_for_company)
end
def resource_name
:user
end
......@@ -180,43 +151,10 @@ def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42380')
end
def path_for_signed_in_user(user)
if requires_confirmation?(user)
users_almost_there_path
else
stored_location_for(user) || dashboard_projects_path
end
end
def requires_confirmation?(user)
return false if user.confirmed?
return false if Feature.enabled?(:soft_email_confirmation)
return false if experiment_enabled?(:signup_flow)
true
end
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
# Part of an experiment to build a new sign up flow. Will be resolved
# with https://gitlab.com/gitlab-org/growth/engineering/issues/64
def choose_layout
if %w(welcome update_registration).include?(action_name)
'welcome'
else
'devise'
end
end
def show_onboarding_issues_experiment?
!helpers.in_subscription_flow? &&
!helpers.in_invitation_flow? &&
!helpers.in_oauth_flow? &&
!helpers.in_trial_flow?
end
def set_user_state
return unless Gitlab::CurrentSettings.require_admin_approval_after_user_signup
......
......@@ -3,8 +3,7 @@
module Mutations
module Boards
class Create < ::Mutations::BaseMutation
include Mutations::ResolvesGroup
include ResolvesProject
include Mutations::ResolvesResourceParent
graphql_name 'CreateBoard'
......@@ -13,12 +12,6 @@ class Create < ::Mutations::BaseMutation
null: true,
description: 'The board after mutation.'
argument :project_path, GraphQL::ID_TYPE,
required: false,
description: 'The project full path the board is associated with.'
argument :group_path, GraphQL::ID_TYPE,
required: false,
description: 'The group full path the board is associated with.'
argument :name,
GraphQL::STRING_TYPE,
required: false,
......@@ -43,10 +36,7 @@ class Create < ::Mutations::BaseMutation
authorize :admin_board
def resolve(args)
group_path = args.delete(:group_path)
project_path = args.delete(:project_path)
board_parent = authorized_find!(group_path: group_path, project_path: project_path)
board_parent = authorized_resource_parent_find!(args)
response = ::Boards::CreateService.new(board_parent, current_user, args).execute
{
......@@ -54,25 +44,6 @@ def resolve(args)
errors: response.errors
}
end
def ready?(**args)
if args.values_at(:project_path, :group_path).compact.blank?
raise Gitlab::Graphql::Errors::ArgumentError,
'group_path or project_path arguments are required'
end
super
end
private
def find_object(group_path: nil, project_path: nil)
if group_path
resolve_group(full_path: group_path)
else
resolve_project(full_path: project_path)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module ResolvesResourceParent
extend ActiveSupport::Concern
include Mutations::ResolvesGroup
include ResolvesProject
included do
argument :project_path, GraphQL::ID_TYPE,
required: false,
description: 'The project full path the resource is associated with'
argument :group_path, GraphQL::ID_TYPE,
required: false,
description: 'The group full path the resource is associated with'
end
def ready?(**args)
unless args[:project_path].present? ^ args[:group_path].present?
raise Gitlab::Graphql::Errors::ArgumentError,
'Exactly one of group_path or project_path arguments is required'
end
super
end
private
def authorized_resource_parent_find!(args)
authorized_find!(project_path: args.delete(:project_path),
group_path: args.delete(:group_path))
end
def find_object(project_path: nil, group_path: nil)
if group_path.present?
resolve_group(full_path: group_path)
else
resolve_project(full_path: project_path)
end
end
end
end
# frozen_string_literal: true
module Mutations
module Labels
class Create < BaseMutation
include Mutations::ResolvesResourceParent
graphql_name 'LabelCreate'
field :label,
Types::LabelType,
null: true,
description: 'The label after mutation'
argument :title, GraphQL::STRING_TYPE,
required: true,