Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-12-07 18:15:03 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-12-07 18:15:03 +0300
commit3a0f6ebaa9a80e34c09f599a624f6e4520009ef1 (patch)
tree4560fa6bcba7d950595e906ee3286dd1e44642f1 /app/assets/javascripts/issuable
parent6dd9e3644eea1a5c605a6a623cae1d53b156b9e5 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/issuable')
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue58
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/constants.js17
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/init_issue_status_select.js17
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js126
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js173
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js19
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js28
-rw-r--r--app/assets/javascripts/issuable/init_issuable_sidebar.js16
-rw-r--r--app/assets/javascripts/issuable/issuable_context.js69
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js219
-rw-r--r--app/assets/javascripts/issuable/issuable_index.js7
11 files changed, 749 insertions, 0 deletions
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue
new file mode 100644
index 00000000000..9509399e91d
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { ISSUE_STATUS_SELECT_OPTIONS } from '../constants';
+
+export default {
+ name: 'StatusSelect',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ data() {
+ return {
+ status: null,
+ };
+ },
+ computed: {
+ dropdownText() {
+ return this.status?.text ?? this.$options.i18n.defaultDropdownText;
+ },
+ selectedValue() {
+ return this.status?.value;
+ },
+ },
+ methods: {
+ onDropdownItemClick(statusOption) {
+ // clear status if the currently checked status is clicked again
+ if (this.status?.value === statusOption.value) {
+ this.status = null;
+ } else {
+ this.status = statusOption;
+ }
+ },
+ },
+ i18n: {
+ dropdownTitle: __('Change status'),
+ defaultDropdownText: __('Select status'),
+ },
+ ISSUE_STATUS_SELECT_OPTIONS,
+};
+</script>
+<template>
+ <div>
+ <input type="hidden" name="update[state_event]" :value="selectedValue" />
+ <gl-dropdown :text="dropdownText" :title="$options.i18n.dropdownTitle" class="gl-w-full">
+ <gl-dropdown-item
+ v-for="statusOption in $options.ISSUE_STATUS_SELECT_OPTIONS"
+ :key="statusOption.value"
+ :is-checked="selectedValue === statusOption.value"
+ is-check-item
+ :title="statusOption.text"
+ @click="onDropdownItemClick(statusOption)"
+ >
+ {{ statusOption.text }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
new file mode 100644
index 00000000000..ad15b25f9cf
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
@@ -0,0 +1,17 @@
+import { __ } from '~/locale';
+
+export const ISSUE_STATUS_MODIFIERS = {
+ REOPEN: 'reopen',
+ CLOSE: 'close',
+};
+
+export const ISSUE_STATUS_SELECT_OPTIONS = [
+ {
+ value: ISSUE_STATUS_MODIFIERS.REOPEN,
+ text: __('Open'),
+ },
+ {
+ value: ISSUE_STATUS_MODIFIERS.CLOSE,
+ text: __('Closed'),
+ },
+];
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/init_issue_status_select.js b/app/assets/javascripts/issuable/bulk_update_sidebar/init_issue_status_select.js
new file mode 100644
index 00000000000..43179a86d70
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/init_issue_status_select.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import StatusSelect from './components/status_select.vue';
+
+export default function initIssueStatusSelect() {
+ const el = document.querySelector('.js-issue-status');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(StatusSelect);
+ },
+ });
+}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js
new file mode 100644
index 00000000000..463e0e5837e
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js
@@ -0,0 +1,126 @@
+import $ from 'jquery';
+import { difference, intersection, union } from 'lodash';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+
+export default {
+ init({ form, issues, prefixId } = {}) {
+ this.prefixId = prefixId || 'issue_';
+ this.form = form || this.getElement('.bulk-update');
+ this.$labelDropdown = this.form.find('.js-label-select');
+ this.issues = issues || this.getElement('.issues-list .issue');
+ this.willUpdateLabels = false;
+ this.bindEvents();
+ },
+
+ bindEvents() {
+ // eslint-disable-next-line @gitlab/no-global-event-off
+ return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
+ },
+
+ onFormSubmit(e) {
+ e.preventDefault();
+ return this.submit();
+ },
+
+ submit() {
+ axios[this.form.attr('method')](this.form.attr('action'), this.getFormDataAsObject())
+ .then(() => window.location.reload())
+ .catch(() => this.onFormSubmitFailure());
+ },
+
+ onFormSubmitFailure() {
+ this.form.find('[type="submit"]').enable();
+ return createFlash({
+ message: __('Issue update failed'),
+ });
+ },
+
+ /**
+ * Simple form serialization, it will return just what we need
+ * Returns key/value pairs from form data
+ */
+
+ getFormDataAsObject() {
+ const formData = {
+ update: {
+ state_event: this.form.find('input[name="update[state_event]"]').val(),
+ assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
+ milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
+ issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
+ subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
+ health_status: this.form.find('input[name="update[health_status]"]').val(),
+ epic_id: this.form.find('input[name="update[epic_id]"]').val(),
+ sprint_id: this.form.find('input[name="update[iteration_id]"]').val(),
+ add_label_ids: [],
+ remove_label_ids: [],
+ },
+ };
+ if (this.willUpdateLabels) {
+ formData.update.add_label_ids = this.$labelDropdown.data('user-checked');
+ formData.update.remove_label_ids = this.$labelDropdown.data('user-unchecked');
+ }
+ return formData;
+ },
+
+ setOriginalDropdownData() {
+ const $labelSelect = $('.bulk-update .js-label-select');
+ const userCheckedIds = $labelSelect.data('user-checked') || [];
+ const userUncheckedIds = $labelSelect.data('user-unchecked') || [];
+
+ // Common labels plus user checked labels minus user unchecked labels
+ const checkedIdsToShow = difference(
+ union(this.getOriginalCommonIds(), userCheckedIds),
+ userUncheckedIds,
+ );
+
+ // Indeterminate labels minus user checked labels minus user unchecked labels
+ const indeterminateIdsToShow = difference(
+ this.getOriginalIndeterminateIds(),
+ userCheckedIds,
+ userUncheckedIds,
+ );
+
+ $labelSelect.data('marked', checkedIdsToShow);
+ $labelSelect.data('indeterminate', indeterminateIdsToShow);
+ },
+
+ // From issuable's initial bulk selection
+ getOriginalCommonIds() {
+ const labelIds = [];
+ this.getElement('.issuable-list input[type="checkbox"]:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
+ });
+ return intersection.apply(this, labelIds);
+ },
+
+ // From issuable's initial bulk selection
+ getOriginalIndeterminateIds() {
+ const uniqueIds = [];
+ const labelIds = [];
+ let issuableLabels = [];
+
+ // Collect unique label IDs for all checked issues
+ this.getElement('.issuable-list input[type="checkbox"]:checked').each((i, el) => {
+ issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
+ issuableLabels.forEach((labelId) => {
+ // Store unique IDs
+ if (uniqueIds.indexOf(labelId) === -1) {
+ uniqueIds.push(labelId);
+ }
+ });
+ // Store array of IDs per issuable
+ labelIds.push(issuableLabels);
+ });
+ // Add uniqueIds to add it as argument for _.intersection
+ labelIds.unshift(uniqueIds);
+ // Return IDs that are present but not in all selected issueables
+ return uniqueIds.filter((x) => !intersection.apply(this, labelIds).includes(x));
+ },
+
+ getElement(selector) {
+ this.scopeEl = this.scopeEl || $('.content');
+ return this.scopeEl.find(selector);
+ },
+};
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
new file mode 100644
index 00000000000..8f94f54dc78
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
@@ -0,0 +1,173 @@
+/* eslint-disable class-methods-use-this, no-new */
+
+import $ from 'jquery';
+import { property } from 'lodash';
+
+import issueableEventHub from '~/issues_list/eventhub';
+import LabelsSelect from '~/labels/labels_select';
+import MilestoneSelect from '~/milestones/milestone_select';
+import initIssueStatusSelect from './init_issue_status_select';
+import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
+import subscriptionSelect from './subscription_select';
+
+const HIDDEN_CLASS = 'hidden';
+const DISABLED_CONTENT_CLASS = 'disabled-content';
+const SIDEBAR_EXPANDED_CLASS = 'right-sidebar-expanded issuable-bulk-update-sidebar';
+const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-sidebar';
+
+export default class IssuableBulkUpdateSidebar {
+ constructor() {
+ this.vueIssuablesListFeature = property(['gon', 'features', 'vueIssuablesList'])(window);
+
+ this.initDomElements();
+ this.bindEvents();
+ this.initDropdowns();
+ this.setupBulkUpdateActions();
+ }
+
+ initDomElements() {
+ this.$page = $('.layout-page');
+ this.$sidebar = $('.right-sidebar');
+ this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar');
+ this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
+ this.$bulkEditSubmitBtn = $('.js-update-selected-issues');
+ this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
+ this.$otherFilters = $('.issues-other-filters');
+ this.$checkAllContainer = $('.check-all-holder');
+ this.$issueChecks = $('.issue-check');
+ this.$issuesList = $('.issuable-list input[type="checkbox"]');
+ this.$issuableIdsInput = $('#update_issuable_ids');
+ }
+
+ bindEvents() {
+ this.$bulkUpdateEnableBtn.on('click', (e) => this.toggleBulkEdit(e, true));
+ this.$bulkEditCancelBtn.on('click', (e) => this.toggleBulkEdit(e, false));
+ this.$checkAllContainer.on('click', (e) => this.selectAll(e));
+ this.$issuesList.on('change', () => this.updateFormState());
+ this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
+ this.$checkAllContainer.on('click', () => this.updateFormState());
+
+ // The event hub connects this bulk update logic with `issues_list_app.vue`.
+ // We can remove it once we've refactored the issues list page bulk edit sidebar to Vue.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/325874
+ issueableEventHub.$on('issuables:enableBulkEdit', () => this.toggleBulkEdit(null, true));
+ issueableEventHub.$on('issuables:updateBulkEdit', () => this.updateFormState());
+ }
+
+ initDropdowns() {
+ new LabelsSelect();
+ new MilestoneSelect();
+ initIssueStatusSelect();
+ subscriptionSelect();
+
+ if (IS_EE) {
+ import('ee/vue_shared/components/sidebar/health_status_select/health_status_bundle')
+ .then(({ default: HealthStatusSelect }) => {
+ HealthStatusSelect();
+ })
+ .catch(() => {});
+ }
+
+ if (IS_EE) {
+ import('ee/vue_shared/components/sidebar/epics_select/epics_select_bundle')
+ .then(({ default: EpicSelect }) => {
+ EpicSelect();
+ })
+ .catch(() => {});
+ }
+
+ if (IS_EE) {
+ import('ee/vue_shared/components/sidebar/iterations_dropdown_bundle')
+ .then(({ default: iterationsDropdown }) => {
+ iterationsDropdown();
+ })
+ .catch((e) => {
+ throw e;
+ });
+ }
+ }
+
+ setupBulkUpdateActions() {
+ IssuableBulkUpdateActions.setOriginalDropdownData();
+ }
+
+ updateFormState() {
+ const noCheckedIssues = !$('.issuable-list input[type="checkbox"]:checked').length;
+
+ this.toggleSubmitButtonDisabled(noCheckedIssues);
+ this.updateSelectedIssuableIds();
+
+ IssuableBulkUpdateActions.setOriginalDropdownData();
+ }
+
+ prepForSubmit() {
+ // if submit button is disabled, submission is blocked. This ensures we disable after
+ // form submission is carried out
+ setTimeout(() => this.$bulkEditSubmitBtn.disable());
+ this.updateSelectedIssuableIds();
+ }
+
+ toggleBulkEdit(e, enable) {
+ e?.preventDefault();
+
+ issueableEventHub.$emit('issuables:toggleBulkEdit', enable);
+
+ this.toggleSidebarDisplay(enable);
+ this.toggleBulkEditButtonDisabled(enable);
+ this.toggleOtherFiltersDisabled(enable);
+ this.toggleCheckboxDisplay(enable);
+ }
+
+ updateSelectedIssuableIds() {
+ this.$issuableIdsInput.val(IssuableBulkUpdateSidebar.getCheckedIssueIds());
+ }
+
+ selectAll() {
+ const checkAllButtonState = this.$checkAllContainer.find('input').prop('checked');
+
+ this.$issuesList.prop('checked', checkAllButtonState);
+ }
+
+ toggleSidebarDisplay(show) {
+ this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
+ this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
+ this.$sidebarInnerContainer.toggleClass(HIDDEN_CLASS, !show);
+ this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
+ this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
+ }
+
+ toggleBulkEditButtonDisabled(disable) {
+ if (disable) {
+ this.$bulkUpdateEnableBtn.disable();
+ } else {
+ this.$bulkUpdateEnableBtn.enable();
+ }
+ }
+
+ toggleCheckboxDisplay(show) {
+ this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show || this.vueIssuablesListFeature);
+ this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
+ }
+
+ toggleOtherFiltersDisabled(disable) {
+ this.$otherFilters.toggleClass(DISABLED_CONTENT_CLASS, disable);
+ }
+
+ toggleSubmitButtonDisabled(disable) {
+ if (disable) {
+ this.$bulkEditSubmitBtn.disable();
+ } else {
+ this.$bulkEditSubmitBtn.enable();
+ }
+ }
+
+ static getCheckedIssueIds() {
+ const $checkedIssues = $('.issuable-list input[type="checkbox"]:checked');
+
+ if ($checkedIssues.length > 0) {
+ return $.map($checkedIssues, (value) => $(value).data('id'));
+ }
+
+ return [];
+ }
+}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
new file mode 100644
index 00000000000..179c2b83c6c
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
@@ -0,0 +1,19 @@
+import issuableBulkUpdateActions from './issuable_bulk_update_actions';
+import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
+
+export default {
+ bulkUpdateSidebar: null,
+
+ init(prefixId) {
+ const bulkUpdateEl = document.querySelector('.issues-bulk-update');
+ const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
+
+ if (bulkUpdateEl && !alreadyInitialized) {
+ issuableBulkUpdateActions.init({ prefixId });
+
+ this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
+ }
+
+ return this.bulkUpdateSidebar;
+ },
+};
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js b/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js
new file mode 100644
index 00000000000..b12ac776b4f
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js
@@ -0,0 +1,28 @@
+import $ from 'jquery';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from '~/locale';
+
+export default function subscriptionSelect() {
+ $('.js-subscription-event').each((i, element) => {
+ const fieldName = $(element).data('fieldName');
+
+ return initDeprecatedJQueryDropdown($(element), {
+ selectable: true,
+ fieldName,
+ toggleLabel(selected, el, instance) {
+ let label = __('Subscription');
+ const $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
+ }
+ return label;
+ },
+ clicked(options) {
+ return options.e.preventDefault();
+ },
+ id(obj, el) {
+ return $(el).data('id');
+ },
+ });
+ });
+}
diff --git a/app/assets/javascripts/issuable/init_issuable_sidebar.js b/app/assets/javascripts/issuable/init_issuable_sidebar.js
new file mode 100644
index 00000000000..ec255439458
--- /dev/null
+++ b/app/assets/javascripts/issuable/init_issuable_sidebar.js
@@ -0,0 +1,16 @@
+/* eslint-disable no-new */
+
+import { getSidebarOptions } from '~/sidebar/mount_sidebar';
+import IssuableContext from '~/issuable/issuable_context';
+import Sidebar from '~/right_sidebar';
+
+export default () => {
+ const sidebarOptEl = document.querySelector('.js-sidebar-options');
+
+ if (!sidebarOptEl) return;
+
+ const sidebarOptions = getSidebarOptions(sidebarOptEl);
+
+ new IssuableContext(sidebarOptions.currentUser);
+ Sidebar.initialize();
+};
diff --git a/app/assets/javascripts/issuable/issuable_context.js b/app/assets/javascripts/issuable/issuable_context.js
new file mode 100644
index 00000000000..453305dd6e0
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_context.js
@@ -0,0 +1,69 @@
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import $ from 'jquery';
+import Cookies from 'js-cookie';
+import { loadCSSFile } from '~/lib/utils/css_utils';
+import UsersSelect from '~/users_select';
+
+export default class IssuableContext {
+ constructor(currentUser) {
+ this.userSelect = new UsersSelect(currentUser);
+ this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
+
+ const $select2 = $('select.select2');
+
+ if ($select2.length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $select2.select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ }
+
+ $('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
+ return $(this).submit();
+ });
+ $('.issuable-sidebar .inline-update').on('change', '.js-assignee', function onClickAssignee() {
+ return $(this).submit();
+ });
+ $(document)
+ .off('click', '.issuable-sidebar .dropdown-content a')
+ .on('click', '.issuable-sidebar .dropdown-content a', (e) => e.preventDefault());
+
+ $(document)
+ .off('click', '.edit-link')
+ .on('click', '.edit-link', function onClickEdit(e) {
+ e.preventDefault();
+ const $block = $(this).parents('.block');
+ const $selectbox = $block.find('.selectbox');
+ if ($selectbox.is(':visible')) {
+ $selectbox.hide();
+ $block.find('.value:not(.dont-hide)').show();
+ } else {
+ $selectbox.show();
+ $block.find('.value:not(.dont-hide)').hide();
+ }
+
+ if ($selectbox.is(':visible')) {
+ setTimeout(() => $block.find('.dropdown-menu-toggle').trigger('click'), 0);
+ }
+ });
+
+ window.addEventListener('beforeunload', () => {
+ // collapsed_gutter cookie hides the sidebar
+ const bpBreakpoint = bp.getBreakpointSize();
+ const supportedSizes = ['xs', 'sm', 'md'];
+
+ if (supportedSizes.includes(bpBreakpoint)) {
+ Cookies.set('collapsed_gutter', true);
+ }
+ });
+ }
+}
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
new file mode 100644
index 00000000000..91f47a86cb7
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -0,0 +1,219 @@
+import $ from 'jquery';
+import Pikaday from 'pikaday';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
+import Autosave from '~/autosave';
+import AutoWidthDropdownSelect from '~/issuable/auto_width_dropdown_select';
+import { loadCSSFile } from '~/lib/utils/css_utils';
+import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
+import { select2AxiosTransport } from '~/lib/utils/select2_utils';
+import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
+import UsersSelect from '~/users_select';
+import ZenMode from '~/zen_mode';
+
+const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
+const MR_TARGET_BRANCH = 'merge_request[target_branch]';
+
+function organizeQuery(obj, isFallbackKey = false) {
+ if (!obj[MR_SOURCE_BRANCH] && !obj[MR_TARGET_BRANCH]) {
+ return obj;
+ }
+
+ if (isFallbackKey) {
+ return {
+ [MR_SOURCE_BRANCH]: obj[MR_SOURCE_BRANCH],
+ };
+ }
+
+ return {
+ [MR_SOURCE_BRANCH]: obj[MR_SOURCE_BRANCH],
+ [MR_TARGET_BRANCH]: obj[MR_TARGET_BRANCH],
+ };
+}
+
+function format(searchTerm, isFallbackKey = false) {
+ const queryObject = queryToObject(searchTerm, { legacySpacesDecode: true });
+ const organizeQueryObject = organizeQuery(queryObject, isFallbackKey);
+ const formattedQuery = objectToQuery(organizeQueryObject);
+
+ return formattedQuery;
+}
+
+function getFallbackKey() {
+ const searchTerm = format(document.location.search, true);
+ return ['autosave', document.location.pathname, searchTerm].join('/');
+}
+
+export default class IssuableForm {
+ constructor(form) {
+ this.form = form;
+ this.toggleWip = this.toggleWip.bind(this);
+ this.renderWipExplanation = this.renderWipExplanation.bind(this);
+ this.resetAutosave = this.resetAutosave.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ // prettier-ignore
+ this.draftRegex = new RegExp(
+ '^\\s*(' + // Line start, then any amount of leading whitespace
+ '\\[draft\\]\\s*' + // [Draft] and any following whitespace
+ '|draft:\\s*' + // Draft: and any following whitespace
+ '|\\(draft\\)\\s*' + // (Draft) and any following whitespace
+ ')+' + // At least one repeated match of the preceding parenthetical
+ '\\s*', // Any amount of trailing whitespace
+ 'i', // Match any case(s)
+ );
+
+ this.gfmAutoComplete = new GfmAutoComplete(
+ gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
+ ).setup();
+ this.usersSelect = new UsersSelect();
+ this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
+ this.zenMode = new ZenMode();
+
+ this.titleField = this.form.find('input[name*="[title]"]');
+ this.descriptionField = this.form.find('textarea[name*="[description]"]');
+ if (!(this.titleField.length && this.descriptionField.length)) {
+ return;
+ }
+
+ this.initAutosave();
+ this.form.on('submit', this.handleSubmit);
+ this.form.on('click', '.btn-cancel', this.resetAutosave);
+ this.initWip();
+
+ const $issuableDueDate = $('#issuable-due-date');
+
+ if ($issuableDueDate.length) {
+ const calendar = new Pikaday({
+ field: $issuableDueDate.get(0),
+ theme: 'gitlab-theme animate-picker',
+ format: 'yyyy-mm-dd',
+ container: $issuableDueDate.parent().get(0),
+ parse: (dateString) => parsePikadayDate(dateString),
+ toString: (date) => pikadayToString(date),
+ onSelect: (dateText) => $issuableDueDate.val(calendar.toString(dateText)),
+ firstDay: gon.first_day_of_week,
+ });
+ calendar.setDate(parsePikadayDate($issuableDueDate.val()));
+ }
+
+ this.$targetBranchSelect = $('.js-target-branch-select', this.form);
+
+ if (this.$targetBranchSelect.length) {
+ this.initTargetBranchDropdown();
+ }
+ }
+
+ initAutosave() {
+ const { search } = document.location;
+ const searchTerm = format(search);
+ const fallbackKey = getFallbackKey();
+
+ this.autosave = new Autosave(
+ this.titleField,
+ [document.location.pathname, searchTerm, 'title'],
+ `${fallbackKey}=title`,
+ );
+
+ return new Autosave(
+ this.descriptionField,
+ [document.location.pathname, searchTerm, 'description'],
+ `${fallbackKey}=description`,
+ );
+ }
+
+ handleSubmit() {
+ return this.resetAutosave();
+ }
+
+ resetAutosave() {
+ this.titleField.data('autosave').reset();
+ return this.descriptionField.data('autosave').reset();
+ }
+
+ initWip() {
+ this.$wipExplanation = this.form.find('.js-wip-explanation');
+ this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
+ if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
+ return undefined;
+ }
+ this.form.on('click', '.js-toggle-wip', this.toggleWip);
+ this.titleField.on('keyup blur', this.renderWipExplanation);
+ return this.renderWipExplanation();
+ }
+
+ workInProgress() {
+ return this.draftRegex.test(this.titleField.val());
+ }
+
+ renderWipExplanation() {
+ if (this.workInProgress()) {
+ // These strings are not "translatable" (the code is hard-coded to look for them)
+ this.$wipExplanation.find('code')[0].textContent =
+ 'Draft'; /* eslint-disable-line @gitlab/require-i18n-strings */
+ this.$wipExplanation.show();
+ return this.$noWipExplanation.hide();
+ }
+ this.$wipExplanation.hide();
+ return this.$noWipExplanation.show();
+ }
+
+ toggleWip(event) {
+ event.preventDefault();
+ if (this.workInProgress()) {
+ this.removeWip();
+ } else {
+ this.addWip();
+ }
+ return this.renderWipExplanation();
+ }
+
+ removeWip() {
+ return this.titleField.val(this.titleField.val().replace(this.draftRegex, ''));
+ }
+
+ addWip() {
+ this.titleField.val(`Draft: ${this.titleField.val()}`);
+ }
+
+ initTargetBranchDropdown() {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ this.$targetBranchSelect.select2({
+ ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
+ ajax: {
+ url: this.$targetBranchSelect.data('endpoint'),
+ dataType: 'JSON',
+ quietMillis: 250,
+ data(search) {
+ return {
+ search,
+ };
+ },
+ results({ results }) {
+ return {
+ // `data` keys are translated so we can't just access them with a string based key
+ results: results[Object.keys(results)[0]].map((name) => ({
+ id: name,
+ text: name,
+ })),
+ };
+ },
+ transport: select2AxiosTransport,
+ },
+ initSelection(el, callback) {
+ const val = el.val();
+
+ callback({
+ id: val,
+ text: val,
+ });
+ },
+ });
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ }
+}
diff --git a/app/assets/javascripts/issuable/issuable_index.js b/app/assets/javascripts/issuable/issuable_index.js
new file mode 100644
index 00000000000..407be7b39d5
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_index.js
@@ -0,0 +1,7 @@
+import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
+
+export default class IssuableIndex {
+ constructor(pagePrefix = 'issuable_') {
+ issuableInitBulkUpdateSidebar.init(pagePrefix);
+ }
+}