summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-07 21:09:26 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-07 21:09:26 +0000
commit17c8111494f51e79744c782db023804f5e4a7410 (patch)
tree8aebe53b8aea72f9d4abac1bf9131203302a5b6e /app
parent4b7575da97d88ef4c7b2ec599b0c3fc457b4f561 (diff)
downloadgitlab-ce-17c8111494f51e79744c782db023804f5e4a7410.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js47
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js2
-rw-r--r--app/assets/javascripts/blob/blob_fork_suggestion.js2
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js5
-rw-r--r--app/assets/javascripts/close_reopen_report_toggle.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js2
-rw-r--r--app/assets/javascripts/comment_type_toggle.js2
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js2
-rw-r--r--app/assets/javascripts/design_management/components/design_note_pin.vue4
-rw-r--r--app/assets/javascripts/diffs/store/utils.js2
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js9
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/stores/recent_searches_store.js14
-rw-r--r--app/assets/javascripts/gl_dropdown.js5
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/groups/new_group_child.js2
-rw-r--r--app/assets/javascripts/ide/components/nav_form.vue4
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js13
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js5
-rw-r--r--app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js4
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js7
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js4
-rw-r--r--app/assets/javascripts/jobs/store/actions.js4
-rw-r--r--app/assets/javascripts/milestones/project_milestone_combobox.vue228
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js31
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue5
-rw-r--r--app/assets/javascripts/notes/index.js50
-rw-r--r--app/assets/javascripts/notes/stores/actions.js2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue9
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js2
-rw-r--r--app/assets/javascripts/registry/settings/store/mutations.js2
-rw-r--r--app/assets/javascripts/registry/shared/constants.js2
-rw-r--r--app/assets/javascripts/releases/components/app_edit.vue47
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue11
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue11
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js11
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutation_types.js1
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutations.js4
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/state.js4
-rw-r--r--app/assets/javascripts/terminal/terminal.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue6
-rw-r--r--app/assets/stylesheets/components/milestone_combobox.scss13
-rw-r--r--app/controllers/projects/alert_management_controller.rb1
-rw-r--r--app/finders/alert_management/alerts_finder.rb2
-rw-r--r--app/graphql/mutations/alert_management/base.rb2
-rw-r--r--app/graphql/types/alert_management/alert_type.rb2
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/releases_helper.rb4
-rw-r--r--app/policies/project_policy.rb5
-rw-r--r--app/services/search_service.rb19
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,