From cf85de264d049f1f8ff14b23f38f8331ae4c60fa Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 6 Nov 2019 21:06:44 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../issuable_bulk_update_sidebar.js | 19 +- .../issuables_list/components/issuable.vue | 327 ++++++++++++++ .../components/issuables_list_app.vue | 277 ++++++++++++ .../javascripts/issuables_list/constants.js | 33 ++ .../javascripts/issuables_list/eventhub.js | 5 + .../javascripts/issuables_list/index.js | 24 + .../javascripts/lib/utils/datetime_utility.js | 15 +- app/assets/javascripts/manual_ordering.js | 6 +- .../javascripts/pages/groups/issues/index.js | 3 + .../components/issue/issue_assignees.vue | 51 ++- app/controllers/groups_controller.rb | 4 + app/views/groups/issues.html.haml | 8 +- app/views/projects/issues/_issue.html.haml | 1 + locale/gitlab.pot | 9 + spec/features/explore/groups_spec.rb | 4 + spec/features/groups/issues_spec.rb | 4 + .../issuables_list_app_spec.js.snap | 15 + .../components/issuable_spec.js | 338 +++++++++++++++ .../components/issuables_list_app_spec.js | 410 ++++++++++++++++++ .../issuables_list/issuable_list_test_data.js | 72 +++ .../components/issue/issue_assignees_spec.js | 191 ++++---- 21 files changed, 1697 insertions(+), 119 deletions(-) create mode 100644 app/assets/javascripts/issuables_list/components/issuable.vue create mode 100644 app/assets/javascripts/issuables_list/components/issuables_list_app.vue create mode 100644 app/assets/javascripts/issuables_list/constants.js create mode 100644 app/assets/javascripts/issuables_list/eventhub.js create mode 100644 app/assets/javascripts/issuables_list/index.js create mode 100644 spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap create mode 100644 spec/frontend/issuables_list/components/issuable_spec.js create mode 100644 spec/frontend/issuables_list/components/issuables_list_app_spec.js create mode 100644 spec/frontend/issuables_list/issuable_list_test_data.js diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 74150ce3a8b..bd6e8433544 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -1,11 +1,13 @@ /* eslint-disable class-methods-use-this, no-new */ import $ from 'jquery'; +import { property } from 'underscore'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import MilestoneSelect from './milestone_select'; import issueStatusSelect from './issue_status_select'; import subscriptionSelect from './subscription_select'; import LabelsSelect from './labels_select'; +import issueableEventHub from './issuables_list/eventhub'; const HIDDEN_CLASS = 'hidden'; const DISABLED_CONTENT_CLASS = 'disabled-content'; @@ -14,6 +16,8 @@ const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-si export default class IssuableBulkUpdateSidebar { constructor() { + this.vueIssuablesListFeature = property(['gon', 'features', 'vueIssuablesList'])(window); + this.initDomElements(); this.bindEvents(); this.initDropdowns(); @@ -41,6 +45,17 @@ export default class IssuableBulkUpdateSidebar { this.$issuesList.on('change', () => this.updateFormState()); this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit()); this.$checkAllContainer.on('click', () => this.updateFormState()); + + if (this.vueIssuablesListFeature) { + issueableEventHub.$on('issuables:updateBulkEdit', () => { + // Danger! Strong coupling ahead! + // The bulk update sidebar and its dropdowns look for .selected-issuable checkboxes, and get data on which issue + // is selected by inspecting the DOM. Ideally, we would pass the selected issuable IDs and their properties + // explicitly, but this component is used in too many places right now to refactor straight away. + + this.updateFormState(); + }); + } } initDropdowns() { @@ -73,6 +88,8 @@ export default class IssuableBulkUpdateSidebar { toggleBulkEdit(e, enable) { e.preventDefault(); + issueableEventHub.$emit('issuables:toggleBulkEdit', enable); + this.toggleSidebarDisplay(enable); this.toggleBulkEditButtonDisabled(enable); this.toggleOtherFiltersDisabled(enable); @@ -106,7 +123,7 @@ export default class IssuableBulkUpdateSidebar { } toggleCheckboxDisplay(show) { - this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show); + this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show || this.vueIssuablesListFeature); this.$issueChecks.toggleClass(HIDDEN_CLASS, !show); } diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue new file mode 100644 index 00000000000..41b826e0394 --- /dev/null +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -0,0 +1,327 @@ + + diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue new file mode 100644 index 00000000000..6b6a8bd4068 --- /dev/null +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -0,0 +1,277 @@ + + + diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issuables_list/constants.js new file mode 100644 index 00000000000..71b9c52c703 --- /dev/null +++ b/app/assets/javascripts/issuables_list/constants.js @@ -0,0 +1,33 @@ +// Maps sort order as it appears in the URL query to API `order_by` and `sort` params. +const PRIORITY = 'priority'; +const ASC = 'asc'; +const DESC = 'desc'; +const CREATED_AT = 'created_at'; +const UPDATED_AT = 'updated_at'; +const DUE_DATE = 'due_date'; +const MILESTONE_DUE = 'milestone_due'; +const POPULARITY = 'popularity'; +const WEIGHT = 'weight'; +const LABEL_PRIORITY = 'label_priority'; +export const RELATIVE_POSITION = 'relative_position'; +export const LOADING_LIST_ITEMS_LENGTH = 8; +export const PAGE_SIZE = 20; +export const PAGE_SIZE_MANUAL = 100; + +export const sortOrderMap = { + priority: { order_by: PRIORITY, sort: ASC }, // asc and desc are flipped for some reason + created_date: { order_by: CREATED_AT, sort: DESC }, + created_asc: { order_by: CREATED_AT, sort: ASC }, + updated_desc: { order_by: UPDATED_AT, sort: DESC }, + updated_asc: { order_by: UPDATED_AT, sort: ASC }, + milestone_due_desc: { order_by: MILESTONE_DUE, sort: DESC }, + milestone: { order_by: MILESTONE_DUE, sort: ASC }, + due_date_desc: { order_by: DUE_DATE, sort: DESC }, + due_date: { order_by: DUE_DATE, sort: ASC }, + popularity: { order_by: POPULARITY, sort: DESC }, + popularity_asc: { order_by: POPULARITY, sort: ASC }, + label_priority: { order_by: LABEL_PRIORITY, sort: ASC }, // asc and desc are flipped + relative_position: { order_by: RELATIVE_POSITION, sort: ASC }, + weight_desc: { order_by: WEIGHT, sort: DESC }, + weight: { order_by: WEIGHT, sort: ASC }, +}; diff --git a/app/assets/javascripts/issuables_list/eventhub.js b/app/assets/javascripts/issuables_list/eventhub.js new file mode 100644 index 00000000000..d1601a7d8f3 --- /dev/null +++ b/app/assets/javascripts/issuables_list/eventhub.js @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +const issueablesEventBus = new Vue(); + +export default issueablesEventBus; diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issuables_list/index.js new file mode 100644 index 00000000000..9fc7fa837ff --- /dev/null +++ b/app/assets/javascripts/issuables_list/index.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import IssuablesListApp from './components/issuables_list_app.vue'; + +export default function initIssuablesList() { + if (!gon.features || !gon.features.vueIssuablesList) { + return; + } + + document.querySelectorAll('.js-issuables-list').forEach(el => { + const { canBulkEdit, ...data } = el.dataset; + + const props = { + ...data, + canBulkEdit: Boolean(canBulkEdit), + }; + + return new Vue({ + el, + render(createElement) { + return createElement(IssuablesListApp, { props }); + }, + }); + }); +} diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index e9d8d0a4184..118d5facafc 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -78,11 +78,11 @@ export const getDayName = date => * @param {date} datetime * @returns {String} */ -export const formatDate = datetime => { +export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => { if (_.isString(datetime) && datetime.match(/\d+-\d+\d+ /)) { throw new Error(__('Invalid date')); } - return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); + return dateFormat(datetime, format); }; /** @@ -558,6 +558,17 @@ export const calculateRemainingMilliseconds = endDate => { export const getDateInPast = (date, daysInPast) => new Date(newDate(date).setDate(date.getDate() - daysInPast)); +/* + * Appending T00:00:00 makes JS assume local time and prevents it from shifting the date + * to match the user's time zone. We want to display the date in server time for now, to + * be consistent with the "edit issue -> due date" UI. + */ + +export const newDateAsLocaleTime = date => { + const suffix = 'T00:00:00'; + return new Date(`${date}${suffix}`); +}; + export const beginOfDayTime = 'T00:00:00Z'; export const endOfDayTime = 'T23:59:59Z'; diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js index 29a0e5a904a..f93dbcd4c47 100644 --- a/app/assets/javascripts/manual_ordering.js +++ b/app/assets/javascripts/manual_ordering.js @@ -18,7 +18,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) => createFlash(s__("ManualOrdering|Couldn't save the order of the issues")); }); -const initManualOrdering = () => { +const initManualOrdering = (draggableSelector = 'li.issue') => { const issueList = document.querySelector('.manual-ordering'); if (!issueList || !(gon.current_user_id > 0)) { @@ -34,14 +34,14 @@ const initManualOrdering = () => { group: { name: 'issues', }, - draggable: 'li.issue', + draggable: draggableSelector, onStart: () => { sortableStart(); }, onUpdate: event => { const el = event.item; - const url = el.getAttribute('url'); + const url = el.getAttribute('url') || el.dataset.url; const prev = el.previousElementSibling; const next = el.nextElementSibling; diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index dcdee77a8ab..090e1a2bc6d 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -1,3 +1,4 @@ +import initIssuablesList from '~/issuables_list'; import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; @@ -11,6 +12,8 @@ document.addEventListener('DOMContentLoaded', () => { IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX); + initIssuablesList(); + initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, isGroupDecendent: true, diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue index 715cf97f0ac..1524b313f9f 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue @@ -1,7 +1,6 @@