diff options
Diffstat (limited to 'app/assets/javascripts/issues_list')
11 files changed, 147 insertions, 43 deletions
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue index b13a389b963..62b52afdaca 100644 --- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue @@ -9,8 +9,7 @@ import { toNumber, omit } from 'lodash'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { scrollToElement, historyPushState } from '~/lib/utils/common_utils'; -// eslint-disable-next-line import/no-deprecated -import { setUrlParams, urlParamsToObject, getParameterByName } from '~/lib/utils/url_utility'; +import { setUrlParams, queryToObject, getParameterByName } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import initManualOrdering from '~/manual_ordering'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; @@ -264,8 +263,7 @@ export default { }); }, getQueryObject() { - // eslint-disable-next-line import/no-deprecated - return urlParamsToObject(window.location.search); + return queryToObject(window.location.search, { gatherArrays: true }); }, onPaginate(newPage) { if (newPage === this.page) return; diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue index 07492b0046c..a687a58a6ad 100644 --- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue +++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue @@ -48,8 +48,8 @@ export default { dueDate() { return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true); }, - isDueDateInPast() { - return isInPast(new Date(this.issue.dueDate)); + showDueDateInRed() { + return isInPast(new Date(this.issue.dueDate)) && !this.issue.closedAt; }, timeEstimate() { return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate; @@ -97,7 +97,7 @@ export default { v-if="issue.dueDate" v-gl-tooltip class="issuable-due-date gl-display-none gl-sm-display-inline-block! gl-mr-3" - :class="{ 'gl-text-red-500': isDueDateInPast }" + :class="{ 'gl-text-red-500': showDueDateInRed }" :title="__('Due date')" data-testid="issuable-due-date" > diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index 6563094ef72..ee0429c0432 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -9,10 +9,11 @@ import { GlTooltipDirective, } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { cloneDeep } from 'lodash'; import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql'; import createFlash from '~/flash'; import { TYPE_USER } from '~/graphql_shared/constants'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; @@ -20,7 +21,6 @@ import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants'; import { CREATED_DESC, i18n, - initialPageParams, issuesCountSmartQueryBase, MAX_LIST_SIZE, PAGE_SIZE, @@ -36,6 +36,7 @@ import { TOKEN_TYPE_LABEL, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_MY_REACTION, + TOKEN_TYPE_TYPE, TOKEN_TYPE_WEIGHT, UPDATED_DESC, urlSortParams, @@ -46,12 +47,13 @@ import { convertToUrlParams, getDueDateValue, getFilterTokens, + getInitialPageParams, getSortKey, getSortOptions, } from '~/issues_list/utils'; import axios from '~/lib/utils/axios_utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; -import { getParameterByName } from '~/lib/utils/url_utility'; +import { getParameterByName, joinPaths } from '~/lib/utils/url_utility'; import { DEFAULT_NONE_ANY, OPERATOR_IS_ONLY, @@ -63,6 +65,7 @@ import { TOKEN_TITLE_LABEL, TOKEN_TITLE_MILESTONE, TOKEN_TITLE_MY_REACTION, + TOKEN_TITLE_TYPE, TOKEN_TITLE_WEIGHT, } from '~/vue_shared/components/filtered_search_bar/constants'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; @@ -73,6 +76,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; import eventHub from '../eventhub'; +import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql'; import searchIterationsQuery from '../queries/search_iterations.query.graphql'; import searchLabelsQuery from '../queries/search_labels.query.graphql'; import searchMilestonesQuery from '../queries/search_milestones.query.graphql'; @@ -160,18 +164,22 @@ export default { }, }, data() { + const filterTokens = getFilterTokens(window.location.search); const state = getParameterByName(PARAM_STATE); + const sortKey = getSortKey(getParameterByName(PARAM_SORT)); const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC; + this.initialFilterTokens = cloneDeep(filterTokens); + return { dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)), exportCsvPathWithQuery: this.getExportCsvPathWithQuery(), - filterTokens: getFilterTokens(window.location.search), + filterTokens, issues: [], pageInfo: {}, - pageParams: initialPageParams, + pageParams: getInitialPageParams(sortKey), showBulkEditSidebar: false, - sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey, + sortKey: sortKey || defaultSortKey, state: state || IssuableStates.Opened, }; }, @@ -275,7 +283,6 @@ export default { avatar_url: gon.current_user_avatar_url, }); } - const tokens = [ { type: TOKEN_TYPE_AUTHOR, @@ -306,7 +313,6 @@ export default { icon: 'clock', token: MilestoneToken, unique: true, - defaultMilestones: [], fetchMilestones: this.fetchMilestones, }, { @@ -317,6 +323,18 @@ export default { defaultLabels: DEFAULT_NONE_ANY, fetchLabels: this.fetchLabels, }, + { + type: TOKEN_TYPE_TYPE, + title: TOKEN_TITLE_TYPE, + icon: 'issues', + token: GlFilteredSearchToken, + operators: OPERATOR_IS_ONLY, + options: [ + { icon: 'issue-type-issue', title: 'issue', value: 'issue' }, + { icon: 'issue-type-incident', title: 'incident', value: 'incident' }, + { icon: 'issue-type-test-case', title: 'test_case', value: 'test_case' }, + ], + }, ]; if (this.isSignedIn) { @@ -518,12 +536,12 @@ export default { }, handleClickTab(state) { if (this.state !== state) { - this.pageParams = initialPageParams; + this.pageParams = getInitialPageParams(this.sortKey); } this.state = state; }, handleFilter(filter) { - this.pageParams = initialPageParams; + this.pageParams = getInitialPageParams(this.sortKey); this.filterTokens = filter; }, handleNextPage() { @@ -560,14 +578,16 @@ export default { } return axios - .put(`${this.issuesPath}/${issueToMove.iid}/reorder`, { - move_before_id: isMovingToBeginning ? null : moveBeforeId, - move_after_id: isMovingToEnd ? null : moveAfterId, + .put(joinPaths(this.issuesPath, issueToMove.iid, 'reorder'), { + move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId), + move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId), }) .then(() => { - // Move issue to new position in list - this.issues.splice(oldIndex, 1); - this.issues.splice(newIndex, 0, issueToMove); + const serializedVariables = JSON.stringify(this.queryVariables); + this.$apollo.mutate({ + mutation: reorderIssuesMutation, + variables: { oldIndex, newIndex, serializedVariables }, + }); }) .catch(() => { createFlash({ message: this.$options.i18n.reorderError }); @@ -575,7 +595,7 @@ export default { }, handleSort(sortKey) { if (this.sortKey !== sortKey) { - this.pageParams = initialPageParams; + this.pageParams = getInitialPageParams(sortKey); } this.sortKey = sortKey; }, @@ -593,7 +613,7 @@ export default { recent-searches-storage-key="issues" :search-input-placeholder="$options.i18n.searchPlaceholder" :search-tokens="searchTokens" - :initial-filter-value="filterTokens" + :initial-filter-value="initialFilterTokens" :sort-options="sortOptions" :initial-sort-by="sortKey" :issuables="issues" diff --git a/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue b/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue index ba0ca57523a..fb1dbef666c 100644 --- a/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue +++ b/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue @@ -59,9 +59,6 @@ export default { shouldShowInProgressAlert: isInProgress(project.jiraImportStatus), }; }, - skip() { - return !this.isJiraConfigured || !this.canEdit; - }, }, }, computed: { @@ -75,6 +72,9 @@ export default { labelTarget() { return `${this.issuesPath}?label_name[]=${encodeURIComponent(this.jiraImport.label.title)}`; }, + shouldRender() { + return this.jiraImport.shouldShowInProgressAlert || this.jiraImport.shouldShowFinishedAlert; + }, }, methods: { hideFinishedAlert() { @@ -89,7 +89,7 @@ export default { </script> <template> - <div class="gl-my-5"> + <div v-if="shouldRender" class="gl-my-5"> <gl-alert v-if="jiraImport.shouldShowInProgressAlert" @dismiss="hideInProgressAlert"> {{ __('Import in progress. Refresh page to see newly added issues.') }} </gl-alert> diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index d94d4b9a19a..3f5b0d1feb5 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -5,6 +5,8 @@ import { FILTER_ANY, FILTER_CURRENT, FILTER_NONE, + FILTER_STARTED, + FILTER_UPCOMING, OPERATOR_IS, OPERATOR_IS_NOT, } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -107,10 +109,14 @@ export const PARAM_DUE_DATE = 'due_date'; export const PARAM_SORT = 'sort'; export const PARAM_STATE = 'state'; -export const initialPageParams = { +export const defaultPageSizeParams = { firstPageSize: PAGE_SIZE, }; +export const largePageSizeParams = { + firstPageSize: PAGE_SIZE_MANUAL, +}; + export const DUE_DATE_NONE = '0'; export const DUE_DATE_ANY = ''; export const DUE_DATE_OVERDUE = 'overdue'; @@ -186,12 +192,19 @@ export const URL_PARAM = 'urlParam'; export const NORMAL_FILTER = 'normalFilter'; export const SPECIAL_FILTER = 'specialFilter'; export const ALTERNATIVE_FILTER = 'alternativeFilter'; -export const SPECIAL_FILTER_VALUES = [FILTER_NONE, FILTER_ANY, FILTER_CURRENT]; +export const SPECIAL_FILTER_VALUES = [ + FILTER_NONE, + FILTER_ANY, + FILTER_CURRENT, + FILTER_UPCOMING, + FILTER_STARTED, +]; export const TOKEN_TYPE_AUTHOR = 'author_username'; export const TOKEN_TYPE_ASSIGNEE = 'assignee_username'; export const TOKEN_TYPE_MILESTONE = 'milestone'; export const TOKEN_TYPE_LABEL = 'labels'; +export const TOKEN_TYPE_TYPE = 'type'; export const TOKEN_TYPE_MY_REACTION = 'my_reaction_emoji'; export const TOKEN_TYPE_CONFIDENTIAL = 'confidential'; export const TOKEN_TYPE_ITERATION = 'iteration'; @@ -231,10 +244,12 @@ export const filters = { [TOKEN_TYPE_MILESTONE]: { [API_PARAM]: { [NORMAL_FILTER]: 'milestoneTitle', + [SPECIAL_FILTER]: 'milestoneWildcardId', }, [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'milestone_title', + [SPECIAL_FILTER]: 'milestone_title', }, [OPERATOR_IS_NOT]: { [NORMAL_FILTER]: 'not[milestone_title]', @@ -256,6 +271,18 @@ export const filters = { }, }, }, + [TOKEN_TYPE_TYPE]: { + [API_PARAM]: { + [NORMAL_FILTER]: 'types', + [SPECIAL_FILTER]: 'types', + }, + [URL_PARAM]: { + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'type[]', + [SPECIAL_FILTER]: 'type[]', + }, + }, + }, [TOKEN_TYPE_MY_REACTION]: { [API_PARAM]: { [NORMAL_FILTER]: 'myReactionEmoji', diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index 71ceb9bef55..dcc7ee72273 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -1,5 +1,7 @@ +import produce from 'immer'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql'; import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; import createDefaultClient from '~/lib/graphql'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; @@ -13,9 +15,16 @@ export function mountJiraIssuesListApp() { return false; } - Vue.use(VueApollo); + const { issuesPath, projectPath } = el.dataset; + const canEdit = parseBoolean(el.dataset.canEdit); + const isJiraConfigured = parseBoolean(el.dataset.isJiraConfigured); + + if (!isJiraConfigured || !canEdit) { + return false; + } - const defaultClient = createDefaultClient(); + Vue.use(VueApollo); + const defaultClient = createDefaultClient({}, { assumeImmutableResults: true }); const apolloProvider = new VueApollo({ defaultClient, }); @@ -26,10 +35,10 @@ export function mountJiraIssuesListApp() { render(createComponent) { return createComponent(JiraIssuesImportStatusRoot, { props: { - canEdit: parseBoolean(el.dataset.canEdit), - isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured), - issuesPath: el.dataset.issuesPath, - projectPath: el.dataset.projectPath, + canEdit, + isJiraConfigured, + issuesPath, + projectPath, }, }); }, @@ -74,7 +83,27 @@ export function mountIssuesListApp() { Vue.use(VueApollo); - const defaultClient = createDefaultClient({}, { assumeImmutableResults: true }); + const resolvers = { + Mutation: { + reorderIssues: (_, { oldIndex, newIndex, serializedVariables }, { cache }) => { + const variables = JSON.parse(serializedVariables); + const sourceData = cache.readQuery({ query: getIssuesQuery, variables }); + + const data = produce(sourceData, (draftData) => { + const issues = draftData.project.issues.nodes.slice(); + const issueToMove = issues[oldIndex]; + issues.splice(oldIndex, 1); + issues.splice(newIndex, 0, issueToMove); + + draftData.project.issues.nodes = issues; + }); + + cache.writeQuery({ query: getIssuesQuery, variables, data }); + }, + }, + }; + + const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true }); const apolloProvider = new VueApollo({ defaultClient, }); diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql index 124190915c0..30a01b4c3b0 100644 --- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql +++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql @@ -12,6 +12,8 @@ query getProjectIssues( $authorUsername: String $labelName: [String] $milestoneTitle: [String] + $milestoneWildcardId: MilestoneWildcardId + $types: [IssueType!] $not: NegatedIssueFilterInput $beforeCursor: String $afterCursor: String @@ -28,6 +30,8 @@ query getProjectIssues( authorUsername: $authorUsername labelName: $labelName milestoneTitle: $milestoneTitle + milestoneWildcardId: $milestoneWildcardId + types: $types not: $not before: $beforeCursor after: $afterCursor diff --git a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql index a1742859640..e6896131da9 100644 --- a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql +++ b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql @@ -7,6 +7,8 @@ query getProjectIssuesCount( $authorUsername: String $labelName: [String] $milestoneTitle: [String] + $milestoneWildcardId: MilestoneWildcardId + $types: [IssueType!] $not: NegatedIssueFilterInput ) { project(fullPath: $projectPath) { @@ -18,6 +20,8 @@ query getProjectIssuesCount( authorUsername: $authorUsername labelName: $labelName milestoneTitle: $milestoneTitle + milestoneWildcardId: $milestoneWildcardId + types: $types not: $not ) { count diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql index f7ebf64ffb8..633b06eced8 100644 --- a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql +++ b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql @@ -7,6 +7,7 @@ fragment IssueFragment on Issue { downvotes dueDate humanTimeEstimate + mergeRequestsCount moved title updatedAt diff --git a/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql b/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql new file mode 100644 index 00000000000..5927e3e83c7 --- /dev/null +++ b/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql @@ -0,0 +1,7 @@ +mutation reorderIssues($oldIndex: Int, $newIndex: Int, $serializedVariables: String) { + reorderIssues( + oldIndex: $oldIndex + newIndex: $newIndex + serializedVariables: $serializedVariables + ) @client +} diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js index 49f937cc453..1d3d07475af 100644 --- a/app/assets/javascripts/issues_list/utils.js +++ b/app/assets/javascripts/issues_list/utils.js @@ -3,12 +3,14 @@ import { BLOCKING_ISSUES_DESC, CREATED_ASC, CREATED_DESC, + defaultPageSizeParams, DUE_DATE_ASC, DUE_DATE_DESC, DUE_DATE_VALUES, filters, LABEL_PRIORITY_ASC, LABEL_PRIORITY_DESC, + largePageSizeParams, MILESTONE_DUE_ASC, MILESTONE_DUE_DESC, NORMAL_FILTER, @@ -21,6 +23,8 @@ import { SPECIAL_FILTER_VALUES, TOKEN_TYPE_ASSIGNEE, TOKEN_TYPE_ITERATION, + TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_TYPE, UPDATED_ASC, UPDATED_DESC, URL_PARAM, @@ -35,6 +39,9 @@ import { OPERATOR_IS_NOT, } from '~/vue_shared/components/filtered_search_bar/constants'; +export const getInitialPageParams = (sortKey) => + sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams; + export const getSortKey = (sort) => Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort); @@ -186,8 +193,17 @@ const getFilterType = (data, tokenType = '') => ? SPECIAL_FILTER : NORMAL_FILTER; -const isIterationSpecialValue = (tokenType, value) => - tokenType === TOKEN_TYPE_ITERATION && SPECIAL_FILTER_VALUES.includes(value); +const isWildcardValue = (tokenType, value) => + (tokenType === TOKEN_TYPE_ITERATION || tokenType === TOKEN_TYPE_MILESTONE) && + SPECIAL_FILTER_VALUES.includes(value); + +const requiresUpperCaseValue = (tokenType, value) => + tokenType === TOKEN_TYPE_TYPE || isWildcardValue(tokenType, value); + +const formatData = (token) => + requiresUpperCaseValue(token.type, token.value.data) + ? token.value.data.toUpperCase() + : token.value.data; export const convertToApiParams = (filterTokens) => { const params = {}; @@ -199,9 +215,7 @@ export const convertToApiParams = (filterTokens) => { const filterType = getFilterType(token.value.data, token.type); const field = filters[token.type][API_PARAM][filterType]; const obj = token.value.operator === OPERATOR_IS_NOT ? not : params; - const data = isIterationSpecialValue(token.type, token.value.data) - ? token.value.data.toUpperCase() - : token.value.data; + const data = formatData(token); Object.assign(obj, { [field]: obj[field] ? [obj[field], data].flat() : data, }); |