提交 692f4b73 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 59222382
......@@ -60,6 +60,9 @@ Style/MutableConstant:
Style/SafeNavigation:
Enabled: false
Style/AccessModifierDeclarations:
AllowModifiersOnSymbols: true
# Frozen String Literal
Style/FrozenStringLiteralComment:
Enabled: true
......
......@@ -498,17 +498,6 @@ Security/YAMLLoad:
- 'spec/initializers/secret_token_spec.rb'
- 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
# Offense count: 10
# Configuration parameters: EnforcedStyle, AllowModifiersOnSymbols.
# SupportedStyles: inline, group
Style/AccessModifierDeclarations:
Exclude:
- 'app/helpers/issues_helper.rb'
- 'app/helpers/lazy_image_tag_helper.rb'
- 'lib/gitlab/cache/request_cache.rb'
- 'lib/gitlab/request_profiler.rb'
- 'spec/support/forgery_protection.rb'
# Offense count: 148
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
......@@ -741,10 +730,6 @@ Rails/SaveBang:
- 'ee/spec/models/license_spec.rb'
- 'ee/spec/models/merge_request_spec.rb'
- 'ee/spec/models/merge_train_spec.rb'
- 'ee/spec/models/operations/feature_flag_scope_spec.rb'
- 'ee/spec/models/operations/feature_flag_spec.rb'
- 'ee/spec/models/operations/feature_flags/strategy_spec.rb'
- 'ee/spec/models/operations/feature_flags/user_list_spec.rb'
- 'spec/models/packages/package_spec.rb'
- 'ee/spec/models/project_ci_cd_setting_spec.rb'
- 'ee/spec/models/project_services/github_service_spec.rb'
......@@ -1124,6 +1109,10 @@ Rails/SaveBang:
- 'spec/models/namespace_spec.rb'
- 'spec/models/note_spec.rb'
- 'spec/models/notification_setting_spec.rb'
- 'spec/models/operations/feature_flag_scope_spec.rb'
- 'spec/models/operations/feature_flag_spec.rb'
- 'spec/models/operations/feature_flags/strategy_spec.rb'
- 'spec/models/operations/feature_flags/user_list_spec.rb'
- 'spec/models/pages_domain_spec.rb'
- 'spec/models/project_auto_devops_spec.rb'
- 'spec/models/project_feature_spec.rb'
......
......@@ -9,13 +9,13 @@ import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
const defaultStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (e, element, combo) => {
const defaultStopCallback = Mousetrap.prototype.stopCallback;
Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
}
return defaultStopCallback(e, element, combo);
return defaultStopCallback.call(this, e, element, combo);
};
function initToggleButton() {
......
......@@ -5,12 +5,11 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
constructor(projectFindFile) {
super();
const oldStopCallback = Mousetrap.stopCallback;
this.projectFindFile = projectFindFile;
const oldStopCallback = Mousetrap.prototype.stopCallback;
Mousetrap.stopCallback = (e, element, combo) => {
Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
if (
element === this.projectFindFile.inputElement[0] &&
element === projectFindFile.inputElement[0] &&
(combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
) {
// when press up/down key in textbox, cursor prevent to move to home/end
......@@ -18,12 +17,12 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
return false;
}
return oldStopCallback(e, element, combo);
return oldStopCallback.call(this, e, element, combo);
};
Mousetrap.bind('up', this.projectFindFile.selectRowUp);
Mousetrap.bind('down', this.projectFindFile.selectRowDown);
Mousetrap.bind('esc', this.projectFindFile.goToTree);
Mousetrap.bind('enter', this.projectFindFile.goToBlob);
Mousetrap.bind('up', projectFindFile.selectRowUp);
Mousetrap.bind('down', projectFindFile.selectRowDown);
Mousetrap.bind('esc', projectFindFile.goToTree);
Mousetrap.bind('enter', projectFindFile.goToBlob);
}
}
import Search from './search';
import initStateFilter from '~/search/state_filter';
document.addEventListener('DOMContentLoaded', () => new Search());
document.addEventListener('DOMContentLoaded', () => {
initStateFilter();
return new Search();
});
<script>
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { FILTER_STATES, FILTER_HEADER, FILTER_TEXT } from '../constants';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
const FILTERS_ARRAY = Object.values(FILTER_STATES);
export default {
name: 'StateFilter',
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
},
props: {
scope: {
type: String,
required: true,
},
state: {
type: String,
required: false,
default: FILTER_STATES.ANY.value,
validator: v => FILTERS_ARRAY.some(({ value }) => value === v),
},
},
computed: {
selectedFilterText() {
let filterText = FILTER_TEXT;
if (this.selectedFilter === FILTER_STATES.CLOSED.value) {
filterText = FILTER_STATES.CLOSED.label;
} else if (this.selectedFilter === FILTER_STATES.OPEN.value) {
filterText = FILTER_STATES.OPEN.label;
}
return filterText;
},
selectedFilter: {
get() {
if (FILTERS_ARRAY.some(({ value }) => value === this.state)) {
return this.state;
}
return FILTER_STATES.ANY.value;
},
set(state) {
visitUrl(setUrlParams({ state }));
},
},
},
methods: {
dropDownItemClass(filter) {
return {
'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
filter === FILTER_STATES.ANY,
};
},
isFilterSelected(filter) {
return filter === this.selectedFilter;
},
handleFilterChange(state) {
this.selectedFilter = state;
},
},
filterStates: FILTER_STATES,
filterHeader: FILTER_HEADER,
filtersArray: FILTERS_ARRAY,
};
</script>
<template>
<gl-dropdown
v-if="scope === 'issues'"
:text="selectedFilterText"
class="col-sm-3 gl-pt-4 gl-pl-0"
>
<header class="gl-text-center gl-font-weight-bold gl-font-lg">
{{ $options.filterHeader }}
</header>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="filter in $options.filtersArray"
:key="filter.value"
:is-check-item="true"
:is-checked="isFilterSelected(filter.value)"
:class="dropDownItemClass(filter)"
@click="handleFilterChange(filter.value)"
>
{{ filter.label }}
</gl-dropdown-item>
</gl-dropdown>
</template>
import { __ } from '~/locale';
export const FILTER_HEADER = __('Status');
export const FILTER_TEXT = __('Any Status');
export const FILTER_STATES = {
ANY: {
label: __('Any'),
value: 'all',
},
OPEN: {
label: __('Open'),
value: 'opened',
},
CLOSED: {
label: __('Closed'),
value: 'closed',
},
};
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import StateFilter from './components/state_filter.vue';
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-search-filter-by-state');
if (!el) return false;
return new Vue({
el,
components: {
StateFilter,
},
data() {
const { dataset } = this.$options.el;
return {
scope: dataset.scope,
state: dataset.state,
};
},
render(createElement) {
return createElement('state-filter', {
props: {
scope: this.scope,
state: this.state,
},
});
},
});
};
......@@ -9,7 +9,7 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
const originalStopCallback = Mousetrap.stopCallback;
const originalStopCallback = Mousetrap.prototype.stopCallback;
export default {
components: {
......@@ -134,7 +134,18 @@ export default {
this.toggle(!this.visible);
});
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
Mousetrap.prototype.stopCallback = function customStopCallback(e, el, combo) {
if (
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
el.classList.contains('inputarea')
) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback.call(this, e, el, combo);
};
},
methods: {
toggle(visible) {
......@@ -199,18 +210,6 @@ export default {
this.cancelMouseOver = false;
this.onMouseOver(index);
},
mousetrapStopCallback(e, el, combo) {
if (
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
el.classList.contains('inputarea')
) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
},
},
};
</script>
......
......@@ -15,7 +15,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import { stripQuotes } from './filtered_search_utils';
import { stripQuotes, uniqueTokens } from './filtered_search_utils';
import { SortDirection } from './constants';
export default {
......@@ -120,10 +120,31 @@ export default {
? __('Sort direction: Ascending')
: __('Sort direction: Descending');
},
/**
* This prop fixes a behaviour affecting GlFilteredSearch
* where selecting duplicate token values leads to history
* dropdown also showing that selection.
*/
filteredRecentSearches() {
return this.recentSearchesStorageKey
? this.recentSearches.filter(item => typeof item !== 'string')
: undefined;
if (this.recentSearchesStorageKey) {
const knownItems = [];
return this.recentSearches.reduce((historyItems, item) => {
// Only include non-string history items (discard items from legacy search)
if (typeof item !== 'string') {
const sanitizedItem = uniqueTokens(item);
const itemString = JSON.stringify(sanitizedItem);
// Only include items which aren't already part of history
if (!knownItems.includes(itemString)) {
historyItems.push(sanitizedItem);
// We're storing string for comparision as doing direct object compare
// won't work due to object reference not being the same.
knownItems.push(itemString);
}
}
return historyItems;
}, []);
}
return undefined;
},
},
watch: {
......@@ -245,12 +266,14 @@ export default {
this.recentSearchesService.save(resultantSearches);
this.recentSearches = [];
},
handleFilterSubmit(filters) {
handleFilterSubmit() {
const filterTokens = uniqueTokens(this.filterValue);
this.filterValue = filterTokens;
if (this.recentSearchesStorageKey) {
this.recentSearchesPromise
.then(() => {
if (filters.length) {
const resultantSearches = this.recentSearchesStore.addRecentSearch(filters);
if (filterTokens.length) {
const resultantSearches = this.recentSearchesStore.addRecentSearch(filterTokens);
this.recentSearchesService.save(resultantSearches);
this.recentSearches = resultantSearches;
}
......@@ -260,7 +283,7 @@ export default {
});
}
this.blurSearchInput();
this.$emit('onFilter', this.removeQuotesEnclosure(filters));
this.$emit('onFilter', this.removeQuotesEnclosure(filterTokens));
},
},
};
......
export const stripQuotes = value => {
return value.includes(' ') ? value.slice(1, -1) : value;
/**
* Strips enclosing quotations from a string if it has one.
*
* @param {String} value String to strip quotes from
*
* @returns {String} String without any enclosure
*/
export const stripQuotes = value => value.replace(/^('|")(.*)('|")$/, '$2');
/**
* This method removes duplicate tokens from tokens array.
*
* @param {Array} tokens Array of tokens as defined by `GlFilteredSearch`
*
* @returns {Array} Unique array of tokens
*/
export const uniqueTokens = tokens => {
const knownTokens = [];
return tokens.reduce((uniques, token) => {
if (typeof token === 'object' && token.type !== 'filtered-search-term') {
const tokenString = `${token.type}${token.value.operator}${token.value.data}`;
if (!knownTokens.includes(tokenString)) {
uniques.push(token);
knownTokens.push(tokenString);
}
} else {
uniques.push(token);
}
return uniques;
}, []);
};
......@@ -4,7 +4,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
include DiffHelper
include RendersNotes
before_action :apply_diff_view_cookie!
before_action :commit
before_action :define_diff_vars
before_action :define_diff_comment_vars, except: [:diffs_batch, :diffs_metadata]
......
......@@ -10,8 +10,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections
include RecordUserLastActivity
include SourcegraphDecorator
include DiffHelper
skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :apply_diff_view_cookie!, only: [:show]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authorize_read_actual_head_pipeline!, only: [
......
......@@ -8,7 +8,7 @@
# current_user - which user use
# params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'open' or 'closed' or 'all'
# state: 'opened' or 'closed' or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
......
......@@ -25,5 +25,5 @@ module LazyImageTagHelper
end
# Required for Banzai::Filter::ImageLazyLoadFilter
module_function :placeholder_image
module_function :placeholder_image # rubocop: disable Style/AccessModifierDeclarations
end
# frozen_string_literal: true
module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets, :state].freeze
def search_autocomplete_opts(term)
return unless current_user
......
# frozen_string_literal: true
module Operations
class FeatureFlag < ApplicationRecord
include AtomicInternalId
include IidRoutes
self.table_name = 'operations_feature_flags'
belongs_to :project
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.operations_feature_flags&.maximum(:iid) }
default_value_for :active, true
# scopes exists only for the first version
has_many :scopes, class_name: 'Operations::FeatureFlagScope'
# strategies exists only for the second version
has_many :strategies, class_name: 'Operations::FeatureFlags::Strategy'
has_many :feature_flag_issues
has_many :issues, through: :feature_flag_issues
has_one :default_scope, -> { where(environment_scope: '*') }, class_name: 'Operations::FeatureFlagScope'
validates :project, presence: true
validates :name,
presence: true,
length: 2..63,
format: {
with: Gitlab::Regex.feature_flag_regex,
message: Gitlab::Regex.feature_flag_regex_message
}
validates :name, uniqueness: { scope: :project_id }
validates :description, allow_blank: true, length: 0..255
validate :first_default_scope, on: :create, if: :has_scopes?
validate :version_associations
before_create :build_default_scope, if: -> { legacy_flag? && scopes.none? }
accepts_nested_attributes_for :scopes, allow_destroy: true
accepts_nested_attributes_for :strategies, allow_destroy: true
scope :ordered, -> { order(:name) }
scope :enabled, -> { where(active: true) }
scope :disabled, -> { where(active: false) }
enum version: {
legacy_flag: 1,
new_version_flag: 2
}
class << self
def preload_relations
preload(:scopes, strategies: :scopes)
end
def for_unleash_client(project, environment)
includes(strategies: [:scopes, :user_list])
.where(project: project)
.merge(Operations::FeatureFlags::Scope.on_environment(environment))
.reorder(:id)
.references(:operations_scopes)
end
end
def related_issues(current_user, preload:)
issues = ::Issue
.select('issues.*, operations_feature_flags_issues.id AS link_id')
.joins(:feature_flag_issues)
.where('operations_feature_flags_issues.feature_flag_id = ?', id)
.order('operations_feature_flags_issues.id ASC')
.includes(preload)
Ability.issues_readable_by_user(issues, current_user)
end
private
def version_associations
if new_version_flag? && scopes.any?
errors.add(:version_associations, 'version 2 feature flags may not have scopes')
elsif legacy_flag? && strategies.any?
errors.add(:version_associations, 'version 1 feature flags may not have strategies')
end
end
def first_default_scope
unless scopes.first.environment_scope == '*'
errors.add(:default_scope, 'has to be the first element')
end
end
def build_default_scope
scopes.build(environment_scope: '*', active: self.active)
end
def has_scopes?
scopes.any?
end
end
end
# frozen_string_literal: true
module Operations
class FeatureFlagScope < ApplicationRecord
prepend HasEnvironmentScope
include Gitlab::Utils::StrongMemoize
self.table_name = 'operations_feature_flag_scopes'
belongs_to :feature_flag