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>2023-11-16 18:14:18 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-16 18:14:18 +0300
commitacee9d6fb529ca8fb91b2b07bd49bd207df23c51 (patch)
treeac718adbbcb6078e403a20dd8f9258fee4d48dbc /app/assets/javascripts
parent77ded523f119396c72e4bcbcd008ff6b84134ef4 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue10
-rw-r--r--app/assets/javascripts/issues/list/utils.js36
-rw-r--r--app/assets/javascripts/lib/utils/datetime/constants.js7
-rw-r--r--app/assets/javascripts/lib/utils/datetime/locale_dateformat.js184
-rw-r--r--app/assets/javascripts/lib/utils/datetime/timeago_utility.js51
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js2
-rw-r--r--app/assets/javascripts/pages/admin/deploy_keys/new/index.js3
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue10
11 files changed, 281 insertions, 62 deletions
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 72bb88ef1d5..adc789a205b 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -107,6 +107,7 @@ import {
getInitialPageParams,
getSortKey,
getSortOptions,
+ groupMultiSelectFilterTokens,
isSortKey,
mapWorkItemWidgetsToIssueFields,
updateUpvotesCount,
@@ -384,6 +385,7 @@ export default {
isProject: this.isProject,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
preloadedUsers,
+ multiSelect: this.glFeatures.groupMultiSelectTokens,
},
{
type: TOKEN_TYPE_ASSIGNEE,
@@ -396,6 +398,7 @@ export default {
isProject: this.isProject,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedUsers,
+ multiSelect: this.glFeatures.groupMultiSelectTokens,
},
{
type: TOKEN_TYPE_MILESTONE,
@@ -803,7 +806,12 @@ export default {
sortKey = defaultSortKey;
}
- this.filterTokens = getFilterTokens(window.location.search);
+ const tokens = getFilterTokens(window.location.search);
+ if (this.glFeatures.groupMultiSelectTokens) {
+ this.filterTokens = groupMultiSelectFilterTokens(tokens, this.searchTokens);
+ } else {
+ this.filterTokens = tokens;
+ }
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
this.pageParams = getInitialPageParams(
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 37df0c8f9ff..c1e10285a92 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -7,6 +7,7 @@ import {
OPERATOR_NOT,
OPERATOR_OR,
OPERATOR_AFTER,
+ OPERATORS_TO_GROUP,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -233,6 +234,41 @@ export const getFilterTokens = (locationSearch) =>
};
});
+export function groupMultiSelectFilterTokens(filterTokensToGroup, tokenDefs) {
+ const groupedTokens = [];
+
+ const multiSelectTokenTypes = tokenDefs.filter((t) => t.multiSelect).map((t) => t.type);
+
+ filterTokensToGroup.forEach((token) => {
+ const shouldGroup =
+ OPERATORS_TO_GROUP.includes(token.value.operator) &&
+ multiSelectTokenTypes.includes(token.type);
+
+ if (!shouldGroup) {
+ groupedTokens.push(token);
+ return;
+ }
+
+ const sameTypeAndOperator = (t) =>
+ t.type === token.type && t.value.operator === token.value.operator;
+ const existingToken = groupedTokens.find(sameTypeAndOperator);
+
+ if (!existingToken) {
+ groupedTokens.push({
+ ...token,
+ value: {
+ ...token.value,
+ data: [token.value.data],
+ },
+ });
+ } else if (!existingToken.value.data.includes(token.value.data)) {
+ existingToken.value.data.push(token.value.data);
+ }
+ });
+
+ return groupedTokens;
+}
+
export const isNotEmptySearchToken = (token) =>
!(token.type === FILTERED_SEARCH_TERM && !token.value.data);
diff --git a/app/assets/javascripts/lib/utils/datetime/constants.js b/app/assets/javascripts/lib/utils/datetime/constants.js
deleted file mode 100644
index 869ade45ebd..00000000000
--- a/app/assets/javascripts/lib/utils/datetime/constants.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// Keys for the memoized Intl dateTime formatters
-export const DATE_WITH_TIME_FORMAT = 'DATE_WITH_TIME_FORMAT';
-export const DATE_ONLY_FORMAT = 'DATE_ONLY_FORMAT';
-
-export const DEFAULT_DATE_TIME_FORMAT = DATE_WITH_TIME_FORMAT;
-
-export const DATE_TIME_FORMATS = [DATE_WITH_TIME_FORMAT, DATE_ONLY_FORMAT];
diff --git a/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js b/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js
new file mode 100644
index 00000000000..81922c4da1d
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js
@@ -0,0 +1,184 @@
+import { createDateTimeFormat } from '~/locale';
+
+/**
+ * Format a Date with the help of {@link DateTimeFormat.asDateTime}
+ *
+ * Note: In case you can use localDateFormat.asDateTime directly, please do that.
+ *
+ * @example
+ * localDateFormat[DATE_WITH_TIME_FORMAT].format(date) // returns 'Jul 6, 2020, 2:43 PM'
+ * localDateFormat[DATE_WITH_TIME_FORMAT].formatRange(date, date) // returns 'Jul 6, 2020, 2:45PM – 8:43 PM'
+ */
+export const DATE_WITH_TIME_FORMAT = 'asDateTime';
+/**
+ * Format a Date with the help of {@link DateTimeFormat.asDate}
+ *
+ * Note: In case you can use localDateFormat.asDate directly, please do that.
+ *
+ * @example
+ * localDateFormat[DATE_ONLY_FORMAT].format(date) // returns 'Jul 05, 2023'
+ * localDateFormat[DATE_ONLY_FORMAT].formatRange(date, date) // returns 'Jul 05 - Jul 07, 2023'
+ */
+export const DATE_ONLY_FORMAT = 'asDate';
+export const DEFAULT_DATE_TIME_FORMAT = DATE_WITH_TIME_FORMAT;
+export const DATE_TIME_FORMATS = [DATE_WITH_TIME_FORMAT, DATE_ONLY_FORMAT];
+
+/**
+ * The DateTimeFormat utilities support formatting a number of types,
+ * essentially anything you might use in the `Date` constructor.
+ *
+ * The reason for this is mostly backwards compatibility, as dateformat did the same
+ * https://github.com/felixge/node-dateformat/blob/c53e475891130a1fecd3b0d9bc5ebf3820b31b44/src/dateformat.js#L37-L41
+ *
+ * @typedef {Date|number|string|null} Dateish
+ *
+ */
+/**
+ * @typedef {Object} DateTimeFormatter
+ * @property {function(Dateish): string} format
+ * Formats a single {@link Dateish}
+ * with {@link Intl.DateTimeFormat.format}
+ * @property {function(Dateish, Dateish): string} formatRange
+ * Formats two {@link Dateish} as a range
+ * with {@link Intl.DateTimeFormat.formatRange}
+ */
+
+class DateTimeFormat {
+ #formatters = {};
+
+ /**
+ * Locale aware formatter to display date _and_ time.
+ *
+ * Use this formatter when in doubt.
+ *
+ * @example
+ * // en-US: returns something like Jul 6, 2020, 2:43 PM
+ * // en-GB: returns something like 6 Jul 2020, 14:43
+ * localDateFormat.asDateTime.format(date)
+ *
+ * @returns {DateTimeFormatter}
+ */
+ get asDateTime() {
+ return (
+ this.#formatters[DATE_WITH_TIME_FORMAT] ||
+ this.#createFormatter(DATE_WITH_TIME_FORMAT, {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ hourCycle: DateTimeFormat.#hourCycle,
+ })
+ );
+ }
+
+ /**
+ * Locale aware formatter to display a only the date.
+ *
+ * Use {@link DateTimeFormat.asDateTime} if you also need to display the time.
+ *
+ * @example
+ * // en-US: returns something like Jul 6, 2020
+ * // en-GB: returns something like 6 Jul 2020
+ * localDateFormat.asDate.format(date)
+ *
+ * @example
+ * // en-US: returns something like Jul 6 – 7, 2020
+ * // en-GB: returns something like 6-7 Jul 2020
+ * localDateFormat.asDate.formatRange(date, date2)
+ *
+ * @returns {DateTimeFormatter}
+ */
+ get asDate() {
+ return (
+ this.#formatters[DATE_ONLY_FORMAT] ||
+ this.#createFormatter(DATE_ONLY_FORMAT, {
+ dateStyle: 'medium',
+ })
+ );
+ }
+
+ /**
+ * Resets the memoized formatters
+ *
+ * While this method only seems to be useful for testing right now,
+ * it could also be used in the future to live-preview the formatting
+ * to the user on their settings page.
+ */
+ reset() {
+ this.#formatters = {};
+ }
+
+ /**
+ * This helper function creates formatters in a memoized fashion.
+ *
+ * The first time a getter is called, it will use this helper
+ * to create an {@link Intl.DateTimeFormat} which is used internally.
+ *
+ * We memoize the creation of the formatter, because using one of them
+ * is about 300 faster than creating them.
+ *
+ * @param {string} name (one of {@link DATE_TIME_FORMATS})
+ * @param {Intl.DateTimeFormatOptions} format
+ * @returns {DateTimeFormatter}
+ */
+ #createFormatter(name, format) {
+ const intlFormatter = createDateTimeFormat(format);
+
+ this.#formatters[name] = {
+ format: (date) => intlFormatter.format(DateTimeFormat.castToDate(date)),
+ formatRange: (date1, date2) => {
+ return intlFormatter.formatRange(
+ DateTimeFormat.castToDate(date1),
+ DateTimeFormat.castToDate(date2),
+ );
+ },
+ };
+
+ return this.#formatters[name];
+ }
+
+ /**
+ * Casts a Dateish to a Date.
+ * @param dateish {Dateish}
+ * @returns {Date}
+ */
+ static castToDate(dateish) {
+ const date = dateish instanceof Date ? dateish : new Date(dateish);
+ if (Number.isNaN(date)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Invalid date provided');
+ }
+ return date;
+ }
+
+ /**
+ * Internal method to determine the {@link Intl.Locale.hourCycle} a user prefers.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
+ * @returns {undefined|'h12'|'h23'}
+ */
+ static get #hourCycle() {
+ switch (window.gon?.time_display_format) {
+ case 1:
+ return 'h12';
+ case 2:
+ return 'h23';
+ default:
+ return undefined;
+ }
+ }
+}
+
+/**
+ * A singleton instance of {@link DateTimeFormat}.
+ * This formatting helper respects the user preferences (locale and 12h/24h preference)
+ * and gives an efficient way to format dates and times.
+ *
+ * Each of the supported formatters has support to format a simple date, but also a range.
+ *
+ *
+ * DateTime (showing both date and times):
+ * - {@link DateTimeFormat.asDateTime localeDateFormat.asDateTime} - the default format for date times
+ *
+ * Date (showing date only):
+ * - {@link DateTimeFormat.asDate localeDateFormat.asDate} - the default format for a date
+ */
+export const localeDateFormat = new DateTimeFormat();
diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
index 89170ecc55d..a25acd5c711 100644
--- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
@@ -1,7 +1,7 @@
import * as timeago from 'timeago.js';
-import { languageCode, s__, createDateTimeFormat } from '~/locale';
+import { languageCode, s__ } from '~/locale';
+import { DEFAULT_DATE_TIME_FORMAT, localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
import { formatDate } from './date_format_utility';
-import { DATE_WITH_TIME_FORMAT, DATE_ONLY_FORMAT, DEFAULT_DATE_TIME_FORMAT } from './constants';
/**
* Timeago uses underscores instead of dashes to separate language from country code.
@@ -107,51 +107,10 @@ timeago.register(timeagoLanguageCode, memoizedLocale());
timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
timeago.register(`${timeagoLanguageCode}-duration`, memoizedLocaleDuration());
-const setupAbsoluteFormatters = () => {
- let cache = {};
-
- // Intl.DateTimeFormat options (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options)
- // For hourCycle please check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
- const hourCycle = [undefined, 'h12', 'h23'];
- const formats = {
- [DATE_WITH_TIME_FORMAT]: () => ({
- dateStyle: 'medium',
- timeStyle: 'short',
- hourCycle: hourCycle[window.gon?.time_display_format || 0],
- }),
- [DATE_ONLY_FORMAT]: () => ({ dateStyle: 'medium' }),
- };
-
- return (formatName = DEFAULT_DATE_TIME_FORMAT) => {
- if (cache.time_display_format !== window.gon?.time_display_format) {
- cache = {
- time_display_format: window.gon?.time_display_format,
- };
- }
-
- if (cache[formatName]) {
- return cache[formatName];
- }
-
- let format = formats[formatName] && formats[formatName]();
- if (!format) {
- format = formats[DEFAULT_DATE_TIME_FORMAT]();
- }
-
- const formatter = createDateTimeFormat(format);
-
- cache[formatName] = {
- format(date) {
- return formatter.format(date instanceof Date ? date : new Date(date));
- },
- };
- return cache[formatName];
- };
-};
-const memoizedFormatters = setupAbsoluteFormatters();
-
export const getTimeago = (formatName) =>
- window.gon?.time_display_relative === false ? memoizedFormatters(formatName) : timeago;
+ window.gon?.time_display_relative === false
+ ? localeDateFormat[formatName] ?? localeDateFormat[DEFAULT_DATE_TIME_FORMAT]
+ : timeago;
/**
* For the given elements, sets a tooltip with a formatted date.
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index a6331bc6551..061ce96407e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,6 +1,6 @@
-export * from './datetime/constants';
export * from './datetime/timeago_utility';
export * from './datetime/date_format_utility';
export * from './datetime/date_calculation_utility';
export * from './datetime/pikaday_utility';
export * from './datetime/time_spent_utility';
+export * from './datetime/locale_dateformat';
diff --git a/app/assets/javascripts/pages/admin/deploy_keys/new/index.js b/app/assets/javascripts/pages/admin/deploy_keys/new/index.js
new file mode 100644
index 00000000000..a79542ee6e0
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/deploy_keys/new/index.js
@@ -0,0 +1,3 @@
+import initDatePickers from '~/behaviors/date_picker';
+
+initDatePickers();
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
index 8ac186e292c..5b0266c95df 100644
--- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -128,6 +128,10 @@ export const I18N_ERROR_INCORRECT_TOKEN_LABEL = s__('ServiceDesk|Incorrect verif
export const I18N_ERROR_INCORRECT_TOKEN_DESC = s__(
"ServiceDesk|The received email didn't contain the verification token that was sent to your email address.",
);
+export const I18N_ERROR_READ_TIMEOUT_LABEL = s__('ServiceDesk|Read timeout');
+export const I18N_ERROR_READ_TIMEOUT_DESC = s__(
+ 'ServiceDesk|The SMTP server did not respond in time.',
+);
export const I18N_VERIFICATION_ERRORS = {
smtp_host_issue: {
@@ -150,4 +154,8 @@ export const I18N_VERIFICATION_ERRORS = {
label: I18N_ERROR_INCORRECT_TOKEN_LABEL,
description: I18N_ERROR_INCORRECT_TOKEN_DESC,
},
+ read_timeout: {
+ label: I18N_ERROR_READ_TIMEOUT_LABEL,
+ description: I18N_ERROR_READ_TIMEOUT_DESC,
+ },
};
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index c698b94749d..b09e8323ba2 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -31,6 +31,8 @@ export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT];
export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR];
export const OPERATORS_AFTER_BEFORE = [...OPERATORS_AFTER, ...OPERATORS_BEFORE];
+export const OPERATORS_TO_GROUP = [OPERATOR_OR, OPERATOR_NOT];
+
export const OPTION_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
export const OPTION_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
export const OPTION_CURRENT = { value: FILTER_CURRENT, text: __('Current') };
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 3857dd9c55d..5d72ac34e73 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -11,7 +11,13 @@ import { debounce, last } from 'lodash';
import { stripQuotes } from '~/lib/utils/text_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT, OPERATOR_OR } from '../constants';
+import {
+ DEBOUNCE_DELAY,
+ FILTERS_NONE_ANY,
+ OPERATOR_NOT,
+ OPERATOR_OR,
+ OPERATORS_TO_GROUP,
+} from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
@@ -102,7 +108,7 @@ export default {
},
activeTokenValue() {
const data =
- this.glFeatures.groupMultiSelectTokens && Array.isArray(this.value.data)
+ this.multiSelectEnabled && Array.isArray(this.value.data)
? last(this.value.data)
: this.value.data;
return this.getActiveTokenValue(this.suggestions, data);
@@ -153,6 +159,22 @@ export default {
? this.activeTokenValue[this.searchBy]
: undefined;
},
+ multiSelectEnabled() {
+ return (
+ this.config.multiSelect &&
+ this.glFeatures.groupMultiSelectTokens &&
+ OPERATORS_TO_GROUP.includes(this.value.operator)
+ );
+ },
+ validatedConfig() {
+ if (this.config.multiSelect && !this.multiSelectEnabled) {
+ return {
+ ...this.config,
+ multiSelect: false,
+ };
+ }
+ return this.config;
+ },
},
watch: {
active: {
@@ -199,7 +221,7 @@ export default {
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(selectedValue) {
- if (this.glFeatures.groupMultiSelectTokens) {
+ if (this.multiSelectEnabled) {
this.$emit('token-selected', selectedValue);
}
@@ -228,7 +250,7 @@ export default {
<template>
<gl-filtered-search-token
- :config="config"
+ :config="validatedConfig"
:value="value"
:active="active"
:multi-select-values="multiSelectValues"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index c5326ead60d..87e295d00dd 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -7,7 +7,7 @@ import { __ } from '~/locale';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { OPTIONS_NONE_ANY } from '../constants';
+import { OPERATORS_TO_GROUP, OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -57,7 +57,11 @@ export default {
return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm;
},
multiSelectEnabled() {
- return this.config.multiSelect && this.glFeatures.groupMultiSelectTokens;
+ return (
+ this.config.multiSelect &&
+ this.glFeatures.groupMultiSelectTokens &&
+ OPERATORS_TO_GROUP.includes(this.value.operator)
+ );
},
},
watch: {
@@ -94,7 +98,7 @@ export default {
return user?.avatarUrl || user?.avatar_url;
},
displayNameFor(username) {
- return this.getActiveUser(this.allUsers, username)?.name || `@${username}`;
+ return this.getActiveUser(this.allUsers, username)?.name || username;
},
avatarFor(username) {
const user = this.getActiveUser(this.allUsers, username);