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>2022-11-17 14:33:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-17 14:33:21 +0300
commit7021455bd1ed7b125c55eb1b33c5a01f2bc55ee0 (patch)
tree5bdc2229f5198d516781f8d24eace62fc7e589e9 /app/assets/javascripts/issues
parent185b095e93520f96e9cfc31d9c3e69b498cdab7c (diff)
Add latest changes from gitlab-org/gitlab@15-6-stable-eev15.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/issues')
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue56
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js25
-rw-r--r--app/assets/javascripts/issues/index.js4
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue56
-rw-r--r--app/assets/javascripts/issues/list/constants.js37
-rw-r--r--app/assets/javascripts/issues/list/graphql.js25
-rw-r--r--app/assets/javascripts/issues/list/index.js27
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql3
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql7
-rw-r--r--app/assets/javascripts/issues/list/utils.js33
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/fields/type.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js6
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue29
15 files changed, 228 insertions, 89 deletions
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
new file mode 100644
index 00000000000..29f6aecca03
--- /dev/null
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { __ } from '~/locale';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+
+export default {
+ i18n: {
+ calendarButtonText: __('Subscribe to calendar'),
+ emptyStateTitle: __('Please select at least one filter to see results'),
+ rssButtonText: __('Subscribe to RSS feed'),
+ searchInputPlaceholder: __('Search or filter results...'),
+ },
+ IssuableListTabs,
+ components: {
+ GlButton,
+ GlEmptyState,
+ IssuableList,
+ },
+ inject: ['calendarPath', 'emptyStateSvgPath', 'isSignedIn', 'rssPath'],
+ data() {
+ return {
+ issues: [],
+ searchTokens: [],
+ sortOptions: [],
+ state: IssuableStates.Opened,
+ };
+ },
+};
+</script>
+
+<template>
+ <issuable-list
+ namespace="dashboard"
+ recent-searches-storage-key="issues"
+ :search-input-placeholder="$options.i18n.searchInputPlaceholder"
+ :search-tokens="searchTokens"
+ :sort-options="sortOptions"
+ :issuables="issues"
+ :tabs="$options.IssuableListTabs"
+ :current-tab="state"
+ >
+ <template #nav-actions>
+ <gl-button :href="rssPath" icon="rss">
+ {{ $options.i18n.rssButtonText }}
+ </gl-button>
+ <gl-button :href="calendarPath" icon="calendar">
+ {{ $options.i18n.calendarButtonText }}
+ </gl-button>
+ </template>
+
+ <template #empty-state>
+ <gl-empty-state :svg-path="emptyStateSvgPath" :title="$options.i18n.emptyStateTitle" />
+ </template>
+ </issuable-list>
+</template>
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
new file mode 100644
index 00000000000..a1ae3b93f7d
--- /dev/null
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import IssuesDashboardApp from './components/issues_dashboard_app.vue';
+
+export function mountIssuesDashboardApp() {
+ const el = document.querySelector('.js-issues-dashboard');
+
+ if (!el) {
+ return null;
+ }
+
+ const { calendarPath, emptyStateSvgPath, isSignedIn, rssPath } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'IssuesDashboardRoot',
+ provide: {
+ calendarPath,
+ emptyStateSvgPath,
+ isSignedIn: parseBoolean(isSignedIn),
+ rssPath,
+ },
+ render: (createComponent) => createComponent(IssuesDashboardApp),
+ });
+}
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index 22ac37656ea..a785790169d 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -18,9 +18,9 @@ import {
} from '~/issues/show';
import { parseIssuableData } from '~/issues/show/utils/parse_data';
import LabelsSelect from '~/labels/labels_select';
-import MilestoneSelect from '~/milestones/milestone_select';
import initNotesApp from '~/notes';
import { store } from '~/notes/stores';
+import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
import initLinkedResources from '~/linked_resources';
@@ -41,11 +41,11 @@ export function initForm() {
new IssuableForm($('.issue-form')); // eslint-disable-line no-new
new IssuableTemplateSelectors({ warnTemplateOverride: true }); // eslint-disable-line no-new
new LabelsSelect(); // eslint-disable-line no-new
- new MilestoneSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initTitleSuggestions();
initTypePopover();
+ mountMilestoneDropdown();
}
export function initShow() {
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 acb6aa93f0f..64de4b1947b 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -27,6 +27,7 @@ import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
DEFAULT_NONE_ANY,
+ FILTERED_SEARCH_TERM,
OPERATOR_IS_ONLY,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
@@ -38,12 +39,22 @@ import {
TOKEN_TITLE_ORGANIZATION,
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
- FILTERED_SEARCH_TERM,
+ OPERATOR_IS_NOT_OR,
+ OPERATOR_IS_AND_IS_NOT,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { WORK_ITEM_TYPE_ENUM_TASK } from '~/work_items/constants';
import {
CREATED_DESC,
defaultTypeTokenOptions,
@@ -59,16 +70,6 @@ import {
PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_ASC,
- TOKEN_TYPE_ASSIGNEE,
- TOKEN_TYPE_AUTHOR,
- TOKEN_TYPE_CONFIDENTIAL,
- TOKEN_TYPE_CONTACT,
- TOKEN_TYPE_LABEL,
- TOKEN_TYPE_MILESTONE,
- TOKEN_TYPE_MY_REACTION,
- TOKEN_TYPE_ORGANIZATION,
- TOKEN_TYPE_RELEASE,
- TOKEN_TYPE_TYPE,
TYPE_TOKEN_TASK_OPTION,
UPDATED_DESC,
urlSortParams,
@@ -140,7 +141,6 @@ export default {
'hasAnyProjects',
'hasBlockedIssuesFeature',
'hasIssueWeightsFeature',
- 'hasMultipleIssueAssigneesFeature',
'hasScopedLabelsFeature',
'initialEmail',
'initialSort',
@@ -239,21 +239,17 @@ export default {
state: this.state,
...this.pageParams,
...this.apiFilterParams,
- types: this.apiFilterParams.types || this.defaultWorkItemTypes,
+ types: this.apiFilterParams.types || defaultWorkItemTypes,
};
},
namespace() {
return this.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
},
- defaultWorkItemTypes() {
- return this.isWorkItemsEnabled
- ? defaultWorkItemTypes
- : defaultWorkItemTypes.filter((type) => type !== WORK_ITEM_TYPE_ENUM_TASK);
- },
typeTokenOptions() {
- return this.isWorkItemsEnabled
- ? defaultTypeTokenOptions.concat(TYPE_TOKEN_TASK_OPTION)
- : defaultTypeTokenOptions;
+ return defaultTypeTokenOptions.concat(TYPE_TOKEN_TASK_OPTION);
+ },
+ hasOrFeature() {
+ return this.glFeatures.orIssuableQueries;
},
hasSearch() {
return (
@@ -272,9 +268,6 @@ export default {
isOpenTab() {
return this.state === IssuableStates.Opened;
},
- isWorkItemsEnabled() {
- return this.glFeatures.workItems;
- },
showCsvButtons() {
return this.isProject && this.isSignedIn;
},
@@ -324,8 +317,8 @@ export default {
icon: 'user',
token: AuthorToken,
dataType: 'user',
- unique: !this.hasMultipleIssueAssigneesFeature,
defaultAuthors: DEFAULT_NONE_ANY,
+ operators: this.hasOrFeature ? OPERATOR_IS_NOT_OR : OPERATOR_IS_AND_IS_NOT,
fetchAuthors: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedAuthors,
@@ -565,6 +558,7 @@ export default {
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
bulkUpdateSidebar.initStatusDropdown();
bulkUpdateSidebar.initSubscriptionsDropdown();
+ bulkUpdateSidebar.initMoveIssuesButton();
const usersSelect = await import('~/users_select');
const UsersSelect = usersSelect.default;
@@ -793,6 +787,7 @@ export default {
:show-page-size-change-controls="showPageSizeControls"
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
+ :show-filtered-search-friendly-text="hasOrFeature"
show-work-item-type-icon
@click-tab="handleClickTab"
@dismiss-alert="handleDismissAlert"
@@ -959,12 +954,17 @@ export default {
<gl-empty-state
v-else
- :description="$options.i18n.noIssuesSignedOutDescription"
:title="$options.i18n.noIssuesSignedOutTitle"
:svg-path="emptyStateSvgPath"
:primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
:primary-button-link="signInPath"
- />
+ >
+ <template #description>
+ <gl-link :href="issuesHelpPagePath" target="_blank">{{
+ $options.i18n.noIssuesSignedOutDescription
+ }}</gl-link>
+ </template>
+ </gl-empty-state>
<issuable-by-email v-if="showIssuableByEmail" class="gl-text-center gl-pt-5 gl-pb-7" />
</div>
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 9fe8899ab39..5ed9ceea856 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -7,7 +7,21 @@ import {
FILTER_UPCOMING,
OPERATOR_IS,
OPERATOR_IS_NOT,
+ OPERATOR_OR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_EPIC,
TOKEN_TYPE_HEALTH,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
WORK_ITEM_TYPE_ENUM_INCIDENT,
@@ -46,10 +60,8 @@ export const i18n = {
noIssuesSignedInDescription: __('Learn more about issues.'),
noIssuesSignedInTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noIssuesSignedOutButtonText: __('Register / Sign In'),
- noIssuesSignedOutDescription: __(
- 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
- ),
- noIssuesSignedOutTitle: __('There are no issues to show'),
+ noIssuesSignedOutDescription: __('Learn more about issues.'),
+ noIssuesSignedOutTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noSearchResultsDescription: __('To widen your search, change or remove filters above'),
noSearchResultsTitle: __('Sorry, your filter produced no results'),
relatedMergeRequests: __('Related merge requests'),
@@ -136,20 +148,6 @@ export const specialFilterValues = [
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_RELEASE = 'release';
-export const TOKEN_TYPE_MY_REACTION = 'my_reaction_emoji';
-export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
-export const TOKEN_TYPE_ITERATION = 'iteration';
-export const TOKEN_TYPE_EPIC = 'epic_id';
-export const TOKEN_TYPE_WEIGHT = 'weight';
-export const TOKEN_TYPE_CONTACT = 'crm_contact';
-export const TOKEN_TYPE_ORGANIZATION = 'crm_organization';
-
export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' };
// This should be consistent with Issue::TYPES_FOR_LIST in the backend
@@ -195,6 +193,9 @@ export const filters = {
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username][]',
},
+ [OPERATOR_OR]: {
+ [NORMAL_FILTER]: 'or[assignee_username][]',
+ },
},
},
[TOKEN_TYPE_MILESTONE]: {
diff --git a/app/assets/javascripts/issues/list/graphql.js b/app/assets/javascripts/issues/list/graphql.js
new file mode 100644
index 00000000000..5ef61727a3d
--- /dev/null
+++ b/app/assets/javascripts/issues/list/graphql.js
@@ -0,0 +1,25 @@
+import produce from 'immer';
+import createDefaultClient from '~/lib/graphql';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+
+const resolvers = {
+ Mutation: {
+ reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => {
+ const variables = JSON.parse(serializedVariables);
+ const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
+
+ const data = produce(sourceData, (draftData) => {
+ const issues = draftData[namespace].issues.nodes.slice();
+ const issueToMove = issues[oldIndex];
+ issues.splice(oldIndex, 1);
+ issues.splice(newIndex, 0, issueToMove);
+
+ draftData[namespace].issues.nodes = issues;
+ });
+
+ cache.writeQuery({ query: getIssuesQuery, variables, data });
+ },
+ },
+};
+
+export const gqlClient = createDefaultClient(resolvers);
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 93333c31b34..5e04dd1971c 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -1,12 +1,11 @@
-import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
-import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue';
+import { gqlClient } from './graphql';
export function mountJiraIssuesListApp() {
const el = document.querySelector('.js-jira-issues-import-status');
@@ -56,26 +55,6 @@ export function mountIssuesListApp() {
Vue.use(VueApollo);
Vue.use(VueRouter);
- const resolvers = {
- Mutation: {
- reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => {
- const variables = JSON.parse(serializedVariables);
- const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
-
- const data = produce(sourceData, (draftData) => {
- const issues = draftData[namespace].issues.nodes.slice();
- const issueToMove = issues[oldIndex];
- issues.splice(oldIndex, 1);
- issues.splice(newIndex, 0, issueToMove);
-
- draftData[namespace].issues.nodes = issues;
- });
-
- cache.writeQuery({ query: getIssuesQuery, variables, data });
- },
- },
- };
-
const {
autocompleteAwardEmojisPath,
calendarPath,
@@ -97,7 +76,6 @@ export function mountIssuesListApp() {
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasIterationsFeature,
- hasMultipleIssueAssigneesFeature,
hasScopedLabelsFeature,
importCsvIssuesPath,
initialEmail,
@@ -125,7 +103,7 @@ export function mountIssuesListApp() {
el,
name: 'IssuesListRoot',
apolloProvider: new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: gqlClient,
}),
router: new VueRouter({
base: window.location.pathname,
@@ -148,7 +126,6 @@ export function mountIssuesListApp() {
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
- hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
initialSort,
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
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 df7016aeb74..b447289b425 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -24,6 +24,7 @@ query getIssues(
$crmContactId: String
$crmOrganizationId: String
$not: NegatedIssueFilterInput
+ $or: UnionedIssueFilterInput
$beforeCursor: String
$afterCursor: String
$firstPageSize: Int
@@ -49,6 +50,7 @@ query getIssues(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
@@ -84,6 +86,7 @@ query getIssues(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
index c1aee772167..fdb0eeb5970 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
@@ -17,6 +17,7 @@ query getIssuesCount(
$crmContactId: String
$crmOrganizationId: String
$not: NegatedIssueFilterInput
+ $or: UnionedIssueFilterInput
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
@@ -37,6 +38,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -57,6 +59,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -77,6 +80,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -101,6 +105,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -122,6 +127,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -143,6 +149,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index f02c7a23f51..2f9ab9d62ee 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -5,6 +5,13 @@ import { __ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS_NOT,
+ OPERATOR_OR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
API_PARAM,
@@ -31,12 +38,6 @@ import {
specialFilterValues,
TITLE_ASC,
TITLE_DESC,
- TOKEN_TYPE_ASSIGNEE,
- TOKEN_TYPE_CONFIDENTIAL,
- TOKEN_TYPE_ITERATION,
- TOKEN_TYPE_MILESTONE,
- TOKEN_TYPE_RELEASE,
- TOKEN_TYPE_TYPE,
UPDATED_ASC,
UPDATED_DESC,
URL_PARAM,
@@ -252,20 +253,36 @@ const formatData = (token) => {
export const convertToApiParams = (filterTokens) => {
const params = {};
const not = {};
+ const or = {};
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.forEach((token) => {
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;
+ let obj;
+ if (token.value.operator === OPERATOR_IS_NOT) {
+ obj = not;
+ } else if (token.value.operator === OPERATOR_OR) {
+ obj = or;
+ } else {
+ obj = params;
+ }
const data = formatData(token);
Object.assign(obj, {
[field]: obj[field] ? [obj[field], data].flat() : data,
});
});
- return Object.keys(not).length ? Object.assign(params, { not }) : params;
+ if (Object.keys(not).length) {
+ Object.assign(params, { not });
+ }
+
+ if (Object.keys(or).length) {
+ Object.assign(params, { or });
+ }
+
+ return params;
};
export const convertToUrlParams = (filterTokens) =>
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index dbe634e7295..180dea77003 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -67,7 +67,7 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:enable-autocomplete="enableAutocomplete"
supports-quick-actions
- init-on-autofocus
+ autofocus
@input="$emit('input', $event)"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable"
diff --git a/app/assets/javascripts/issues/show/components/fields/type.vue b/app/assets/javascripts/issues/show/components/fields/type.vue
index 75d0b9e5e76..5695efd7114 100644
--- a/app/assets/javascripts/issues/show/components/fields/type.vue
+++ b/app/assets/javascripts/issues/show/components/fields/type.vue
@@ -42,7 +42,7 @@ export default {
const {
issueState: { issueType },
} = this;
- return capitalize(issueType);
+ return issuableTypes.find((type) => type.value === issueType)?.text || capitalize(issueType);
},
shouldShowIncident() {
return this.issueType === INCIDENT_TYPE || this.canCreateIncident;
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 74d166f82bb..c01de63ced9 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -7,6 +7,7 @@ import {
GlLink,
GlModal,
GlModalDirective,
+ GlTooltipDirective,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
@@ -51,6 +52,7 @@ export default {
},
directives: {
GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
},
mixins: [trackingMixin],
inject: {
@@ -287,12 +289,15 @@ export default {
<gl-dropdown
v-if="hasDesktopDropdown"
+ v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex! gl-ml-3"
icon="ellipsis_v"
category="tertiary"
data-qa-selector="issue_actions_ellipsis_dropdown"
:text="dropdownText"
:text-sr-only="true"
+ :title="dropdownText"
+ :aria-label="dropdownText"
data-testid="desktop-dropdown"
no-caret
right
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index aa7b9805b5f..db846009409 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -1,4 +1,4 @@
-import { __, s__ } from '~/locale';
+import { __, n__, s__ } from '~/locale';
export const timelineTabI18n = Object.freeze({
title: s__('Incident|Timeline'),
@@ -15,6 +15,8 @@ export const timelineFormI18n = Object.freeze({
save: __('Save'),
cancel: __('Cancel'),
description: __('Description'),
+ hint: __('You can enter up to 280 characters'),
+ textRemaining: (count) => n__('%d character remaining', '%d characters remaining', count),
saveAndAdd: s__('Incident|Save and add another event'),
areaLabel: s__('Incident|Timeline text'),
});
@@ -38,3 +40,5 @@ export const timelineItemI18n = Object.freeze({
moreActions: __('More actions'),
timeUTC: __('%{time} UTC'),
});
+
+export const MAX_TEXT_LENGTH = 280;
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index 55cd8b5f606..72dfccca467 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -2,7 +2,7 @@
import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-import { timelineFormI18n } from './constants';
+import { MAX_TEXT_LENGTH, timelineFormI18n } from './constants';
import { getUtcShiftedDate } from './utils';
export default {
@@ -26,6 +26,7 @@ export default {
GlButton,
},
i18n: timelineFormI18n,
+ MAX_TEXT_LENGTH,
directives: {
autofocusonshow,
},
@@ -63,6 +64,9 @@ export default {
};
},
computed: {
+ isTimelineTextValid() {
+ return this.timelineTextCount > 0 && this.timelineTextRemainingCount >= 0;
+ },
occurredAtString() {
const year = this.datePickerInput.getFullYear();
const month = this.datePickerInput.getMonth();
@@ -74,8 +78,11 @@ export default {
return utcDate.toISOString();
},
- hasTimelineText() {
- return this.timelineText.length > 0;
+ timelineTextRemainingCount() {
+ return MAX_TEXT_LENGTH - this.timelineTextCount;
+ },
+ timelineTextCount() {
+ return this.timelineText.length;
},
},
mounted() {
@@ -158,9 +165,21 @@ export default {
dir="auto"
data-supports-quick-actions="false"
:aria-label="$options.i18n.description"
+ aria-describedby="timeline-form-hint"
:placeholder="$options.i18n.areaPlaceholder"
+ :maxlength="$options.MAX_TEXT_LENGTH"
>
</textarea>
+ <div id="timeline-form-hint" class="gl-sr-only">{{ $options.i18n.hint }}</div>
+ <div
+ aria-hidden="true"
+ class="gl-absolute gl-text-gray-500 gl-font-sm gl-line-height-14 gl-right-4 gl-bottom-3"
+ >
+ {{ timelineTextRemainingCount }}
+ </div>
+ <div role="status" class="gl-sr-only">
+ {{ $options.i18n.textRemaining(timelineTextRemainingCount) }}
+ </div>
</template>
</markdown-field>
</gl-form-group>
@@ -171,7 +190,7 @@ export default {
category="primary"
class="gl-mr-3"
data-testid="save-button"
- :disabled="!hasTimelineText"
+ :disabled="!isTimelineTextValid"
:loading="isEventProcessed"
@click="handleSave(false)"
>
@@ -183,7 +202,7 @@ export default {
category="secondary"
class="gl-mr-3 gl-ml-n2"
data-testid="save-and-add-button"
- :disabled="!hasTimelineText"
+ :disabled="!isTimelineTextValid"
:loading="isEventProcessed"
@click="handleSave(true)"
>