diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-07 21:09:26 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-07 21:09:26 +0000 |
commit | 17c8111494f51e79744c782db023804f5e4a7410 (patch) | |
tree | 8aebe53b8aea72f9d4abac1bf9131203302a5b6e /app | |
parent | 4b7575da97d88ef4c7b2ec599b0c3fc457b4f561 (diff) | |
download | gitlab-ce-17c8111494f51e79744c782db023804f5e4a7410.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
53 files changed, 487 insertions, 158 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 904bf117dc0..e527659a939 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -23,6 +23,8 @@ const Api = { projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', projectRunnersPath: '/api/:version/projects/:id/runners', projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches', + projectSearchPath: '/api/:version/projects/:id/search', + projectMilestonesPath: '/api/:version/projects/:id/milestones', mergeRequestsPath: '/api/:version/merge_requests', groupLabelsPath: '/groups/:namespace_path/-/labels', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', @@ -75,13 +77,11 @@ const Api = { const url = Api.buildUrl(Api.groupsPath); return axios .get(url, { - params: Object.assign( - { - search: query, - per_page: DEFAULT_PER_PAGE, - }, - options, - ), + params: { + search: query, + per_page: DEFAULT_PER_PAGE, + ...options, + }, }) .then(({ data }) => { callback(data); @@ -248,6 +248,23 @@ const Api = { .then(({ data }) => data); }, + projectSearch(id, options = {}) { + const url = Api.buildUrl(Api.projectSearchPath).replace(':id', encodeURIComponent(id)); + + return axios.get(url, { + params: { + search: options.search, + scope: options.scope, + }, + }); + }, + + projectMilestones(id) { + const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id)); + + return axios.get(url); + }, + mergeRequests(params = {}) { const url = Api.buildUrl(Api.mergeRequestsPath); @@ -282,7 +299,7 @@ const Api = { }; return axios .get(url, { - params: Object.assign({}, defaults, options), + params: { ...defaults, ...options }, }) .then(({ data }) => callback(data)) .catch(() => flash(__('Something went wrong while fetching projects'))); @@ -365,13 +382,11 @@ const Api = { users(query, options) { const url = Api.buildUrl(this.usersPath); return axios.get(url, { - params: Object.assign( - { - search: query, - per_page: DEFAULT_PER_PAGE, - }, - options, - ), + params: { + search: query, + per_page: DEFAULT_PER_PAGE, + ...options, + }, }); }, @@ -402,7 +417,7 @@ const Api = { }; return axios .get(url, { - params: Object.assign({}, defaults, options), + params: { ...defaults, ...options }, }) .then(({ data }) => callback(data)) .catch(() => flash(__('Something went wrong while fetching projects'))); diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js index d5d8edd5ac0..c35a073b291 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js @@ -22,7 +22,7 @@ function eventHasModifierKeys(event) { export default class ShortcutsBlob extends Shortcuts { constructor(opts) { - const options = Object.assign({}, defaults, opts); + const options = { ...defaults, ...opts }; super(options.skipResetBindings); this.options = options; diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js index 476b9405a9e..44dfbfcfe1c 100644 --- a/app/assets/javascripts/blob/blob_fork_suggestion.js +++ b/app/assets/javascripts/blob/blob_fork_suggestion.js @@ -17,7 +17,7 @@ const defaults = { class BlobForkSuggestion { constructor(options) { - this.elementMap = Object.assign({}, defaults, options); + this.elementMap = { ...defaults, ...options }; this.onOpenButtonClick = this.onOpenButtonClick.bind(this); this.onCancelButtonClick = this.onCancelButtonClick.bind(this); } diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index 68ea28e68d9..fceb8c9d48e 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -19,14 +19,15 @@ export function getBoardSortableDefaultOptions(obj) { const touchEnabled = 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch); - const defaultSortOptions = Object.assign({}, sortableConfig, { + const defaultSortOptions = { + ...sortableConfig, filter: '.no-drag', delay: touchEnabled ? 100 : 0, scrollSensitivity: touchEnabled ? 60 : 100, scrollSpeed: 20, onStart: sortableStart, onEnd: sortableEnd, - }); + }; Object.keys(obj).forEach(key => { defaultSortOptions[key] = obj[key]; diff --git a/app/assets/javascripts/close_reopen_report_toggle.js b/app/assets/javascripts/close_reopen_report_toggle.js index 882d20671cc..bcddce6e727 100644 --- a/app/assets/javascripts/close_reopen_report_toggle.js +++ b/app/assets/javascripts/close_reopen_report_toggle.js @@ -2,7 +2,7 @@ import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; // Todo: Remove this when fixing issue in input_setter plugin -const InputSetter = Object.assign({}, ISetter); +const InputSetter = { ...ISetter }; class CloseReopenReportToggle { constructor(opts = {}) { diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index d46525def06..3699a3b8b2b 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -325,7 +325,7 @@ export default class Clusters { handleClusterStatusSuccess(data) { const prevStatus = this.store.state.status; - const prevApplicationMap = Object.assign({}, this.store.state.applications); + const prevApplicationMap = { ...this.store.state.applications }; this.store.updateStateFromServer(data.data); diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js index a259667bb75..2fcd40a901d 100644 --- a/app/assets/javascripts/comment_type_toggle.js +++ b/app/assets/javascripts/comment_type_toggle.js @@ -2,7 +2,7 @@ import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; // Todo: Remove this when fixing issue in input_setter plugin -const InputSetter = Object.assign({}, ISetter); +const InputSetter = { ...ISetter }; class CommentTypeToggle { constructor(opts = {}) { diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index ba585444ba5..801566d2f2f 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -13,7 +13,7 @@ import { import confidentialMergeRequestState from './confidential_merge_request/state'; // Todo: Remove this when fixing issue in input_setter plugin -const InputSetter = Object.assign({}, ISetter); +const InputSetter = { ...ISetter }; const CREATE_MERGE_REQUEST = 'create-mr'; const CREATE_BRANCH = 'create-branch'; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 304a0726597..4f9069f61a5 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -84,7 +84,7 @@ export default { events.forEach(item => { if (!item) return; - const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); + const eventItem = { ...DEFAULT_EVENT_OBJECTS[stage.slug], ...item }; eventItem.totalTime = eventItem.total_time; diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/design_management/components/design_note_pin.vue index b6b587a12e6..50ea69d52ce 100644 --- a/app/assets/javascripts/design_management/components/design_note_pin.vue +++ b/app/assets/javascripts/design_management/components/design_note_pin.vue @@ -28,9 +28,7 @@ export default { return this.label === null; }, pinStyle() { - return this.repositioning - ? Object.assign({}, this.position, { cursor: 'move' }) - : this.position; + return this.repositioning ? { ...this.position, cursor: 'move' } : this.position; }, pinLabel() { return this.isNewNote diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index b46b8d95d5f..eb4c6683035 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -233,7 +233,7 @@ export function trimFirstCharOfLineContent(line = {}) { // eslint-disable-next-line no-param-reassign delete line.text; - const parsedLine = Object.assign({}, line); + const parsedLine = { ...line }; if (line.rich_text) { const firstChar = parsedLine.rich_text.charAt(0); diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index e07ec693948..1992e753255 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -58,13 +58,14 @@ export default class EnvironmentsStore { let filtered = {}; if (env.size > 1) { - filtered = Object.assign({}, env, { + filtered = { + ...env, isFolder: true, isLoadingFolderContent: oldEnvironmentState.isLoading || false, folderName: env.name, isOpen: oldEnvironmentState.isOpen || false, children: oldEnvironmentState.children || [], - }); + }; } if (env.latest) { @@ -166,7 +167,7 @@ export default class EnvironmentsStore { let updated = env; if (env.latest) { - updated = Object.assign({}, env, env.latest); + updated = { ...env, ...env.latest }; delete updated.latest; } else { updated = env; @@ -192,7 +193,7 @@ export default class EnvironmentsStore { const { environments } = this.state; const updatedEnvironments = environments.map(env => { - const updateEnv = Object.assign({}, env); + const updateEnv = { ...env }; if (env.id === environment.id) { updateEnv[prop] = newValue; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index d051b60814e..161a65c511d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -120,7 +120,7 @@ export default class FilteredSearchDropdownManager { filter: key, }; const extraArguments = mappingKey.extraArguments || {}; - const glArguments = Object.assign({}, defaultArguments, extraArguments); + const glArguments = { ...defaultArguments, ...extraArguments }; // Passing glArguments to `new glClass(<arguments>)` mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))(); diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js index b3eb0475d6f..cdbc9ec84bd 100644 --- a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js +++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js @@ -2,14 +2,12 @@ import { uniq } from 'lodash'; class RecentSearchesStore { constructor(initialState = {}, allowedKeys) { - this.state = Object.assign( - { - isLocalStorageAvailable: true, - recentSearches: [], - allowedKeys, - }, - initialState, - ); + this.state = { + isLocalStorageAvailable: true, + recentSearches: [], + allowedKeys, + ...initialState, + }; } addRecentSearch(newSearch) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 1490498c511..be4b4b5f87d 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -595,13 +595,14 @@ class GitLabDropdown { return renderItem({ instance: this, - options: Object.assign({}, this.options, { + options: { + ...this.options, icon: this.icon, highlight: this.highlight, highlightText: text => this.highlightTextMatches(text, this.filterInput.val()), highlightTemplate: this.highlightTemplate.bind(this), parent, - }), + }, data, group, index, diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index ced10fff129..0b7735a7db9 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -8,7 +8,7 @@ export default class GLForm { constructor(form, enableGFM = {}) { this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); - this.enableGFM = Object.assign({}, defaultAutocompleteConfig, enableGFM); + this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM }; // Disable autocomplete for keywords which do not have dataSources available const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {}; Object.keys(this.enableGFM).forEach(item => { diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js index 012177479c6..bb2aea3ea76 100644 --- a/app/assets/javascripts/groups/new_group_child.js +++ b/app/assets/javascripts/groups/new_group_child.js @@ -2,7 +2,7 @@ import { visitUrl } from '../lib/utils/url_utility'; import DropLab from '../droplab/drop_lab'; import ISetter from '../droplab/plugins/input_setter'; -const InputSetter = Object.assign({}, ISetter); +const InputSetter = { ...ISetter }; const NEW_PROJECT = 'new-project'; const NEW_SUBGROUP = 'new-subgroup'; diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue index 195504a6861..70a92b8d3ab 100644 --- a/app/assets/javascripts/ide/components/nav_form.vue +++ b/app/assets/javascripts/ide/components/nav_form.vue @@ -25,13 +25,13 @@ export default { <div class="ide-nav-form p-0"> <tabs v-if="showMergeRequests" stop-propagation> <tab active> - <template slot="title"> + <template #title> {{ __('Branches') }} </template> <branches-search-list /> </tab> <tab> - <template slot="title"> + <template #title> {{ __('Merge Requests') }} </template> <merge-request-search-list /> diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js index 9b7ed68b893..29e29d7fcd3 100644 --- a/app/assets/javascripts/ide/lib/diff/diff.js +++ b/app/assets/javascripts/ide/lib/diff/diff.js @@ -14,13 +14,12 @@ export const computeDiff = (originalContent, newContent) => { endLineNumber: lineNumber + change.count - 1, }); } else if ('added' in change || 'removed' in change) { - acc.push( - Object.assign({}, change, { - lineNumber, - modified: undefined, - endLineNumber: lineNumber + change.count - 1, - }), - ); + acc.push({ + ...change, + lineNumber, + modified: undefined, + endLineNumber: lineNumber + change.count - 1, + }); } if (!change.removed) { diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js index 9230f3839c1..034fdad4305 100644 --- a/app/assets/javascripts/ide/stores/mutations/project.js +++ b/app/assets/javascripts/ide/stores/mutations/project.js @@ -16,9 +16,7 @@ export default { }); Object.assign(state, { - projects: Object.assign({}, state.projects, { - [projectPath]: project, - }), + projects: { ...state.projects, [projectPath]: project }, }); }, [types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) { diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js index 359943b4ab7..c8f14a680c2 100644 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -14,12 +14,13 @@ export default { }, [types.CREATE_TREE](state, { treePath }) { Object.assign(state, { - trees: Object.assign({}, state.trees, { + trees: { + ...state.trees, [treePath]: { tree: [], loading: true, }, - }), + }, }); }, [types.SET_DIRECTORY_DATA](state, { data, treePath }) { diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js index df3d90cff68..deaef686f59 100644 --- a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js +++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js @@ -32,9 +32,7 @@ export function removeCommentIndicator(imageFrameEl) { commentIndicatorEl.remove(); } - return Object.assign({}, meta, { - removed: willRemove, - }); + return { ...meta, removed: willRemove }; } export function showCommentIndicator(imageFrameEl, coordinate) { diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js index a319bcccb8f..74ca907c99f 100644 --- a/app/assets/javascripts/image_diff/helpers/dom_helper.js +++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js @@ -4,12 +4,7 @@ export function setPositionDataAttribute(el, options) { const { x, y, width, height } = options; const { position } = el.dataset; - const positionObject = Object.assign({}, JSON.parse(position), { - x, - y, - width, - height, - }); + const positionObject = { ...JSON.parse(position), x, y, width, height }; el.setAttribute('data-position', JSON.stringify(positionObject)); } diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js index 26c1b0ec7be..89f696dd1d8 100644 --- a/app/assets/javascripts/image_diff/image_diff.js +++ b/app/assets/javascripts/image_diff/image_diff.js @@ -75,9 +75,7 @@ export default class ImageDiff { if (this.renderCommentBadge) { imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options); } else { - const numberBadgeOptions = Object.assign({}, options, { - badgeText: index + 1, - }); + const numberBadgeOptions = { ...options, badgeText: index + 1 }; imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions); } diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index f4030939f2c..0ce8dfe4442 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -220,7 +220,7 @@ export const fetchJobsForStage = ({ dispatch }, stage = {}) => { }, }) .then(({ data }) => { - const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true })); + const retriedJobs = data.retried.map(job => ({ ...job, retried: true })); const jobs = data.latest_statuses.concat(retriedJobs); dispatch('receiveJobsForStageSuccess', jobs); @@ -236,7 +236,7 @@ export const receiveJobsForStageError = ({ commit }) => { export const triggerManualJob = ({ state }, variables) => { const parsedVariables = variables.map(variable => { - const copyVar = Object.assign({}, variable); + const copyVar = { ...variable }; delete copyVar.id; return copyVar; }); diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue new file mode 100644 index 00000000000..19148d6184f --- /dev/null +++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue @@ -0,0 +1,228 @@ +<script> +import { + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownHeader, + GlNewDropdownItem, + GlLoadingIcon, + GlSearchBoxByType, + GlIcon, +} from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; +import Api from '~/api'; +import createFlash from '~/flash'; +import { intersection, debounce } from 'lodash'; + +export default { + components: { + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownHeader, + GlNewDropdownItem, + GlLoadingIcon, + GlSearchBoxByType, + GlIcon, + }, + model: { + prop: 'preselectedMilestones', + event: 'change', + }, + props: { + projectId: { + type: String, + required: true, + }, + preselectedMilestones: { + type: Array, + default: () => [], + required: false, + }, + extraLinks: { + type: Array, + default: () => [], + required: false, + }, + }, + data() { + return { + searchQuery: '', + projectMilestones: [], + searchResults: [], + selectedMilestones: [], + requestCount: 0, + }; + }, + translations: { + milestone: __('Milestone'), + selectMilestone: __('Select milestone'), + noMilestone: __('No milestone'), + noResultsLabel: __('No matching results'), + searchMilestones: __('Search Milestones'), + }, + computed: { + selectedMilestonesLabel() { + if (this.milestoneTitles.length === 1) { + return this.milestoneTitles[0]; + } + + if (this.milestoneTitles.length > 1) { + const firstMilestoneName = this.milestoneTitles[0]; + const numberOfOtherMilestones = this.milestoneTitles.length - 1; + return sprintf(__('%{firstMilestoneName} + %{numberOfOtherMilestones} more'), { + firstMilestoneName, + numberOfOtherMilestones, + }); + } + + return this.$options.translations.noMilestone; + }, + milestoneTitles() { + return this.preselectedMilestones.map(milestone => milestone.title); + }, + dropdownItems() { + return this.searchResults.length ? this.searchResults : this.projectMilestones; + }, + noResults() { + return this.searchQuery.length > 2 && this.searchResults.length === 0; + }, + isLoading() { + return this.requestCount !== 0; + }, + }, + mounted() { + this.fetchMilestones(); + }, + methods: { + fetchMilestones() { + this.requestCount += 1; + + Api.projectMilestones(this.projectId) + .then(({ data }) => { + this.projectMilestones = this.getTitles(data); + this.selectedMilestones = intersection(this.projectMilestones, this.milestoneTitles); + }) + .catch(() => { + createFlash(__('An error occurred while loading milestones')); + }) + .finally(() => { + this.requestCount -= 1; + }); + }, + searchMilestones: debounce(function searchMilestones() { + this.requestCount += 1; + const options = { + search: this.searchQuery, + scope: 'milestones', + }; + + if (this.searchQuery.length < 3) { + this.requestCount -= 1; + this.searchResults = []; + return; + } + + Api.projectSearch(this.projectId, options) + .then(({ data }) => { + const searchResults = this.getTitles(data); + + this.searchResults = searchResults.length ? searchResults : []; + }) + .catch(() => { + createFlash(__('An error occurred while searching for milestones')); + }) + .finally(() => { + this.requestCount -= 1; + }); + }, 100), + toggleMilestoneSelection(clickedMilestone) { + if (!clickedMilestone) return []; + + let milestones = [...this.preselectedMilestones]; + const hasMilestone = this.milestoneTitles.includes(clickedMilestone); + + if (hasMilestone) { + milestones = milestones.filter(({ title }) => title !== clickedMilestone); + } else { + milestones.push({ title: clickedMilestone }); + } + + return milestones; + }, + onMilestoneClicked(clickedMilestone) { + const milestones = this.toggleMilestoneSelection(clickedMilestone); + this.$emit('change', milestones); + + this.selectedMilestones = intersection( + this.projectMilestones, + milestones.map(milestone => milestone.title), + ); + }, + isSelectedMilestone(milestoneTitle) { + return this.selectedMilestones.includes(milestoneTitle); + }, + getTitles(milestones) { + return milestones.filter(({ state }) => state === 'active').map(({ title }) => title); + }, + }, +}; +</script> + +<template> + <gl-new-dropdown> + <template slot="button-content"> + <span ref="buttonText" class="flex-grow-1 ml-1 text-muted">{{ + selectedMilestonesLabel + }}</span> + <gl-icon name="chevron-down" /> + </template> + + <gl-new-dropdown-header> + <span class="text-center d-block">{{ $options.translations.selectMilestone }}</span> + </gl-new-dropdown-header> + + <gl-new-dropdown-divider /> + + <gl-search-box-by-type + v-model.trim="searchQuery" + class="m-2" + :placeholder="this.$options.translations.searchMilestones" + @input="searchMilestones" + /> + + <gl-new-dropdown-item @click="onMilestoneClicked(null)"> + <span :class="{ 'pl-4': true, 'selected-item': selectedMilestones.length === 0 }"> + {{ $options.translations.noMilestone }} + </span> + </gl-new-dropdown-item> + + <gl-new-dropdown-divider /> + + <template v-if="isLoading"> + <gl-loading-icon /> + <gl-new-dropdown-divider /> + </template> + <template v-else-if="noResults"> + <div class="dropdown-item-space"> + <span ref="noResults" class="pl-4">{{ $options.translations.noResultsLabel }}</span> + </div> + <gl-new-dropdown-divider /> + </template> + <template v-else-if="dropdownItems.length"> + <gl-new-dropdown-item + v-for="item in dropdownItems" + :key="item" + role="milestone option" + @click="onMilestoneClicked(item)" + > + <span :class="{ 'pl-4': true, 'selected-item': isSelectedMilestone(item) }"> + {{ item }} + </span> + </gl-new-dropdown-item> + <gl-new-dropdown-divider /> + </template> + + <gl-new-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url"> + <span class="pl-4">{{ item.text }}</span> + </gl-new-dropdown-item> + </gl-new-dropdown> +</template> diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js index ec9c800b7a2..2580f8e86b1 100644 --- a/app/assets/javascripts/mr_notes/init_notes.js +++ b/app/assets/javascripts/mr_notes/init_notes.js @@ -15,6 +15,19 @@ export default () => { notesApp, }, store, + data() { + const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; + const noteableData = JSON.parse(notesDataset.noteableData); + noteableData.noteableType = notesDataset.noteableType; + noteableData.targetType = notesDataset.targetType; + + return { + noteableData, + currentUserData: JSON.parse(notesDataset.currentUserData), + notesData: JSON.parse(notesDataset.notesData), + helpPagePath: notesDataset.helpPagePath, + }; + }, computed: { ...mapGetters(['discussionTabCounter']), ...mapState({ @@ -54,19 +67,6 @@ export default () => { updateDiscussionTabCounter() { this.notesCountBadge.text(this.discussionTabCounter); }, - dataset() { - const data = this.$el.dataset; - const noteableData = JSON.parse(data.noteableData); - noteableData.noteableType = data.noteableType; - noteableData.targetType = data.targetType; - - return { - noteableData, - notesData: JSON.parse(data.notesData), - userData: JSON.parse(data.currentUserData), - helpPagePath: data.helpPagePath, - }; - }, }, render(createElement) { // NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`, @@ -76,8 +76,11 @@ export default () => { return createElement(discussionKeyboardNavigator, [ createElement('notes-app', { props: { - ...this.dataset(), + noteableData: this.noteableData, + notesData: this.notesData, + userData: this.currentUserData, shouldShow: this.isShowTabActive, + helpPagePath: this.helpPagePath, }, }), ]); diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index c1dd56aedf2..faa6006945d 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -230,10 +230,11 @@ export default { const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') }; if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) { - return Object.assign({}, defaultConfig, { + return { + ...defaultConfig, filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE, persistFilter: false, - }); + }; } return defaultConfig; }, diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 6fd3cee5340..8f9e2359e0d 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -14,36 +14,38 @@ document.addEventListener('DOMContentLoaded', () => { notesApp, }, store, - methods: { - setData() { - const notesDataset = this.$el.dataset; - const parsedUserData = JSON.parse(notesDataset.currentUserData); - const noteableData = JSON.parse(notesDataset.noteableData); - let currentUserData = {}; + data() { + const notesDataset = document.getElementById('js-vue-notes').dataset; + const parsedUserData = JSON.parse(notesDataset.currentUserData); + const noteableData = JSON.parse(notesDataset.noteableData); + let currentUserData = {}; - noteableData.noteableType = notesDataset.noteableType; - noteableData.targetType = notesDataset.targetType; + noteableData.noteableType = notesDataset.noteableType; + noteableData.targetType = notesDataset.targetType; - if (parsedUserData) { - currentUserData = { - id: parsedUserData.id, - name: parsedUserData.name, - username: parsedUserData.username, - avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, - path: parsedUserData.path, - }; - } - - return { - noteableData, - userData: currentUserData, - notesData: JSON.parse(notesDataset.notesData), + if (parsedUserData) { + currentUserData = { + id: parsedUserData.id, + name: parsedUserData.name, + username: parsedUserData.username, + avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, + path: parsedUserData.path, }; - }, + } + + return { + noteableData, + currentUserData, + notesData: JSON.parse(notesDataset.notesData), + }; }, render(createElement) { return createElement('notes-app', { - props: { ...this.setData() }, + props: { + noteableData: this.noteableData, + notesData: this.notesData, + userData: this.currentUserData, + }, }); }, }); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index a358515c2ec..0999d0aa7ac 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -248,7 +248,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const hasQuickActions = utils.hasQuickActions(placeholderText); const replyId = noteData.data.in_reply_to_discussion_id; let methodToDispatch; - const postData = Object.assign({}, noteData); + const postData = { ...noteData }; if (postData.isDraft === true) { methodToDispatch = replyId ? 'batchComments/addDraftToDiscussion' diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index e25f8ab4790..981914dd046 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -99,9 +99,10 @@ export default { // 3. If GitLab user does not have avatar, they might have a Gravatar } else if (this.pipeline.commit.author_gravatar_url) { - commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + commitAuthorInformation = { + ...this.pipeline.commit.author, avatar_url: this.pipeline.commit.author_gravatar_url, - }); + }; } // 4. If committer is not a GitLab User, they can have a Gravatar } else { diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue index 9739ef76867..80a1c83f171 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue @@ -29,7 +29,14 @@ export default { successPercentage() { // Returns a full number when the decimals equal .00. // Otherwise returns a float to two decimal points - return Number(((this.report.success_count / this.report.total_count) * 100 || 0).toFixed(2)); + // Do not include skipped tests as part of the total when doing success calculations. + + const totalCompletedCount = this.report.total_count - this.report.skipped_count; + + if (totalCompletedCount > 0) { + return Number(((this.report.success_count / totalCompletedCount) * 100 || 0).toFixed(2)); + } + return 0; }, formattedDuration() { return formatTime(secondsToMilliseconds(this.report.total_time)); diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 1ef73760e02..c6f65277c8d 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -15,7 +15,7 @@ export default class PipelineStore { * @param {Object} pipeline */ storePipeline(pipeline = {}) { - const pipelineCopy = Object.assign({}, pipeline); + const pipelineCopy = { ...pipeline }; if (pipelineCopy.triggered_by) { pipelineCopy.triggered_by = [pipelineCopy.triggered_by]; diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js index bb7071b020b..3ba13419b98 100644 --- a/app/assets/javascripts/registry/settings/store/mutations.js +++ b/app/assets/javascripts/registry/settings/store/mutations.js @@ -21,7 +21,7 @@ export default { state.original = Object.freeze(settings); }, [types.RESET_SETTINGS](state) { - state.settings = Object.assign({}, state.original); + state.settings = { ...state.original }; }, [types.TOGGLE_LOADING](state) { state.isLoading = !state.isLoading; diff --git a/app/assets/javascripts/registry/shared/constants.js b/app/assets/javascripts/registry/shared/constants.js index 7a839e4a3ed..4689d01b1c8 100644 --- a/app/assets/javascripts/registry/shared/constants.js +++ b/app/assets/javascripts/registry/shared/constants.js @@ -41,5 +41,5 @@ export const NAME_REGEX_KEEP_LABEL = s__( ); export const NAME_REGEX_KEEP_PLACEHOLDER = ''; export const NAME_REGEX_KEEP_DESCRIPTION = s__( - 'ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported', + 'ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported', ); diff --git a/app/assets/javascripts/releases/components/app_edit.vue b/app/assets/javascripts/releases/components/app_edit.vue index 1a1b2591cc8..433df839acb 100644 --- a/app/assets/javascripts/releases/components/app_edit.vue +++ b/app/assets/javascripts/releases/components/app_edit.vue @@ -9,6 +9,7 @@ import { BACK_URL_PARAM } from '~/releases/constants'; import { getParameterByName } from '~/lib/utils/common_utils'; import AssetLinksForm from './asset_links_form.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue'; export default { name: 'ReleaseEditApp', @@ -18,6 +19,7 @@ export default { GlButton, MarkdownField, AssetLinksForm, + MilestoneCombobox, }, directives: { autofocusonshow, @@ -32,6 +34,10 @@ export default { 'markdownPreviewPath', 'releasesPagePath', 'updateReleaseApiDocsPath', + 'release', + 'newMilestonePath', + 'manageMilestonesPath', + 'projectId', ]), ...mapGetters('detail', ['isValid']), showForm() { @@ -82,6 +88,14 @@ export default { this.updateReleaseNotes(notes); }, }, + releaseMilestones: { + get() { + return this.$store.state.detail.release.milestones; + }, + set(milestones) { + this.updateReleaseMilestones(milestones); + }, + }, cancelPath() { return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath; }, @@ -91,6 +105,18 @@ export default { isSaveChangesDisabled() { return this.isUpdatingRelease || !this.isValid; }, + milestoneComboboxExtraLinks() { + return [ + { + text: __('Create new'), + url: this.newMilestonePath, + }, + { + text: __('Manage milestones'), + url: this.manageMilestonesPath, + }, + ]; + }, }, created() { this.fetchRelease(); @@ -101,6 +127,7 @@ export default { 'updateRelease', 'updateReleaseTitle', 'updateReleaseNotes', + 'updateReleaseMilestones', ]), }, }; @@ -137,6 +164,16 @@ export default { class="form-control" /> </gl-form-group> + <gl-form-group class="w-50"> + <label>{{ __('Milestones') }}</label> + <div class="d-flex flex-column col-md-6 col-sm-10 pl-0"> + <milestone-combobox + v-model="releaseMilestones" + :project-id="projectId" + :extra-links="milestoneComboboxExtraLinks" + /> + </div> + </gl-form-group> <gl-form-group> <label for="release-notes">{{ __('Release notes') }}</label> <div class="bordered-box pr-3 pl-3"> @@ -158,8 +195,7 @@ export default { :placeholder="__('Write your release notes or drag your files hereā¦')" @keydown.meta.enter="updateRelease()" @keydown.ctrl.enter="updateRelease()" - > - </textarea> + ></textarea> </markdown-field> </div> </gl-form-group> @@ -174,12 +210,9 @@ export default { type="submit" :aria-label="__('Save changes')" :disabled="isSaveChangesDisabled" + >{{ __('Save changes') }}</gl-button > - {{ __('Save changes') }} - </gl-button> - <gl-button :href="cancelPath" class="js-cancel-button"> - {{ __('Cancel') }} - </gl-button> + <gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button> </div> </form> </div> diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index 215a376fc76..67085ecca2b 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading, GlEmptyState, GlLink } from '@gitlab/ui'; +import { GlSkeletonLoading, GlEmptyState, GlLink, GlButton } from '@gitlab/ui'; import { getParameterByName, historyPushState, @@ -18,6 +18,7 @@ export default { ReleaseBlock, TablePagination, GlLink, + GlButton, }, props: { projectId: { @@ -69,14 +70,16 @@ export default { </script> <template> <div class="flex flex-column mt-2"> - <gl-link + <gl-button v-if="newReleasePath" :href="newReleasePath" :aria-describedby="shouldRenderEmptyState && 'releases-description'" - class="btn btn-success align-self-end mb-2 js-new-release-btn" + category="primary" + variant="success" + class="align-self-end mb-2 js-new-release-btn" > {{ __('New release') }} - </gl-link> + </gl-button> <gl-skeleton-loading v-if="isLoading" class="js-loading" /> diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue index 1e703c247ae..ed49841757a 100644 --- a/app/assets/javascripts/releases/components/release_block_header.vue +++ b/app/assets/javascripts/releases/components/release_block_header.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; import { BACK_URL_PARAM } from '~/releases/constants'; import { setUrlParams } from '~/lib/utils/url_utility'; @@ -10,6 +10,7 @@ export default { GlLink, GlBadge, Icon, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -50,14 +51,16 @@ export default { __('Upcoming Release') }}</gl-badge> </h2> - <gl-link + <gl-button v-if="editLink" v-gl-tooltip - class="btn btn-default append-right-10 js-edit-button ml-2" + category="primary" + variant="default" + class="append-right-10 js-edit-button ml-2 pb-2" :title="__('Edit this release')" :href="editLink" > <icon name="pencil" /> - </gl-link> + </gl-button> </div> </template> diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js index 7b84c18242c..3bc427dfa16 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/actions.js +++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js @@ -18,7 +18,12 @@ export const fetchRelease = ({ dispatch, state }) => { return api .release(state.projectId, state.tagName) - .then(({ data: release }) => { + .then(({ data }) => { + const release = { + ...data, + milestones: data.milestones || [], + }; + dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true })); }) .catch(error => { @@ -28,6 +33,8 @@ export const fetchRelease = ({ dispatch, state }) => { export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title); export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes); +export const updateReleaseMilestones = ({ commit }, milestones) => + commit(types.UPDATE_RELEASE_MILESTONES, milestones); export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE); export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => { @@ -45,12 +52,14 @@ export const updateRelease = ({ dispatch, state, getters }) => { dispatch('requestUpdateRelease'); const { release } = state; + const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : []; return ( api .updateRelease(state.projectId, state.tagName, { name: release.name, description: release.description, + milestones, }) /** diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js index 04944b76e42..1d6356990ce 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js +++ b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js @@ -4,6 +4,7 @@ export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR'; export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE'; export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES'; +export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES'; export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE'; export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS'; diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js index 3d97e3a75c2..5c29b402cba 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js +++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js @@ -28,6 +28,10 @@ export default { state.release.description = notes; }, + [types.UPDATE_RELEASE_MILESTONES](state, milestones) { + state.release.milestones = milestones; + }, + [types.REQUEST_UPDATE_RELEASE](state) { state.isUpdatingRelease = true; }, diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js index b513e1bed79..6d0d102c719 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/state.js +++ b/app/assets/javascripts/releases/stores/modules/detail/state.js @@ -6,6 +6,8 @@ export default ({ markdownPreviewPath, updateReleaseApiDocsPath, releaseAssetsDocsPath, + manageMilestonesPath, + newMilestonePath, }) => ({ projectId, tagName, @@ -14,6 +16,8 @@ export default ({ markdownPreviewPath, updateReleaseApiDocsPath, releaseAssetsDocsPath, + manageMilestonesPath, + newMilestonePath, /** The Release object */ release: null, diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index f4e546e4d4e..cf9064aba57 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -13,14 +13,11 @@ Terminal.applyAddon(webLinks); export default class GLTerminal { constructor(element, options = {}) { - this.options = Object.assign( - {}, - { - cursorBlink: true, - screenKeys: true, - }, - options, - ); + this.options = { + cursorBlink: true, + screenKeys: true, + ...options, + }; this.container = element; this.onDispose = []; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue index a95a5a50a8b..f38b66fdfdf 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue @@ -164,7 +164,11 @@ export default { 'js-dropdown-button', 'js-btn-cancel-create', 'js-sidebar-dropdown-toggle', - ].some(className => target?.classList.contains(className)); + ].some( + className => + target?.classList.contains(className) || + target?.parentElement.classList.contains(className), + ); const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( className => $(target).parents(className).length, diff --git a/app/assets/stylesheets/components/milestone_combobox.scss b/app/assets/stylesheets/components/milestone_combobox.scss new file mode 100644 index 00000000000..e0637088bbb --- /dev/null +++ b/app/assets/stylesheets/components/milestone_combobox.scss @@ -0,0 +1,13 @@ +.selected-item::before { + content: '\f00c'; + color: $green-500; + position: absolute; + left: 16px; + top: 16px; + transform: translateY(-50%); + font: 14px FontAwesome; +} + +.dropdown-item-space { + padding: 8px 12px; +} diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb index 9be8a89fc02..9c919186c42 100644 --- a/app/controllers/projects/alert_management_controller.rb +++ b/app/controllers/projects/alert_management_controller.rb @@ -3,6 +3,7 @@ class Projects::AlertManagementController < Projects::ApplicationController before_action :ensure_list_feature_enabled, only: :index before_action :ensure_detail_feature_enabled, only: :details + before_action :authorize_read_alert_management_alert! before_action do push_frontend_feature_flag(:alert_list_status_filtering_enabled) end diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb index 061cfb1fde1..a48dadc7fdb 100644 --- a/app/finders/alert_management/alerts_finder.rb +++ b/app/finders/alert_management/alerts_finder.rb @@ -31,7 +31,7 @@ module AlertManagement end def authorized? - Ability.allowed?(current_user, :read_alert_management_alerts, project) + Ability.allowed?(current_user, :read_alert_management_alert, project) end end end diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index eb0d2304ba3..37103c63a3d 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -18,7 +18,7 @@ module Mutations null: true, description: "The alert after mutation" - authorize :update_alert_management_alerts + authorize :update_alert_management_alert private diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index 69fc2718f1e..c0283e6d476 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -6,7 +6,7 @@ module Types graphql_name 'AlertManagementAlert' description "Describes an alert from the project's Alert Management" - authorize :read_alert_management_alerts + authorize :read_alert_management_alert field :iid, GraphQL::ID_TYPE, diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3fb0e600465..2151d1a85d7 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -448,7 +448,7 @@ module ProjectsHelper clusters: :read_cluster, serverless: :read_cluster, error_tracking: :read_sentry_issue, - alert_management: :read_alert_management, + alert_management: :read_alert_management_alert, labels: :read_label, issues: :read_issue, project_members: :read_project_member, diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb index af51427dc91..1238567a4ed 100644 --- a/app/helpers/releases_helper.rb +++ b/app/helpers/releases_helper.rb @@ -30,7 +30,9 @@ module ReleasesHelper markdown_docs_path: help_page_path('user/markdown'), releases_page_path: project_releases_path(@project, anchor: @release.tag), update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'), - release_assets_docs_path: help_page(anchor: 'release-assets') + release_assets_docs_path: help_page(anchor: 'release-assets'), + manage_milestones_path: project_milestones_path(@project), + new_milestone_path: new_project_milestone_url(@project) } end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 6aa3d791a0f..a8105ae6f7c 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -236,11 +236,8 @@ class ProjectPolicy < BasePolicy enable :read_merge_request enable :read_sentry_issue enable :update_sentry_issue - enable :read_alert_management enable :read_prometheus enable :read_metrics_dashboard_annotation - enable :read_alert_management_alerts - enable :update_alert_management_alerts enable :metrics_dashboard end @@ -306,6 +303,8 @@ class ProjectPolicy < BasePolicy enable :create_metrics_dashboard_annotation enable :delete_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation + enable :read_alert_management_alert + enable :update_alert_management_alert enable :create_design enable :destroy_design end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index c96599f9958..bf21eba28f7 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -6,6 +6,9 @@ class SearchService SEARCH_TERM_LIMIT = 64 SEARCH_CHAR_LIMIT = 4096 + DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE + MAX_PER_PAGE = 200 + def initialize(current_user, params = {}) @current_user = current_user @params = params.dup @@ -60,11 +63,19 @@ class SearchService end def search_objects - @search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page])) + @search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page)) end private + def per_page + per_page_param = params[:per_page].to_i + + return DEFAULT_PER_PAGE unless per_page_param.positive? + + [MAX_PER_PAGE, per_page_param].min + end + def visible_result?(object) return true unless object.respond_to?(:to_ability_name) && DeclarativePolicy.has_policy?(object) @@ -75,13 +86,13 @@ class SearchService results = results_collection.to_a permitted_results = results.select { |object| visible_result?(object) } - filtered_results = (results - permitted_results).each_with_object({}) do |object, memo| + redacted_results = (results - permitted_results).each_with_object({}) do |object, memo| memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name } end - log_redacted_search_results(filtered_results.values) if filtered_results.any? + log_redacted_search_results(redacted_results.values) if redacted_results.any? - return results_collection.id_not_in(filtered_results.keys) if results_collection.is_a?(ActiveRecord::Relation) + return results_collection.id_not_in(redacted_results.keys) if results_collection.is_a?(ActiveRecord::Relation) Kaminari.paginate_array( permitted_results, |