diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-07 18:15:03 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-07 18:15:03 +0300 |
commit | 3a0f6ebaa9a80e34c09f599a624f6e4520009ef1 (patch) | |
tree | 4560fa6bcba7d950595e906ee3286dd1e44642f1 /app/assets/javascripts/issuable | |
parent | 6dd9e3644eea1a5c605a6a623cae1d53b156b9e5 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/issuable')
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); + } +} |