summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-09 15:07:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-09 15:07:50 +0000
commit608d6aaa3d80a33862ca2c29d96bfd687b1a011b (patch)
tree665f96928bb42b40cbc34d70a09ee951f15fb468
parent6180f62ab34662c64103872b8352b25817b73a8d (diff)
downloadgitlab-ce-608d6aaa3d80a33862ca2c29d96bfd687b1a011b.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/gl_form.js19
-rw-r--r--app/assets/javascripts/jobs/store/actions.js9
-rw-r--r--app/assets/javascripts/lib/utils/scroll_utils.js29
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_app.vue4
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_dropdown.vue22
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js66
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js91
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js116
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js23
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue19
-rw-r--r--app/assets/javascripts/work_items/utils.js17
-rw-r--r--app/assets/stylesheets/framework/mixins.scss35
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss11
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb4
-rw-r--r--app/models/concerns/counter_attribute.rb17
-rw-r--r--app/models/snippet_user_mention.rb4
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml59
-rw-r--r--config/feature_flags/development/github_client_fetch_repos_via_graphql.yml2
-rw-r--r--config/feature_flags/development/mr_compare_dropdowns.yml8
-rw-r--r--config/feature_flags/ops/split_log_bulk_increment_counter.yml8
-rw-r--r--db/migrate/20230207005549_initialize_conversion_of_snippet_user_mentions_note_id_to_bigint.rb16
-rw-r--r--db/post_migrate/20230207005701_backfill_snippet_user_mentions_note_id_for_bigint_conversion.rb16
-rw-r--r--db/schema_migrations/202302070055491
-rw-r--r--db/schema_migrations/202302070057011
-rw-r--r--db/structure.sql14
-rw-r--r--doc/administration/geo/replication/troubleshooting.md19
-rw-r--r--doc/api/group_epic_boards.md110
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/api/runners.md10
-rw-r--r--doc/architecture/blueprints/ci_pipeline_components/index.md46
-rw-r--r--doc/user/group/epics/manage_epics.md1
-rw-r--r--doc/user/project/quick_actions.md28
-rw-r--r--doc/user/ssh.md6
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa/page/merge_request/new.rb3
-rw-r--r--spec/frontend/gl_form_spec.js41
-rw-r--r--spec/frontend/lib/utils/scroll_utils_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js25
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js44
-rw-r--r--spec/frontend/work_items/utils_spec.js27
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb64
47 files changed, 673 insertions, 451 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index b4edbf7434d..8c585d8e1a8 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-b0919f19443fbc6a155caf6d029fbe1cdd19a4c0
+4a6f31c9182921e5ca14e1b273e8440e510fb403
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 2b157fac878..f4008fe3cc9 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,5 +1,6 @@
import autosize from 'autosize';
import $ from 'jquery';
+import { isEmpty } from 'lodash';
import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_complete';
import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
import dropzoneInput from './dropzone_input';
@@ -12,14 +13,22 @@ export default class GLForm {
* @param {jQuery} form Root element of the GLForm
* @param {Object} enableGFM Which autocomplete features should be enabled?
* @param {Boolean} forceNew If true, treat the element as a **new** form even if `gfm-form` class already exists.
+ * @param {Object} gfmDataSources The paths of the autocomplete data sources to use for GfmAutoComplete
+ * By default, the backend embeds these in the global object gl.GfmAutocomplete.dataSources.
+ * Use this param to override them.
*/
- constructor(form, enableGFM = {}, forceNew = false) {
+ constructor(form, enableGFM = {}, forceNew = false, gfmDataSources = {}) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM };
// Disable autocomplete for keywords which do not have dataSources available
- const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
+ let dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
+
+ if (!isEmpty(gfmDataSources)) {
+ dataSources = gfmDataSources;
+ }
+
Object.keys(this.enableGFM).forEach((item) => {
if (item !== 'emojis' && !dataSources[item]) {
this.enableGFM[item] = false;
@@ -29,7 +38,7 @@ export default class GLForm {
// Before we start, we should clean up any previous data for this form
this.destroy();
// Set up the form
- this.setupForm(forceNew);
+ this.setupForm(dataSources, forceNew);
this.form.data('glForm', this);
}
@@ -46,7 +55,7 @@ export default class GLForm {
this.form.data('glForm', null);
}
- setupForm(forceNew = false) {
+ setupForm(dataSources, forceNew = false) {
const isNewForm = this.form.is(':not(.gfm-form)') || forceNew;
this.form.removeClass('js-new-note-form');
if (isNewForm) {
@@ -57,7 +66,7 @@ export default class GLForm {
this.form.find('.js-note-text'),
this.form.find('.js-comment-button, .js-note-new-discussion'),
);
- this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
+ this.autoComplete = new GfmAutoComplete(dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
this.formDropzone = dropzoneInput(this.form, { parallelUploads: 1 });
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index fb4cb64c9cb..af2d720643f 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -8,7 +8,6 @@ import {
canScroll,
isScrolledToBottom,
isScrolledToTop,
- isScrolledToMiddle,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
@@ -124,15 +123,15 @@ export const scrollBottom = ({ dispatch }) => {
*/
export const toggleScrollButtons = ({ dispatch }) => {
if (canScroll()) {
- if (isScrolledToMiddle()) {
- dispatch('enableScrollTop');
- dispatch('enableScrollBottom');
- } else if (isScrolledToTop()) {
+ if (isScrolledToTop()) {
dispatch('disableScrollTop');
dispatch('enableScrollBottom');
} else if (isScrolledToBottom()) {
dispatch('disableScrollBottom');
dispatch('enableScrollTop');
+ } else {
+ dispatch('enableScrollTop');
+ dispatch('enableScrollBottom');
}
} else {
dispatch('disableScrollBottom');
diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js
index 01e43fd3b93..bab84448657 100644
--- a/app/assets/javascripts/lib/utils/scroll_utils.js
+++ b/app/assets/javascripts/lib/utils/scroll_utils.js
@@ -7,14 +7,11 @@ export const canScroll = () => $(document).height() > $(window).height();
* @returns {Boolean}
*/
export const isScrolledToBottom = () => {
- const $document = $(document);
-
- const currentPosition = $document.scrollTop();
- const scrollHeight = $document.height();
-
- const windowHeight = $(window).height();
+ // Use clientHeight to account for any horizontal scrollbar.
+ const { scrollHeight, scrollTop, clientHeight } = document.documentElement;
- return scrollHeight - currentPosition === windowHeight;
+ // scrollTop can be a float, so round up to next integer.
+ return Math.ceil(scrollTop + clientHeight) >= scrollHeight;
};
/**
@@ -31,21 +28,3 @@ export const scrollDown = () => {
export const scrollUp = () => {
$(document).scrollTop(0);
};
-
-/**
- * Checks if scroll position is in the middle of the page
- * @returns {Boolean}
- */
-export const isScrolledToMiddle = () => {
- const $document = $(document);
- const currentPosition = $document.scrollTop();
- const scrollHeight = $document.height();
- const windowHeight = $(window).height();
-
- return currentPosition > 0 && scrollHeight - currentPosition !== windowHeight;
-};
-
-export const toggleDisableButton = ($button, disable) => {
- if (disable && $button.prop('disabled')) return;
- $button.prop('disabled', disable);
-};
diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue
index 0e60f4bd8f9..8e02048f494 100644
--- a/app/assets/javascripts/merge_requests/components/compare_app.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_app.vue
@@ -35,6 +35,9 @@ export default {
toggleClass: {
default: () => ({}),
},
+ branchQaSelector: {
+ default: '',
+ },
},
data() {
return {
@@ -105,6 +108,7 @@ export default {
:input-name="inputs.branch.name"
:default="currentBranch"
:toggle-class="toggleClass.branch"
+ :qa-selector="branchQaSelector"
@selected="selectBranch"
/>
</div>
diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
index e005044b4e5..d70cf26ec6e 100644
--- a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlListbox } from '@gitlab/ui';
+import { GlListbox, GlButton, GlIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
@@ -8,6 +8,8 @@ import axios from '~/lib/utils/axios_utils';
export default {
components: {
GlListbox,
+ GlButton,
+ GlIcon,
},
props: {
staticData: {
@@ -46,6 +48,11 @@ export default {
required: false,
default: '',
},
+ qaSelector: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -134,6 +141,17 @@ export default {
@shown="fetchData"
@search="searchData"
@select="selectItem"
- />
+ >
+ <template #toggle>
+ <gl-button
+ class="gl-w-full gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown"
+ :class="toggleClass"
+ :data-qa-selector="qaSelector"
+ >
+ {{ current.text || dropdownHeader }}
+ <gl-icon name="chevron-down" class="gl-new-dropdown-chevron gl-float-right" />
+ </gl-button>
+ </template>
+ </gl-listbox>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
deleted file mode 100644
index 653f903c6d1..00000000000
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
-import { localTimeAgo } from '~/lib/utils/datetime_utility';
-import initCompareAutocomplete from './compare_autocomplete';
-import initTargetProjectDropdown from './target_project_dropdown';
-
-const updateCommitList = (url, $emptyState, $loadingIndicator, $commitList, params) => {
- $emptyState.hide();
- $loadingIndicator.show();
- $commitList.empty();
-
- return axios
- .get(url, {
- params,
- })
- .then(({ data }) => {
- $loadingIndicator.hide();
- $commitList.html(data);
- localTimeAgo($commitList.get(0).querySelectorAll('.js-timeago'));
-
- if (!data) {
- $emptyState.show();
- }
- });
-};
-
-export default (mrNewCompareNode) => {
- const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
-
- if (!window.gon?.features?.mrCompareDropdowns) {
- initTargetProjectDropdown();
- }
-
- const updateSourceBranchCommitList = () =>
- updateCommitList(
- sourceBranchUrl,
- $(mrNewCompareNode).find('.js-source-commit-empty'),
- $(mrNewCompareNode).find('.js-source-loading'),
- $(mrNewCompareNode).find('.mr_source_commit'),
- {
- ref: $(mrNewCompareNode).find("input[name='merge_request[source_branch]']").val(),
- },
- );
- const updateTargetBranchCommitList = () =>
- updateCommitList(
- targetBranchUrl,
- $(mrNewCompareNode).find('.js-target-commit-empty'),
- $(mrNewCompareNode).find('.js-target-loading'),
- $(mrNewCompareNode).find('.mr_target_commit'),
- {
- target_project_id: $(mrNewCompareNode)
- .find("input[name='merge_request[target_project_id]']")
- .val(),
- ref: $(mrNewCompareNode).find("input[name='merge_request[target_branch]']").val(),
- },
- );
- initCompareAutocomplete('branches', ($dropdown) => {
- if ($dropdown.is('.js-target-branch')) {
- updateTargetBranchCommitList();
- } else if ($dropdown.is('.js-source-branch')) {
- updateSourceBranchCommitList();
- }
- });
- updateSourceBranchCommitList();
- updateTargetBranchCommitList();
-};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
deleted file mode 100644
index 65942464e2b..00000000000
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
+++ /dev/null
@@ -1,91 +0,0 @@
-/* eslint-disable func-names */
-
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { __ } from '~/locale';
-import { fixTitle } from '~/tooltips';
-
-export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
- $('.js-compare-dropdown').each(function () {
- const $dropdown = $(this);
- const selected = $dropdown.data('selected');
- const defaultText = $dropdown.data('defaultText').trim();
- const $dropdownContainer = $dropdown.closest('.dropdown');
- const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
- const $filterInput = $('input[type="search"]', $dropdownContainer);
- initDeprecatedJQueryDropdown($dropdown, {
- data(term, callback) {
- const params = {
- ref: $dropdown.data('ref'),
- search: term,
- };
-
- if (limitTo) {
- params.find = limitTo;
- }
-
- axios
- .get($dropdown.data('refsUrl'), {
- params,
- })
- .then(({ data }) => {
- if (limitTo) {
- callback(data[capitalizeFirstCharacter(limitTo)] || []);
- } else {
- callback(data);
- }
- })
- .catch(() =>
- createAlert({
- message: __('Error fetching refs'),
- }),
- );
- },
- selectable: true,
- filterable: true,
- filterRemote: Boolean($dropdown.data('refsUrl')),
- fieldName: $dropdown.data('fieldName'),
- filterInput: 'input[type="search"]',
- renderRow(ref) {
- const link = $('<a />')
- .attr('href', '#')
- .addClass(ref === selected ? 'is-active' : '')
- .text(ref)
- .attr('data-ref', ref);
- if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
- }
- return $('<li />').append(link);
- },
- id(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel(obj, $el) {
- if ($el.hasClass('is-active')) {
- return $el.text().trim();
- }
-
- return defaultText;
- },
- clicked: () => clickHandler($dropdown),
- });
- $filterInput.on('keyup', (e) => {
- const keyCode = e.keyCode || e.which;
- if (keyCode !== 13) return;
- const text = $filterInput.val();
- $fieldInput.val(text);
- $('.dropdown-toggle-text', $dropdown).text(text);
- $dropdownContainer.removeClass('open');
- });
-
- $dropdownContainer.on('click', '.dropdown-content a', (e) => {
- $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
- if ($dropdown.hasClass('has-tooltip')) {
- fixTitle($dropdown);
- }
- });
- });
-}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index d2ac23c48e2..868fa204a55 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -3,7 +3,6 @@ import initPipelines from '~/commit/pipelines/pipelines_bundle';
import MergeRequest from '~/merge_request';
import CompareApp from '~/merge_requests/components/compare_app.vue';
import { __ } from '~/locale';
-import initCompare from './compare';
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
@@ -11,74 +10,71 @@ if (mrNewCompareNode) {
const sourceCompareEl = document.getElementById('js-source-project-dropdown');
const compareEl = document.querySelector('.js-merge-request-new-compare');
- if (window.gon?.features?.mrCompareDropdowns) {
- // eslint-disable-next-line no-new
- new Vue({
- el: sourceCompareEl,
- name: 'SourceCompareApp',
- provide: {
- currentProject: JSON.parse(sourceCompareEl.dataset.currentProject),
- currentBranch: JSON.parse(sourceCompareEl.dataset.currentBranch),
- branchCommitPath: compareEl.dataset.sourceBranchUrl,
- inputs: {
- project: {
- id: 'merge_request_source_project_id',
- name: 'merge_request[source_project_id]',
- },
- branch: {
- id: 'merge_request_source_branch',
- name: 'merge_request[source_branch]',
- },
- },
- i18n: {
- projectHeaderText: __('Select source project'),
- branchHeaderText: __('Select source branch'),
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: sourceCompareEl,
+ name: 'SourceCompareApp',
+ provide: {
+ currentProject: JSON.parse(sourceCompareEl.dataset.currentProject),
+ currentBranch: JSON.parse(sourceCompareEl.dataset.currentBranch),
+ branchCommitPath: compareEl.dataset.sourceBranchUrl,
+ inputs: {
+ project: {
+ id: 'merge_request_source_project_id',
+ name: 'merge_request[source_project_id]',
},
- toggleClass: {
- project: 'js-source-project',
- branch: 'js-source-branch',
+ branch: {
+ id: 'merge_request_source_branch',
+ name: 'merge_request[source_branch]',
},
},
- render(h) {
- return h(CompareApp);
+ i18n: {
+ projectHeaderText: __('Select source project'),
+ branchHeaderText: __('Select source branch'),
},
- });
+ toggleClass: {
+ project: 'js-source-project',
+ branch: 'js-source-branch',
+ },
+ branchQaSelector: 'source_branch_dropdown',
+ },
+ render(h) {
+ return h(CompareApp);
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: targetCompareEl,
- name: 'TargetCompareApp',
- provide: {
- currentProject: JSON.parse(targetCompareEl.dataset.currentProject),
- currentBranch: JSON.parse(targetCompareEl.dataset.currentBranch),
- projectsPath: targetCompareEl.dataset.targetProjectsPath,
- branchCommitPath: compareEl.dataset.targetBranchUrl,
- inputs: {
- project: {
- id: 'merge_request_target_project_id',
- name: 'merge_request[target_project_id]',
- },
- branch: {
- id: 'merge_request_target_branch',
- name: 'merge_request[target_branch]',
- },
- },
- i18n: {
- projectHeaderText: __('Select target project'),
- branchHeaderText: __('Select target branch'),
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: targetCompareEl,
+ name: 'TargetCompareApp',
+ provide: {
+ currentProject: JSON.parse(targetCompareEl.dataset.currentProject),
+ currentBranch: JSON.parse(targetCompareEl.dataset.currentBranch),
+ projectsPath: targetCompareEl.dataset.targetProjectsPath,
+ branchCommitPath: compareEl.dataset.targetBranchUrl,
+ inputs: {
+ project: {
+ id: 'merge_request_target_project_id',
+ name: 'merge_request[target_project_id]',
},
- toggleClass: {
- project: 'js-target-project',
- branch: 'js-target-branch',
+ branch: {
+ id: 'merge_request_target_branch',
+ name: 'merge_request[target_branch]',
},
},
- render(h) {
- return h(CompareApp);
+ i18n: {
+ projectHeaderText: __('Select target project'),
+ branchHeaderText: __('Select target branch'),
},
- });
- } else {
- initCompare(mrNewCompareNode);
- }
+ toggleClass: {
+ project: 'js-target-project',
+ branch: 'js-target-branch',
+ },
+ },
+ render(h) {
+ return h(CompareApp);
+ },
+ });
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
deleted file mode 100644
index e9f0e008435..00000000000
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-
-export default () => {
- const $targetProjectDropdown = $('.js-target-project');
- initDeprecatedJQueryDropdown($targetProjectDropdown, {
- selectable: true,
- fieldName: $targetProjectDropdown.data('fieldName'),
- filterable: true,
- id(obj, $el) {
- return $el.data('id');
- },
- toggleLabel(obj, $el) {
- return $el.text().trim();
- },
- clicked({ $el }) {
- $('.mr_target_commit').empty();
- const $targetBranchDropdown = $('.js-target-branch');
- $targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
- $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu();
- },
- });
-};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 7b76fc3fc6d..6f4cddbdfa2 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -82,6 +82,11 @@ export default {
required: false,
default: true,
},
+ autocompleteDataSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
line: {
type: Object,
required: false,
@@ -257,6 +262,7 @@ export default {
contacts: this.enableAutocomplete,
},
true,
+ this.autocompleteDataSources,
);
},
beforeDestroy() {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 9d294369afa..7e6b0e4a63b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -46,6 +46,11 @@ export default {
required: false,
default: true,
},
+ autocompleteDataSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
enableAutocomplete: {
type: Boolean,
required: false,
@@ -139,6 +144,7 @@ export default {
:textarea-value="value"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
+ :autocomplete-data-sources="autocompleteDataSources"
:uploads-path="uploadsPath"
:enable-preview="enablePreview"
show-content-editor-switcher
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index a93b1450012..399c220bc96 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -10,7 +10,7 @@ import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import { getWorkItemQuery } from '../utils';
+import { getWorkItemQuery, autocompleteDataSources, markdownPreviewPath } from '../utils';
import workItemDescriptionSubscription from '../graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
@@ -46,7 +46,8 @@ export default {
required: true,
},
},
- markdownDocsPath: helpPagePath('user/markdown'),
+ markdownDocsPath: helpPagePath('user/project/quick_actions'),
+ quickActionsDocsPath: helpPagePath('user/project/quick_actions'),
data() {
return {
workItem: {},
@@ -140,9 +141,10 @@ export default {
return this.workItemDescription?.lastEditedBy?.webPath;
},
markdownPreviewPath() {
- return `${gon.relative_url_root || ''}/${this.fullPath}/preview_markdown?target_type=${
- this.workItemType
- }`;
+ return markdownPreviewPath(this.fullPath, this.workItem.iid);
+ },
+ autocompleteDataSources() {
+ return autocompleteDataSources(this.fullPath, this.workItem.iid);
},
},
methods: {
@@ -248,7 +250,10 @@ export default {
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
:form-field-props="formFieldProps"
+ :quick-actions-docs-path="$options.quickActionsDocsPath"
+ :autocomplete-data-sources="autocompleteDataSources"
enable-autocomplete
+ supports-quick-actions
init-on-autofocus
use-bottom-toolbar
@input="setDescriptionText"
@@ -262,6 +267,8 @@ export default {
:is-submitting="isSubmitting"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
+ :quick-actions-docs-path="$options.quickActionsDocsPath"
+ :autocomplete-data-sources="autocompleteDataSources"
class="gl-px-3 bordered-box gl-mt-5"
>
<template #textarea>
@@ -272,7 +279,7 @@ export default {
:disabled="isSubmitting"
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
- data-supports-quick-actions="false"
+ data-supports-quick-actions="true"
@keydown.meta.enter="updateWorkItem"
@keydown.ctrl.enter="updateWorkItem"
@keydown.exact.esc.stop="cancelEditing"
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 93d9c48ba27..3274dab37f3 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -14,3 +14,20 @@ export function getWorkItemNotesQuery(isFetchedByIid) {
export const findHierarchyWidgetChildren = (workItem) =>
workItem.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY).children.nodes;
+
+const autocompleteSourcesPath = (autocompleteType, fullPath, workItemIid) => {
+ return `${
+ gon.relative_url_root || ''
+ }/${fullPath}/-/autocomplete_sources/${autocompleteType}?type=WorkItem&type_id=${workItemIid}`;
+};
+
+export const autocompleteDataSources = (fullPath, iid) => ({
+ labels: autocompleteSourcesPath('labels', fullPath, iid),
+ members: autocompleteSourcesPath('members', fullPath, iid),
+ commands: autocompleteSourcesPath('commands', fullPath, iid),
+});
+
+export const markdownPreviewPath = (fullPath, iid) =>
+ `${
+ gon.relative_url_root || ''
+ }/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 628406d5889..c5e50299e6d 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -21,20 +21,6 @@
}
}
-@keyframes blinking-scroll-button {
- 0% {
- opacity: 0.2;
- }
-
- 50% {
- opacity: 1;
- }
-
- 100% {
- opacity: 0.2;
- }
-}
-
@mixin str-truncated($max-width: 82%) {
display: inline-block;
overflow: hidden;
@@ -308,27 +294,6 @@
margin-right: 0;
}
}
-
- .btn-scroll.animate {
- .scroll-arrow {
- animation: blinking-scroll-button 1.5s ease-in-out infinite;
- }
-
- .scroll-dot {
- animation: blinking-scroll-button 1.5s ease-in-out infinite;
- animation-delay: 0.3s;
- }
-
- &:disabled {
- opacity: 1;
- }
- }
-
- .btn-scroll:disabled,
- .btn-refresh:disabled {
- opacity: 0.35;
- cursor: not-allowed;
- }
}
@mixin build-loader-animation {
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index e1153bdbff2..4b55b39d6f3 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -37,6 +37,7 @@
.btn-with-notification {
mix-blend-mode: unset !important; // Our tertiary buttons otherwise use another mix-blend mode, making border-color semi-transparent.
+ position: relative;
.notification {
background-color: $blue-500;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index c0cb831149b..a128efc1e69 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -185,14 +185,8 @@ $comparison-empty-state-height: 62px;
margin-bottom: 0;
}
- .dropdown-menu-toggle {
- width: 100%;
- }
-
- .dropdown-menu {
- left: 5px;
- right: 5px;
- width: auto;
+ .gl-dropdown-custom-toggle {
+ @include gl-w-full;
}
}
@@ -388,6 +382,7 @@ $comparison-empty-state-height: 62px;
.gl-button-text {
@include gl-w-full;
+ @include gl-text-left;
}
}
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 24767804436..3b399e3294e 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -20,10 +20,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
:branch_to
]
- before_action do
- push_frontend_feature_flag(:mr_compare_dropdowns, project)
- end
-
def new
define_new_vars
end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index 784afd1f231..58ea57962c5 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -93,7 +93,7 @@ module CounterAttribute
run_after_commit_or_now do
new_value = counter(attribute).increment(increment)
- log_increment_counter(attribute, increment.amount, new_value)
+ log_increment_counter(attribute, increment, new_value)
end
end
@@ -101,7 +101,7 @@ module CounterAttribute
run_after_commit_or_now do
new_value = counter(attribute).bulk_increment(increments)
- log_increment_counter(attribute, increments.sum(&:amount), new_value)
+ log_bulk_increment_counter(attribute, increments, new_value)
end
end
@@ -198,7 +198,8 @@ module CounterAttribute
message: 'Increment counter attribute',
attribute: attribute,
project_id: project_id,
- increment: increment,
+ increment: increment.amount,
+ ref: increment.ref,
new_counter_value: new_value,
current_db_value: read_attribute(attribute)
)
@@ -206,6 +207,16 @@ module CounterAttribute
Gitlab::AppLogger.info(payload)
end
+ def log_bulk_increment_counter(attribute, increments, new_value)
+ if Feature.enabled?(:split_log_bulk_increment_counter, type: :ops)
+ increments.each do |increment|
+ log_increment_counter(attribute, increment, new_value)
+ end
+ else
+ log_increment_counter(attribute, Gitlab::Counters::Increment.new(amount: increments.sum(&:amount)), new_value)
+ end
+ end
+
def log_clear_counter(attribute)
payload = Gitlab::ApplicationContext.current.merge(
message: 'Clear counter attribute',
diff --git a/app/models/snippet_user_mention.rb b/app/models/snippet_user_mention.rb
index 87ce77a5787..138feb6ab29 100644
--- a/app/models/snippet_user_mention.rb
+++ b/app/models/snippet_user_mention.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class SnippetUserMention < UserMention
+ include IgnorableColumns
+
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.0', remove_after: '2023-05-22'
+
belongs_to :snippet
belongs_to :note
end
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 51238ab9bce..0570d22529b 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -9,68 +9,13 @@
.card-new-merge-request
%h2.gl-font-size-h2
= _('Source branch')
- - if Feature.enabled?(:mr_compare_dropdowns, @project)
- #js-source-project-dropdown{ data: { current_project: { value: f.object.source_project_id.to_s, text: f.object.source_project.full_path, refsUrl: refs_project_path(f.object.source_project) }.to_json, current_branch: { value: f.object.source_branch.presence, text: f.object.source_branch.presence }.to_json } }
- - else
- .clearfix
- .merge-request-select.dropdown
- = f.hidden_field :source_project_id
- = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted?, default_text: _("Select source project") }, { toggle_class: "js-compare-dropdown js-source-project" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-source-project
- = dropdown_title(_("Select source project"))
- = dropdown_filter(_("Search projects"))
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/project',
- projects: [@merge_request.source_project],
- selected: f.object.source_project_id
- .merge-request-select.dropdown
- = f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, default_text: _("Select target branch"), qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
- .dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
- = dropdown_title(_("Select source branch"))
- = dropdown_filter(_("Search branches"))
- = dropdown_content
- = dropdown_loading
- .gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
- .compare-commit-empty.js-source-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
- = sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
- = _('Select a branch to compare')
- = gl_loading_icon(css_class: 'js-source-loading gl-py-3')
- %ul.list-unstyled.mr_source_commit
+ #js-source-project-dropdown{ data: { current_project: { value: f.object.source_project_id.to_s, text: f.object.source_project.full_path, refsUrl: refs_project_path(f.object.source_project) }.to_json, current_branch: { value: f.object.source_branch.presence, text: f.object.source_branch.presence }.to_json } }
.col-lg-6
.card-new-merge-request
%h2.gl-font-size-h2
= _('Target branch')
- - if Feature.enabled?(:mr_compare_dropdowns, @project)
- #js-target-project-dropdown{ data: { target_projects_path: project_new_merge_request_json_target_projects_path(@project), current_project: { value: f.object.target_project_id.to_s, text: f.object.target_project.full_path, refsUrl: refs_project_path(f.object.target_project) }.to_json, current_branch: { value: f.object.target_branch.presence, text: f.object.target_branch.presence }.to_json } }
- - else
- .clearfix
- .merge-request-select.dropdown
- - projects = target_projects(@project)
- = f.hidden_field :target_project_id
- = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted?, default_text: _("Select target project") }, { toggle_class: "js-compare-dropdown js-target-project" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-target-project
- = dropdown_title(_("Select target project"))
- = dropdown_filter(_("Search projects"))
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/project',
- projects: projects,
- selected: f.object.target_project_id
- .merge-request-select.dropdown
- = f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch, default_text: _("Select target branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
- .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
- = dropdown_title(_("Select target branch"))
- = dropdown_filter(_("Search branches"))
- = dropdown_content
- = dropdown_loading
- .gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
- .compare-commit-empty.js-target-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
- = sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
- = _('Select a branch to compare')
- = gl_loading_icon(css_class: 'js-target-loading gl-py-3')
- %ul.list-unstyled.mr_target_commit
+ #js-target-project-dropdown{ data: { target_projects_path: project_new_merge_request_json_target_projects_path(@project), current_project: { value: f.object.target_project_id.to_s, text: f.object.target_project.full_path, refsUrl: refs_project_path(f.object.target_project) }.to_json, current_branch: { value: f.object.target_branch.presence, text: f.object.target_branch.presence }.to_json } }
- if @merge_request.errors.any?
= form_errors(@merge_request)
diff --git a/config/feature_flags/development/github_client_fetch_repos_via_graphql.yml b/config/feature_flags/development/github_client_fetch_repos_via_graphql.yml
index 7ff87410458..2d045e8ca06 100644
--- a/config/feature_flags/development/github_client_fetch_repos_via_graphql.yml
+++ b/config/feature_flags/development/github_client_fetch_repos_via_graphql.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385649
milestone: '15.7'
type: development
group: group::import
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/mr_compare_dropdowns.yml b/config/feature_flags/development/mr_compare_dropdowns.yml
deleted file mode 100644
index bffab02389b..00000000000
--- a/config/feature_flags/development/mr_compare_dropdowns.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: mr_compare_dropdowns
-introduced_by_url:
-rollout_issue_url:
-milestone: '15.7'
-type: development
-group: group::code review
-default_enabled: false
diff --git a/config/feature_flags/ops/split_log_bulk_increment_counter.yml b/config/feature_flags/ops/split_log_bulk_increment_counter.yml
new file mode 100644
index 00000000000..ba8c3a7d22e
--- /dev/null
+++ b/config/feature_flags/ops/split_log_bulk_increment_counter.yml
@@ -0,0 +1,8 @@
+---
+name: split_log_bulk_increment_counter
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111147
+rollout_issue_url:
+milestone: '15.9'
+type: ops
+group: group::pipeline insights
+default_enabled: false
diff --git a/db/migrate/20230207005549_initialize_conversion_of_snippet_user_mentions_note_id_to_bigint.rb b/db/migrate/20230207005549_initialize_conversion_of_snippet_user_mentions_note_id_to_bigint.rb
new file mode 100644
index 00000000000..3f994a0e40f
--- /dev/null
+++ b/db/migrate/20230207005549_initialize_conversion_of_snippet_user_mentions_note_id_to_bigint.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class InitializeConversionOfSnippetUserMentionsNoteIdToBigint < Gitlab::Database::Migration[2.1]
+ TABLE = :snippet_user_mentions
+ COLUMNS = %i[note_id]
+
+ enable_lock_retries!
+
+ def up
+ initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+
+ def down
+ revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+end
diff --git a/db/post_migrate/20230207005701_backfill_snippet_user_mentions_note_id_for_bigint_conversion.rb b/db/post_migrate/20230207005701_backfill_snippet_user_mentions_note_id_for_bigint_conversion.rb
new file mode 100644
index 00000000000..4d4c36bf8fc
--- /dev/null
+++ b/db/post_migrate/20230207005701_backfill_snippet_user_mentions_note_id_for_bigint_conversion.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class BackfillSnippetUserMentionsNoteIdForBigintConversion < Gitlab::Database::Migration[2.1]
+ TABLE = :snippet_user_mentions
+ COLUMNS = %i[note_id]
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+
+ def down
+ revert_backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+end
diff --git a/db/schema_migrations/20230207005549 b/db/schema_migrations/20230207005549
new file mode 100644
index 00000000000..fc3dedf162b
--- /dev/null
+++ b/db/schema_migrations/20230207005549
@@ -0,0 +1 @@
+060067232f46ea992de5d5392c2918f81167d224b3b90f3a7567b624a3d8d4e3 \ No newline at end of file
diff --git a/db/schema_migrations/20230207005701 b/db/schema_migrations/20230207005701
new file mode 100644
index 00000000000..1ffa8f0b483
--- /dev/null
+++ b/db/schema_migrations/20230207005701
@@ -0,0 +1 @@
+1602c379715b3ca22c75fc4ff39cda49d0735db29d0be2256265fb5313ea332f \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 69bc358b06e..005df75da1f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -270,6 +270,15 @@ BEGIN
END;
$$;
+CREATE FUNCTION trigger_bfc6e47be8cc() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+ NEW."note_id_convert_to_bigint" := NEW."note_id";
+ RETURN NEW;
+END;
+$$;
+
CREATE FUNCTION trigger_c5a5f48f12b0() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -22073,7 +22082,8 @@ CREATE TABLE snippet_user_mentions (
note_id integer,
mentioned_users_ids integer[],
mentioned_projects_ids integer[],
- mentioned_groups_ids integer[]
+ mentioned_groups_ids integer[],
+ note_id_convert_to_bigint bigint
);
CREATE SEQUENCE snippet_user_mentions_id_seq
@@ -33545,6 +33555,8 @@ CREATE TRIGGER trigger_3dc62927cae8 BEFORE INSERT OR UPDATE ON design_user_menti
CREATE TRIGGER trigger_7f4fcd5aa322 BEFORE INSERT OR UPDATE ON sent_notifications FOR EACH ROW EXECUTE FUNCTION trigger_7f4fcd5aa322();
+CREATE TRIGGER trigger_bfc6e47be8cc BEFORE INSERT OR UPDATE ON snippet_user_mentions FOR EACH ROW EXECUTE FUNCTION trigger_bfc6e47be8cc();
+
CREATE TRIGGER trigger_c5a5f48f12b0 BEFORE INSERT OR UPDATE ON epic_user_mentions FOR EACH ROW EXECUTE FUNCTION trigger_c5a5f48f12b0();
CREATE TRIGGER trigger_c7107f30d69d BEFORE INSERT OR UPDATE ON merge_request_metrics FOR EACH ROW EXECUTE FUNCTION trigger_c7107f30d69d();
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 068e631f62a..804abad22a2 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -38,9 +38,28 @@ to help identify if something is wrong:
- Is the secondary site's tracking database configured?
- Is the secondary site's tracking database connected?
- Is the secondary site's tracking database up-to-date?
+- Is the secondary site's status less than 10 minutes old?
![Geo health check](img/geo_site_health_v14_0.png)
+A site shows as "Unhealthy" if the site's status is more than 10 minutes old. It that case, try running the following in the [Rails console](../../operations/rails_console.md) on the affected site:
+
+```ruby
+Geo::MetricsUpdateWorker.new.perform
+```
+
+If it raises an error, then the error is probably also preventing the jobs from completing. If it takes longer than 10 minutes, then there may be a performance issue, and the UI may always show "Unhealthy" even if the status eventually does get updated.
+
+If it successfully updates the status, then something may be wrong with Sidekiq. Is it running? Do the logs show errors? This job is supposed to be enqueued every minute. It takes an exclusive lease in Redis to ensure that only one of these jobs can run at a time. The primary site updates its status directly in the PostgreSQL database. Secondary sites send an HTTP Post request to the primary site with their status data.
+
+A site also shows as "Unhealthy" if certain health checks fail. You can reveal the failure by running the following in the [Rails console](../../operations/rails_console.md) on the affected site:
+
+```ruby
+Gitlab::Geo::HealthCheck.new.perform_checks
+```
+
+If it returns `""` (an empty string) or `"Healthy"`, then the checks succeeded. If it returns anything else, then the message should explain what failed, or show the exception message.
+
For information about how to resolve common error messages reported from the user interface,
see [Fixing Common Errors](#fixing-common-errors).
diff --git a/doc/api/group_epic_boards.md b/doc/api/group_epic_boards.md
index 93be9431874..bb3d2a8acf4 100644
--- a/doc/api/group_epic_boards.md
+++ b/doc/api/group_epic_boards.md
@@ -66,7 +66,8 @@ Example response:
"color": "#F0AD4E",
"description": null
},
- "position": 1
+ "position": 1,
+ "list_type": "label"
},
{
"id": 2,
@@ -76,7 +77,8 @@ Example response:
"color": "#FF0000",
"description": null
},
- "position": 2
+ "position": 2,
+ "list_type": "label"
},
{
"id": 3,
@@ -86,7 +88,8 @@ Example response:
"color": "#FF5F00",
"description": null
},
- "position": 3
+ "position": 3,
+ "list_type": "label"
}
]
}
@@ -144,7 +147,8 @@ Example response:
"color" : "#F0AD4E",
"description" : null
},
- "position" : 1
+ "position" : 1,
+ "list_type": "label"
},
{
"id" : 2,
@@ -154,7 +158,8 @@ Example response:
"color" : "#FF0000",
"description" : null
},
- "position" : 2
+ "position" : 2,
+ "list_type": "label"
},
{
"id" : 3,
@@ -164,8 +169,101 @@ Example response:
"color" : "#FF5F00",
"description" : null
},
- "position" : 3
+ "position" : 3,
+ "list_type": "label"
}
]
}
```
+
+## List group epic board lists
+
+Gets a list of the epic board's lists.
+Does not include `open` and `closed` lists.
+
+```plaintext
+GET /groups/:id/epic_boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) accessible by the authenticated user |
+| `board_id` | integer | yes | The ID of an epic board |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epic_boards/1/lists"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1,
+ "list_type" : "label",
+ "collapsed" : false
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2,
+ "list_type" : "label",
+ "collapsed" : false
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3,
+ "list_type" : "label",
+ "collapsed" : false
+ }
+]
+```
+
+## Single group epic board list
+
+Gets a single board list.
+
+```plaintext
+GET /groups/:id/epic_boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) accessible by the authenticated user |
+| `board_id` | integer | yes | The ID of an epic board |
+| `list_id` | integer | yes | The ID of an epic board's list |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epic_boards/1/lists/1"
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1,
+ "list_type" : "label",
+ "collapsed" : false
+}
+```
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index f9c6d3ce8d0..3121b0658e7 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -429,7 +429,7 @@ Supported attributes:
| `config_file` | string | no | The path of changelog configuration file in the project's Git repository, defaults to `.gitlab/changelog_config.yml`. |
| `date` | datetime | no | The date and time of the release, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z`. Defaults to the current time. |
| `from` | string | no | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. |
-| `to` | string | no | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. Defaults to the branch specified in the `branch` attribute. |
+| `to` | string | no | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. Defaults to the HEAD of the default project branch. |
| `trailer` | string | no | The Git trailer to use for including commits, defaults to `Changelog`. |
```shell
diff --git a/doc/api/runners.md b/doc/api/runners.md
index f791d727a51..e9c389119c4 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -6,7 +6,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Runners API **(FREE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/2640) in GitLab 8.5.
+[Pagination](rest/index.md#pagination) is available on the following API endpoints (they return 20 items by default):
+
+```plaintext
+GET /runners
+GET /runners/all
+GET /runners/:id/jobs
+GET /projects/:id/runners
+GET /groups/:id/runners
+```
## Registration and authentication tokens
diff --git a/doc/architecture/blueprints/ci_pipeline_components/index.md b/doc/architecture/blueprints/ci_pipeline_components/index.md
index 29709cd98d8..1bb459cf033 100644
--- a/doc/architecture/blueprints/ci_pipeline_components/index.md
+++ b/doc/architecture/blueprints/ci_pipeline_components/index.md
@@ -1,5 +1,5 @@
---
-status: proposed
+status: ongoing
creation-date: "2022-09-14"
authors: [ "@ayufan", "@fabiopitino", "@grzesiek" ]
coach: [ "@ayufan", "@grzesiek" ]
@@ -8,20 +8,22 @@ owning-stage: "~devops::verify"
participating-stages: []
---
-# CI/CD pipeline components catalog
+# CI/CD Catalog
## Summary
## Goals
-The goal of the CI/CD pipeline components catalog is to make the reusing pipeline configurations
-easier and more efficient.
-Providing a way to discover, understand and learn how to reuse pipeline constructs allows for a more streamlined experience.
-Having a CI/CD pipeline components catalog also sets a framework for users to collaborate on pipeline constructs so that they can be evolved
-and improved over time.
+The goal of the CI/CD pipeline components catalog is to make the reusing
+pipeline configurations easier and more efficient. Providing a way to
+discover, understand and learn how to reuse pipeline constructs allows for a
+more streamlined experience. Having a CI/CD pipeline components catalog also
+sets a framework for users to collaborate on pipeline constructs so that they
+can be evolved and improved over time.
-This blueprint defines the architectural guidelines on how to build a CI/CD catalog of pipeline components.
-This blueprint also defines the long-term direction for iterations and improvements to the solution.
+This blueprint defines the architectural guidelines on how to build a CI/CD
+catalog of pipeline components. This blueprint also defines the long-term
+direction for iterations and improvements to the solution.
## Challenges
@@ -393,6 +395,32 @@ scan-website:
With `$[[ inputs.XXX ]]` inputs are interpolated immediately after parsing the content.
+### CI configuration interpolation perspectives and limitations
+
+With `spec:` users will be able to define input arguments for CI configuration.
+With `with:` keywords, they will pass these arguments to CI components.
+
+`inputs` in `$[[ inputs.something ]]` is going to be an initial "object" or
+"container" that we will provide, to allow users to access their arguments in
+the interpolation block. This, however, can evolve into myriads of directions, for example:
+
+1. We could provide `variables` or `env` object, for users to access their environment variables easier.
+1. We can extend the block evaluation to easier navigate JSON or YAML objects passed from elsewhere.
+1. We can provide access to the repository files, snippets or issues from there too.
+
+The CI configuration interpolation is a relative compute-intensive technology,
+especially because we foresee this mechanism being used frequently on
+GitLab.com. In order to ensure that users are using this responsibly, we have
+introduced various limits, required to keep our production system safe. The
+limits should not impact users, because there are application limits available
+on a different level (maximum YAML size supported, timeout on parsing YAML
+files etc); the interpolation limits we've introduced are typically much higher
+then these. Some of them are:
+
+1. An interpolation block should not be larger than 1 kilobyte.
+1. A YAML value with interpolation in it can't be larger than 1 megabyte.
+1. YAML configuration can't consist of more than half million entries.
+
### Why input parameters and not environment variables?
Until today we have been leveraging environment variables to pass information around.
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index a61f41dcd91..25f875ccdfd 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -269,6 +269,7 @@ To filter:
FLAG:
On self-managed GitLab, by default this feature is not available.
To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `or_issuable_queries`.
+On GitLab.com, this feature is not available.
The feature is not ready for production use.
When this feature is enabled, you can use the OR operator (**is one of: `||`**)
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 194b501d1fb..55689b1b714 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -129,6 +129,34 @@ threads. Some quick actions might not be available to all subscription tiers.
| `/weight <value>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set weight. Valid options for `<value>` include `0`, `1`, `2`, and so on. |
| `/zoom <Zoom URL>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add a Zoom meeting to this issue or incident. In [GitLab 15.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/230853) users on GitLab Premium can add a short description when [adding a Zoom link to an incident](../../operations/incident_management/linked_resources.md#link-zoom-meetings-from-an-incident).|
+## Work items
+
+The following quick actions can be applied through the description field when editing work items.
+
+| Command | Task | Objective | Key Result | Action |
+|:-------------------------------------------------------------------------------------------------|:-----------------------|:-----------------------|:-----------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `/title <new title>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Change title. |
+| `/close` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Close. |
+| `/reopen` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Reopen. |
+| `/shrug <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `¯\_(ツ)_/¯`. |
+| `/tableflip <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `(╯°□°)╯︵ ┻━┻`. |
+| `/cc @user` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Mention a user. In GitLab 15.0 and later, this command performs no action. You can instead type `CC @user` or only `@user`. [In GitLab 14.9 and earlier](https://gitlab.com/gitlab-org/gitlab/-/issues/31200), mentioning a user at the start of a line creates a specific type of to-do item notification. |
+| `/assign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** Yes | Assign one or more users. |
+| `/assign me` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** Yes | Assign yourself. |
+| `/unassign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** Yes | Remove specific assignees. |
+| `/unassign` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** Yes | Remove all assignees. |
+| `/reassign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** Yes | Replace current assignees with those specified. |
+| `/label ~label1 ~label2` or `/labels ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Add one or more labels. Label names can also start without a tilde (`~`), but mixed syntax is not supported. |
+| `/relabel ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Replace current labels with those specified. |
+| `/unlabel ~label1 ~label2` or `/remove_label ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Remove specified labels. |
+| `/unlabel` or `/remove_label` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Remove all labels. |
+| `/due <date>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** Yes | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. |
+| `/remove_due_date` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** Yes | Remove due date. |
+| `/health_status <value>` | **{check-circle}** Yes | **{dotted-circle}** Yes | **{dotted-circle}** Yes | Set [health status](issues/managing_issues.md#health-status). Valid options for `<value>` are `on_track`, `needs_attention`, and `at_risk`. |
+| `/clear_health_status` | **{check-circle}** Yes | **{dotted-circle}** Yes | **{dotted-circle}** Yes | Clear [health status](issues/managing_issues.md#health-status). |
+| `/weight <value>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set weight. Valid options for `<value>` include `0`, `1`, and `2`. |
+| `/clear_weight` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Clear weight. |
+
## Commit messages
The following quick actions are applicable for commit messages:
diff --git a/doc/user/ssh.md b/doc/user/ssh.md
index 003bc5c91c1..2be126e4452 100644
--- a/doc/user/ssh.md
+++ b/doc/user/ssh.md
@@ -22,9 +22,11 @@ SSH uses two keys, a public key and a private key.
When you need to copy or upload your SSH public key, make sure you do not accidentally copy or upload your private key instead.
-You cannot expose data by uploading your public key. For example, you can use your public key to
-[sign commits](project/repository/ssh_signed_commits/index.md),
+You cannot expose data by uploading your public key. When you need to copy or upload your SSH public key, make sure you do not accidentally copy or upload your private key instead.
+
+You can use your private key to [sign commits](project/repository/ssh_signed_commits/index.md),
which makes your use of GitLab and your data even more secure.
+This signature then can be verified by anyone using your public key.
For details, see [Asymmetric cryptography, also known as public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography).
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 78d10e5b54d..84c3d7ce4c2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16175,9 +16175,6 @@ msgstr ""
msgid "Error fetching payload data."
msgstr ""
-msgid "Error fetching refs"
-msgstr ""
-
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
index dc2f908a906..ffd40fabf05 100644
--- a/qa/qa/page/merge_request/new.rb
+++ b/qa/qa/page/merge_request/new.rb
@@ -10,6 +10,9 @@ module QA
view 'app/views/projects/merge_requests/creations/_new_compare.html.haml' do
element :compare_branches_button
+ end
+
+ view 'app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js' do
element :source_branch_dropdown
end
diff --git a/spec/frontend/gl_form_spec.js b/spec/frontend/gl_form_spec.js
index ab5627ce216..6ad9d9f4338 100644
--- a/spec/frontend/gl_form_spec.js
+++ b/spec/frontend/gl_form_spec.js
@@ -6,6 +6,47 @@ import '~/lib/utils/common_utils';
describe('GLForm', () => {
const testContext = {};
+ const mockGl = {
+ GfmAutoComplete: {
+ dataSources: {
+ commands: '/group/projects/-/autocomplete_sources/commands',
+ },
+ },
+ };
+
+ describe('Setting up GfmAutoComplete', () => {
+ describe('setupForm', () => {
+ let setupFormSpy;
+
+ beforeEach(() => {
+ setupFormSpy = jest.spyOn(GLForm.prototype, 'setupForm');
+
+ testContext.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
+ testContext.textarea = testContext.form.find('textarea');
+ });
+
+ it('should be called with the global data source `windows.gl`', () => {
+ window.gl = { ...mockGl };
+ testContext.glForm = new GLForm(testContext.form, {}, false);
+
+ expect(setupFormSpy).toHaveBeenCalledTimes(1);
+ expect(setupFormSpy).toHaveBeenCalledWith(window.gl.GfmAutoComplete.dataSources, false);
+ });
+
+ it('should be called with the provided custom data source', () => {
+ window.gl = { ...mockGl };
+
+ const customDataSources = {
+ foobar: '/group/projects/-/autocomplete_sources/foobar',
+ };
+
+ testContext.glForm = new GLForm(testContext.form, {}, false, customDataSources);
+
+ expect(setupFormSpy).toHaveBeenCalledTimes(1);
+ expect(setupFormSpy).toHaveBeenCalledWith(customDataSources, false);
+ });
+ });
+ });
describe('when instantiated', () => {
beforeEach(() => {
diff --git a/spec/frontend/lib/utils/scroll_utils_spec.js b/spec/frontend/lib/utils/scroll_utils_spec.js
new file mode 100644
index 00000000000..d42e25b929c
--- /dev/null
+++ b/spec/frontend/lib/utils/scroll_utils_spec.js
@@ -0,0 +1,21 @@
+import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
+
+describe('isScrolledToBottom', () => {
+ const setScrollGetters = (getters) => {
+ Object.entries(getters).forEach(([name, value]) => {
+ jest.spyOn(Element.prototype, name, 'get').mockReturnValue(value);
+ });
+ };
+
+ it.each`
+ context | scrollTop | scrollHeight | result
+ ${'returns false when not scrolled to bottom'} | ${0} | ${2000} | ${false}
+ ${'returns true when scrolled to bottom'} | ${1000} | ${2000} | ${true}
+ ${'returns true when scrolled to bottom with subpixel precision'} | ${999.25} | ${2000} | ${true}
+ ${'returns true when cannot scroll'} | ${0} | ${500} | ${true}
+ `('$context', ({ scrollTop, scrollHeight, result }) => {
+ setScrollGetters({ scrollTop, clientHeight: 1000, scrollHeight });
+
+ expect(isScrolledToBottom()).toBe(result);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 3dc52730bb4..68ce07f86b9 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,3 +1,4 @@
+import $ from 'jquery';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
@@ -6,7 +7,7 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue';
import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -75,6 +76,22 @@ describe('Markdown field component', () => {
);
}
+ function createWrapper({ autocompleteDataSources = {} } = {}) {
+ subject = shallowMountExtended(MarkdownField, {
+ propsData: {
+ markdownDocsPath,
+ markdownPreviewPath,
+ isSubmitting: false,
+ textareaValue,
+ lines: [],
+ enablePreview: true,
+ restrictedToolBarItems,
+ showContentEditorSwitcher: false,
+ autocompleteDataSources,
+ },
+ });
+ }
+
const getPreviewLink = () => subject.findByTestId('preview-tab');
const getWriteLink = () => subject.findByTestId('write-tab');
const getMarkdownButton = () => subject.find('.js-md');
@@ -85,6 +102,7 @@ describe('Markdown field component', () => {
const findDropzone = () => subject.find('.div-dropzone');
const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader);
const findMarkdownToolbar = () => subject.findComponent(MarkdownToolbar);
+ const findGlForm = () => $(subject.vm.$refs['gl-form']).data('glForm');
describe('mounted', () => {
const previewHTML = `
@@ -101,6 +119,18 @@ describe('Markdown field component', () => {
findDropzone().element.addEventListener('click', dropzoneSpy);
});
+ describe('GlForm', () => {
+ beforeEach(() => {
+ createWrapper({ autocompleteDataSources: { commands: '/foobar/-/autocomplete_sources' } });
+ });
+
+ it('initializes GlForm with autocomplete data sources', () => {
+ expect(findGlForm().autoComplete.dataSources).toMatchObject({
+ commands: '/foobar/-/autocomplete_sources',
+ });
+ });
+ });
+
it('renders textarea inside backdrop', () => {
expect(subject.find('.zen-backdrop textarea').element).not.toBeNull();
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 12eda284aea..26b536984ff 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -24,6 +24,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const formFieldName = 'form[markdown_field]';
const formFieldPlaceholder = 'Write some markdown';
const formFieldAriaLabel = 'Edit your content';
+ const autocompleteDataSources = { commands: '/foobar/-/autcomplete_sources' };
let mock;
const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
@@ -35,6 +36,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
markdownDocsPath,
quickActionsDocsPath,
enableAutocomplete,
+ autocompleteDataSources,
enablePreview,
formFieldProps: {
id: formFieldId,
@@ -68,18 +70,17 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('displays markdown field by default', () => {
buildWrapper({ propsData: { supportsQuickActions: true } });
- expect(findMarkdownField().props()).toEqual(
- expect.objectContaining({
- markdownPreviewPath: renderMarkdownPath,
- quickActionsDocsPath,
- canAttachFile: true,
- enableAutocomplete,
- textareaValue: value,
- markdownDocsPath,
- uploadsPath: window.uploads_path,
- enablePreview,
- }),
- );
+ expect(findMarkdownField().props()).toMatchObject({
+ autocompleteDataSources,
+ markdownPreviewPath: renderMarkdownPath,
+ quickActionsDocsPath,
+ canAttachFile: true,
+ enableAutocomplete,
+ textareaValue: value,
+ markdownDocsPath,
+ uploadsPath: window.uploads_path,
+ enablePreview,
+ });
});
it('renders markdown field textarea', () => {
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 05476ef5ca0..a12ec23c15a 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -16,6 +16,7 @@ import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
updateWorkItemMutationResponse,
workItemDescriptionSubscriptionResponse,
@@ -102,6 +103,49 @@ describe('WorkItemDescription', () => {
wrapper.destroy();
});
+ describe('editing description with workItemsMvc FF enabled', () => {
+ beforeEach(() => {
+ workItemsMvc = true;
+ });
+
+ it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
+ const {
+ iid,
+ project: { fullPath },
+ } = workItemQueryResponse.data.workItem;
+
+ await createComponent({ isEditing: true });
+
+ expect(findMarkdownEditor().props()).toMatchObject({
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
+ supportsQuickActions: true,
+ renderMarkdownPath: markdownPreviewPath(fullPath, iid),
+ quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ });
+ });
+ });
+
+ describe('editing description with workItemsMvc FF disabled', () => {
+ beforeEach(() => {
+ workItemsMvc = false;
+ });
+
+ it('passes correct autocompletion data and preview markdown sources', async () => {
+ const {
+ iid,
+ project: { fullPath },
+ } = workItemQueryResponse.data.workItem;
+
+ await createComponent({ isEditing: true });
+
+ expect(findMarkdownField().props()).toMatchObject({
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
+ markdownPreviewPath: markdownPreviewPath(fullPath, iid),
+ quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ });
+ });
+ });
+
describe.each([true, false])(
'editing description with workItemsMvc %workItemsMvcEnabled',
(workItemsMvcEnabled) => {
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
new file mode 100644
index 00000000000..aa24b80cf08
--- /dev/null
+++ b/spec/frontend/work_items/utils_spec.js
@@ -0,0 +1,27 @@
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
+
+describe('autocompleteDataSources', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('returns corrrect data sources', () => {
+ expect(autocompleteDataSources('project/group', '2')).toMatchObject({
+ commands: '/foobar/project/group/-/autocomplete_sources/commands?type=WorkItem&type_id=2',
+ labels: '/foobar/project/group/-/autocomplete_sources/labels?type=WorkItem&type_id=2',
+ members: '/foobar/project/group/-/autocomplete_sources/members?type=WorkItem&type_id=2',
+ });
+ });
+});
+
+describe('markdownPreviewPath', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('returns corrrect data sources', () => {
+ expect(markdownPreviewPath('project/group', '2')).toEqual(
+ '/foobar/project/group/preview_markdown?target_type=WorkItem&target_id=2',
+ );
+ });
+});
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index fe36b1006b0..14758f9690f 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -76,6 +76,22 @@ RSpec.shared_examples 'work items description' do
end
end
+ it 'autocompletes available quick actions', :aggregate_failures do
+ click_button "Edit description"
+
+ find('[aria-label="Description"]').send_keys("/")
+
+ wait_for_requests
+
+ page.within('.atwho-container') do
+ expect(page).to have_text("title")
+ expect(page).to have_text("shrug")
+ expect(page).to have_text("tableflip")
+ expect(page).to have_text("close")
+ expect(page).to have_text("cc")
+ end
+ end
+
context 'on conflict' do
let_it_be(:other_user) { create(:user) }
let(:expected_warning) { 'Someone edited the description at the same time you did.' }
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
index f98be12523d..5755b9a56b1 100644
--- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
describe attribute do
describe '#increment_counter', :redis do
let(:amount) { 10 }
- let(:increment) { Gitlab::Counters::Increment.new(amount: amount) }
+ let(:increment) { Gitlab::Counters::Increment.new(amount: amount, ref: 3) }
let(:counter_key) { model.counter(attribute).key }
subject { model.increment_counter(attribute, increment) }
@@ -31,6 +31,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
attribute: attribute,
project_id: model.project_id,
increment: amount,
+ ref: increment.ref,
new_counter_value: 0 + amount,
current_db_value: model.read_attribute(attribute),
'correlation_id' => an_instance_of(String),
@@ -74,27 +75,36 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
describe '#bulk_increment_counter', :redis do
- let(:increments) { [Gitlab::Counters::Increment.new(amount: 10), Gitlab::Counters::Increment.new(amount: 5)] }
+ let(:increments) do
+ [
+ Gitlab::Counters::Increment.new(amount: 10, ref: 1),
+ Gitlab::Counters::Increment.new(amount: 5, ref: 2)
+ ]
+ end
+
let(:total_amount) { increments.sum(&:amount) }
let(:counter_key) { model.counter(attribute).key }
subject { model.bulk_increment_counter(attribute, increments) }
context 'when attribute is a counter attribute' do
- it 'increments the counter in Redis and logs it' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- hash_including(
- message: 'Increment counter attribute',
- attribute: attribute,
- project_id: model.project_id,
- increment: total_amount,
- new_counter_value: 0 + total_amount,
- current_db_value: model.read_attribute(attribute),
- 'correlation_id' => an_instance_of(String),
- 'meta.feature_category' => 'test',
- 'meta.caller_id' => 'caller'
+ it 'increments the counter in Redis and logs each increment' do
+ increments.each do |increment|
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ hash_including(
+ message: 'Increment counter attribute',
+ attribute: attribute,
+ project_id: model.project_id,
+ increment: increment.amount,
+ ref: increment.ref,
+ new_counter_value: 0 + total_amount,
+ current_db_value: model.read_attribute(attribute),
+ 'correlation_id' => an_instance_of(String),
+ 'meta.feature_category' => 'test',
+ 'meta.caller_id' => 'caller'
+ )
)
- )
+ end
subject
@@ -104,6 +114,30 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
end
+ context 'when feature flag split_log_bulk_increment_counter is disabled' do
+ before do
+ stub_feature_flags(split_log_bulk_increment_counter: false)
+ end
+
+ it 'logs a single total increment' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ hash_including(
+ message: 'Increment counter attribute',
+ attribute: attribute,
+ project_id: model.project_id,
+ increment: increments.sum(&:amount),
+ new_counter_value: 0 + total_amount,
+ current_db_value: model.read_attribute(attribute),
+ 'correlation_id' => an_instance_of(String),
+ 'meta.feature_category' => 'test',
+ 'meta.caller_id' => 'caller'
+ )
+ )
+
+ subject
+ end
+ end
+
it 'does not increment the counter for the record' do
expect { subject }.not_to change { model.reset.read_attribute(attribute) }
end