diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-16 18:14:18 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-16 18:14:18 +0300 |
commit | acee9d6fb529ca8fb91b2b07bd49bd207df23c51 (patch) | |
tree | ac718adbbcb6078e403a20dd8f9258fee4d48dbc /app/assets/javascripts | |
parent | 77ded523f119396c72e4bcbcd008ff6b84134ef4 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
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); |