diff options
30 files changed, 82 insertions, 2098 deletions
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js index aceae188b73..da5fb831ae5 100644 --- a/app/assets/javascripts/lib/utils/constants.js +++ b/app/assets/javascripts/lib/utils/constants.js @@ -3,15 +3,6 @@ export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250; export const THOUSAND = 1000; export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80; export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12; - -export const DATETIME_RANGE_TYPES = { - fixed: 'fixed', - anchored: 'anchored', - rolling: 'rolling', - open: 'open', - invalid: 'invalid', -}; - export const BV_SHOW_MODAL = 'bv::show::modal'; export const BV_HIDE_MODAL = 'bv::hide::modal'; export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip'; diff --git a/app/assets/javascripts/lib/utils/datetime_range.js b/app/assets/javascripts/lib/utils/datetime_range.js index 39fb26a95b7..f0a03874949 100644 --- a/app/assets/javascripts/lib/utils/datetime_range.js +++ b/app/assets/javascripts/lib/utils/datetime_range.js @@ -1,25 +1,4 @@ -import { pick, omit, isEqual, isEmpty } from 'lodash'; import dateformat from '~/lib/dateformat'; -import { DATETIME_RANGE_TYPES } from './constants'; -import { secondsToMilliseconds } from './datetime_utility'; - -const MINIMUM_DATE = new Date(0); - -const DEFAULT_DIRECTION = 'before'; - -const durationToMillis = (duration) => { - if (Object.entries(duration).length === 1 && Number.isFinite(duration.seconds)) { - return secondsToMilliseconds(duration.seconds); - } - // eslint-disable-next-line @gitlab/require-i18n-strings - throw new Error('Invalid duration: only `seconds` is supported'); -}; - -const dateMinusDuration = (date, duration) => new Date(date.getTime() - durationToMillis(duration)); - -const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration)); - -const isValidDuration = (duration) => Boolean(duration && Number.isFinite(duration.seconds)); export const isValidDateString = (dateString) => { if (typeof dateString !== 'string' || !dateString.trim()) { @@ -38,291 +17,3 @@ export const isValidDateString = (dateString) => { } return !Number.isNaN(Date.parse(isoFormatted)); }; - -const handleRangeDirection = ({ direction = DEFAULT_DIRECTION, anchorDate, minDate, maxDate }) => { - let startDate; - let endDate; - - if (direction === DEFAULT_DIRECTION) { - startDate = minDate; - endDate = anchorDate; - } else { - startDate = anchorDate; - endDate = maxDate; - } - - return { - startDate, - endDate, - }; -}; - -/** - * Converts a fixed range to a fixed range - * @param {Object} fixedRange - A range with fixed start and - * end (e.g. "midnight January 1st 2020 to midday January31st 2020") - */ -const convertFixedToFixed = ({ start, end }) => ({ - start, - end, -}); - -/** - * Converts an anchored range to a fixed range - * @param {Object} anchoredRange - A duration of time - * relative to a fixed point in time (e.g., "the 30 minutes - * before midnight January 1st 2020", or "the 2 days - * after midday on the 11th of May 2019") - */ -const convertAnchoredToFixed = ({ anchor, duration, direction }) => { - const anchorDate = new Date(anchor); - - const { startDate, endDate } = handleRangeDirection({ - minDate: dateMinusDuration(anchorDate, duration), - maxDate: datePlusDuration(anchorDate, duration), - direction, - anchorDate, - }); - - return { - start: startDate.toISOString(), - end: endDate.toISOString(), - }; -}; - -/** - * Converts a rolling change to a fixed range - * - * @param {Object} rollingRange - A time range relative to - * now (e.g., "last 2 minutes", or "next 2 days") - */ -const convertRollingToFixed = ({ duration, direction }) => { - // Use Date.now internally for easier mocking in tests - const now = new Date(Date.now()); - - return convertAnchoredToFixed({ - duration, - direction, - anchor: now.toISOString(), - }); -}; - -/** - * Converts an open range to a fixed range - * - * @param {Object} openRange - A time range relative - * to an anchor (e.g., "before midnight on the 1st of - * January 2020", or "after midday on the 11th of May 2019") - */ -const convertOpenToFixed = ({ anchor, direction }) => { - // Use Date.now internally for easier mocking in tests - const now = new Date(Date.now()); - - const { startDate, endDate } = handleRangeDirection({ - minDate: MINIMUM_DATE, - maxDate: now, - direction, - anchorDate: new Date(anchor), - }); - - return { - start: startDate.toISOString(), - end: endDate.toISOString(), - }; -}; - -/** - * Handles invalid date ranges - */ -const handleInvalidRange = () => { - // eslint-disable-next-line @gitlab/require-i18n-strings - throw new Error('The input range does not have the right format.'); -}; - -const handlers = { - invalid: handleInvalidRange, - fixed: convertFixedToFixed, - anchored: convertAnchoredToFixed, - rolling: convertRollingToFixed, - open: convertOpenToFixed, -}; - -/** - * Validates and returns the type of range - * - * @param {Object} Date time range - * @returns {String} `key` value for one of the handlers - */ -export function getRangeType(range) { - const { start, end, anchor, duration } = range; - - if ((start || end) && !anchor && !duration) { - return isValidDateString(start) && isValidDateString(end) - ? DATETIME_RANGE_TYPES.fixed - : DATETIME_RANGE_TYPES.invalid; - } - if (anchor && duration) { - return isValidDateString(anchor) && isValidDuration(duration) - ? DATETIME_RANGE_TYPES.anchored - : DATETIME_RANGE_TYPES.invalid; - } - if (duration && !anchor) { - return isValidDuration(duration) ? DATETIME_RANGE_TYPES.rolling : DATETIME_RANGE_TYPES.invalid; - } - if (anchor && !duration) { - return isValidDateString(anchor) ? DATETIME_RANGE_TYPES.open : DATETIME_RANGE_TYPES.invalid; - } - return DATETIME_RANGE_TYPES.invalid; -} - -/** - * convertToFixedRange Transforms a `range of time` into a `fixed range of time`. - * - * The following types of a `ranges of time` can be represented: - * - * Fixed Range: A range with fixed start and end (e.g. "midnight January 1st 2020 to midday January 31st 2020") - * Anchored Range: A duration of time relative to a fixed point in time (e.g., "the 30 minutes before midnight January 1st 2020", or "the 2 days after midday on the 11th of May 2019") - * Rolling Range: A time range relative to now (e.g., "last 2 minutes", or "next 2 days") - * Open Range: A time range relative to an anchor (e.g., "before midnight on the 1st of January 2020", or "after midday on the 11th of May 2019") - * - * @param {Object} dateTimeRange - A Time Range representation - * It contains the data needed to create a fixed time range plus - * a label (recommended) to indicate the range that is covered. - * - * A definition via a TypeScript notation is presented below: - * - * - * type Duration = { // A duration of time, always in seconds - * seconds: number; - * } - * - * type Direction = 'before' | 'after'; // Direction of time relative to an anchor - * - * type FixedRange = { - * start: ISO8601; - * end: ISO8601; - * label: string; - * } - * - * type AnchoredRange = { - * anchor: ISO8601; - * duration: Duration; - * direction: Direction; // defaults to 'before' - * label: string; - * } - * - * type RollingRange = { - * duration: Duration; - * direction: Direction; // defaults to 'before' - * label: string; - * } - * - * type OpenRange = { - * anchor: ISO8601; - * direction: Direction; // defaults to 'before' - * label: string; - * } - * - * type DateTimeRange = FixedRange | AnchoredRange | RollingRange | OpenRange; - * - * - * @returns {FixedRange} An object with a start and end in ISO8601 format. - */ -export const convertToFixedRange = (dateTimeRange) => - handlers[getRangeType(dateTimeRange)](dateTimeRange); - -/** - * Returns a copy of the object only with time range - * properties relevant to time range calculation. - * - * Filtered properties are: - * - 'start' - * - 'end' - * - 'anchor' - * - 'duration' - * - 'direction': if direction is already the default, its removed. - * - * @param {Object} timeRange - A time range object - * @returns Copy of time range - */ -const pruneTimeRange = (timeRange) => { - const res = pick(timeRange, ['start', 'end', 'anchor', 'duration', 'direction']); - if (res.direction === DEFAULT_DIRECTION) { - return omit(res, 'direction'); - } - return res; -}; - -/** - * Returns true if the time ranges are equal according to - * the time range calculation properties - * - * @param {Object} timeRange - A time range object - * @param {Object} other - Time range object to compare with. - * @returns true if the time ranges are equal, false otherwise - */ -export const isEqualTimeRanges = (timeRange, other) => { - const tr1 = pruneTimeRange(timeRange); - const tr2 = pruneTimeRange(other); - return isEqual(tr1, tr2); -}; - -/** - * Searches for a time range in a array of time ranges using - * only the properies relevant to time ranges calculation. - * - * @param {Object} timeRange - Time range to search (needle) - * @param {Array} timeRanges - Array of time tanges (haystack) - */ -export const findTimeRange = (timeRange, timeRanges) => - timeRanges.find((element) => isEqualTimeRanges(element, timeRange)); - -// Time Ranges as URL Parameters Utils - -/** - * List of possible time ranges parameters - */ -export const timeRangeParamNames = ['start', 'end', 'anchor', 'duration_seconds', 'direction']; - -/** - * Converts a valid time range to a flat key-value pairs object. - * - * Duration is flatted to avoid having nested objects. - * - * @param {Object} A time range - * @returns key-value pairs object that can be used as parameters in a URL. - */ -export const timeRangeToParams = (timeRange) => { - let params = pruneTimeRange(timeRange); - if (timeRange.duration) { - const durationParms = {}; - Object.keys(timeRange.duration).forEach((key) => { - durationParms[`duration_${key}`] = timeRange.duration[key].toString(); - }); - params = { ...durationParms, ...params }; - params = omit(params, 'duration'); - } - return params; -}; - -/** - * Converts a valid set of flat params to a time range object - * - * Parameters that are not part of time range object are ignored. - * - * @param {params} params - key-value pairs object. - */ -export const timeRangeFromParams = (params) => { - const timeRangeParams = pick(params, timeRangeParamNames); - let range = Object.entries(timeRangeParams).reduce((acc, [key, val]) => { - // unflatten duration - if (key.startsWith('duration_')) { - acc.duration = acc.duration || {}; - acc.duration[key.slice('duration_'.length)] = parseInt(val, 10); - return acc; - } - return { [key]: val, ...acc }; - }, {}); - range = pruneTimeRange(range); - return !isEmpty(range) ? range : null; -}; diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue index 65cec94253c..02488e99c0e 100644 --- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue @@ -104,10 +104,8 @@ export default { }, }, mounted() { - if (this.glFeatures.superSidebarFlyoutMenus) { - this.decideFlyoutState(); - window.addEventListener('resize', this.decideFlyoutState); - } + this.decideFlyoutState(); + window.addEventListener('resize', this.decideFlyoutState); }, beforeDestroy() { window.removeEventListener('resize', this.decideFlyoutState); diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue deleted file mode 100644 index d8a2789a419..00000000000 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue +++ /dev/null @@ -1,283 +0,0 @@ -<script> -import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui'; -import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range'; -import { __, sprintf } from '~/locale'; - -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import DateTimePickerInput from './date_time_picker_input.vue'; -import { - defaultTimeRanges, - defaultTimeRange, - isValidInputString, - inputStringToIsoDate, - isoDateToInputString, -} from './date_time_picker_lib'; - -const events = { - input: 'input', - invalid: 'invalid', -}; - -export default { - components: { - GlIcon, - GlButton, - GlDropdown, - GlDropdownItem, - GlFormGroup, - TooltipOnTruncate, - DateTimePickerInput, - }, - props: { - value: { - type: Object, - required: false, - default: () => defaultTimeRange, - }, - options: { - type: Array, - required: false, - default: () => defaultTimeRanges, - }, - customEnabled: { - type: Boolean, - required: false, - default: true, - }, - utc: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - timeRange: this.value, - - /** - * Valid start iso date string, null if not valid value - */ - startDate: null, - /** - * Invalid start date string as input by the user - */ - startFallbackVal: '', - - /** - * Valid end iso date string, null if not valid value - */ - endDate: null, - /** - * Invalid end date string as input by the user - */ - endFallbackVal: '', - }; - }, - computed: { - startInputValid() { - return isValidInputString(this.startDate); - }, - endInputValid() { - return isValidInputString(this.endDate); - }, - isValid() { - return this.startInputValid && this.endInputValid; - }, - - startInput: { - get() { - return this.dateToInput(this.startDate) || this.startFallbackVal; - }, - set(val) { - try { - this.startDate = this.inputToDate(val); - this.startFallbackVal = null; - } catch (e) { - this.startDate = null; - this.startFallbackVal = val; - } - this.timeRange = null; - }, - }, - endInput: { - get() { - return this.dateToInput(this.endDate) || this.endFallbackVal; - }, - set(val) { - try { - this.endDate = this.inputToDate(val); - this.endFallbackVal = null; - } catch (e) { - this.endDate = null; - this.endFallbackVal = val; - } - this.timeRange = null; - }, - }, - - timeWindowText() { - try { - const timeRange = findTimeRange(this.value, this.options); - if (timeRange) { - return timeRange.label; - } - - const { start, end } = convertToFixedRange(this.value); - if (isValidInputString(start) && isValidInputString(end)) { - return sprintf(__('%{start} to %{end}'), { - start: this.stripZerosInDateTime(this.dateToInput(start)), - end: this.stripZerosInDateTime(this.dateToInput(end)), - }); - } - } catch { - return __('Invalid date range'); - } - return ''; - }, - - customLabel() { - if (this.utc) { - return __('Custom range (UTC)'); - } - return __('Custom range'); - }, - }, - watch: { - value(newValue) { - const { start, end } = convertToFixedRange(newValue); - this.timeRange = this.value; - this.startDate = start; - this.endDate = end; - }, - }, - mounted() { - try { - const { start, end } = convertToFixedRange(this.timeRange); - this.startDate = start; - this.endDate = end; - } catch { - // when dates cannot be parsed, emit error. - this.$emit(events.invalid); - } - - // Validate on mounted, and trigger an update if needed - if (!this.isValid) { - this.$emit(events.invalid); - } - }, - methods: { - dateToInput(date) { - if (date === null) { - return null; - } - return isoDateToInputString(date, this.utc); - }, - inputToDate(value) { - return inputStringToIsoDate(value, this.utc); - }, - stripZerosInDateTime(str = '') { - return str.replace(' 00:00:00', ''); - }, - closeDropdown() { - this.$refs.dropdown.hide(); - }, - isOptionActive(option) { - return isEqualTimeRanges(option, this.timeRange); - }, - setQuickRange(option) { - this.timeRange = option; - this.$emit(events.input, this.timeRange); - }, - setFixedRange() { - this.timeRange = convertToFixedRange({ - start: this.startDate, - end: this.endDate, - }); - this.$emit(events.input, this.timeRange); - }, - }, -}; -</script> -<template> - <tooltip-on-truncate - :title="timeWindowText" - :truncate-target="(elem) => elem.querySelector('.gl-dropdown-toggle-text')" - placement="top" - class="d-inline-block" - > - <gl-dropdown - ref="dropdown" - :text="timeWindowText" - v-bind="$attrs" - class="date-time-picker w-100" - menu-class="date-time-picker-menu" - toggle-class="date-time-picker-toggle text-truncate" - > - <template #button-content> - <span class="gl-flex-grow-1 text-truncate">{{ timeWindowText }}</span> - <span v-if="utc" class="gl-text-gray-500 gl-font-weight-bold gl-font-sm">{{ - __('UTC') - }}</span> - <gl-icon class="gl-dropdown-caret" name="chevron-down" /> - </template> - - <div class="d-flex justify-content-between gl-p-2"> - <gl-form-group - v-if="customEnabled" - :label="customLabel" - label-for="custom-from-time" - label-class="gl-pb-2" - class="custom-time-range-form-group col-md-7 gl-pl-2 gl-pr-0 m-0" - > - <div class="gl-pt-3"> - <date-time-picker-input - id="custom-time-from" - v-model="startInput" - :label="__('From')" - :state="startInputValid" - /> - <date-time-picker-input - id="custom-time-to" - v-model="endInput" - :label="__('To')" - :state="endInputValid" - /> - </div> - <gl-form-group> - <gl-button data-testid="cancelButton" @click="closeDropdown">{{ - __('Cancel') - }}</gl-button> - <gl-button - variant="confirm" - category="primary" - :disabled="!isValid" - @click="setFixedRange()" - > - {{ __('Apply') }} - </gl-button> - </gl-form-group> - </gl-form-group> - <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-px-2 m-0"> - <template #label> - <span class="gl-pl-7">{{ __('Quick range') }}</span> - </template> - - <gl-dropdown-item - v-for="(option, index) in options" - :key="index" - :active="isOptionActive(option)" - active-class="active" - @click="setQuickRange(option)" - > - <gl-icon - name="mobile-issue-close" - class="align-bottom" - :class="{ invisible: !isOptionActive(option) }" - /> - {{ option.label }} - </gl-dropdown-item> - </gl-form-group> - </div> - </gl-dropdown> - </tooltip-on-truncate> -</template> diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue deleted file mode 100644 index 190d4e1f104..00000000000 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue +++ /dev/null @@ -1,77 +0,0 @@ -<script> -import { GlFormGroup, GlFormInput } from '@gitlab/ui'; -import { uniqueId } from 'lodash'; -import { __, sprintf } from '~/locale'; -import { dateFormats } from './date_time_picker_lib'; - -const inputGroupText = { - invalidFeedback: sprintf(__('Format: %{dateFormat}'), { - dateFormat: dateFormats.inputFormat, - }), - placeholder: dateFormats.inputFormat, -}; - -export default { - components: { - GlFormGroup, - GlFormInput, - }, - props: { - state: { - default: null, - required: true, - validator: (prop) => typeof prop === 'boolean' || prop === null, - }, - value: { - default: null, - required: false, - validator: (prop) => typeof prop === 'string' || prop === null, - }, - label: { - type: String, - default: '', - required: true, - }, - id: { - type: String, - required: false, - default: () => uniqueId('dateTimePicker_'), - }, - }, - data() { - return { - inputGroupText, - }; - }, - computed: { - invalidFeedback() { - return this.state ? '' : this.inputGroupText.invalidFeedback; - }, - inputState() { - // When the state is valid we want to show no - // green outline. Hence passing null and not true. - if (this.state === true) { - return null; - } - return this.state; - }, - }, - methods: { - onInputBlur(e) { - this.$emit('input', e.target.value.trim() || null); - }, - }, -}; -</script> - -<template> - <gl-form-group :label="label" label-size="sm" :label-for="id" :invalid-feedback="invalidFeedback"> - <gl-form-input - :id="id" - :value="value" - :state="inputState" - :placeholder="inputGroupText.placeholder" - @blur="onInputBlur" - /> - </gl-form-group> -</template> diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js deleted file mode 100644 index 38b1a587b34..00000000000 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js +++ /dev/null @@ -1,91 +0,0 @@ -import dateformat from '~/lib/dateformat'; -import { __ } from '~/locale'; - -/** - * Default time ranges for the date picker. - * @see app/assets/javascripts/lib/utils/datetime_range.js - */ -export const defaultTimeRanges = [ - { - duration: { seconds: 60 * 30 }, - label: __('30 minutes'), - }, - { - duration: { seconds: 60 * 60 * 3 }, - label: __('3 hours'), - }, - { - duration: { seconds: 60 * 60 * 8 }, - label: __('8 hours'), - default: true, - }, - { - duration: { seconds: 60 * 60 * 24 * 1 }, - label: __('1 day'), - }, -]; - -export const defaultTimeRange = defaultTimeRanges.find((tr) => tr.default); - -export const dateFormats = { - /** - * Format used by users to input dates - * - * Note: Should be a format that can be parsed by Date.parse. - */ - inputFormat: 'yyyy-mm-dd HH:MM:ss', - /** - * Format used to strip timezone from inputs - */ - stripTimezoneFormat: "yyyy-mm-dd'T'HH:MM:ss'Z'", -}; - -/** - * Returns true if the date can be parsed succesfully after - * being typed by a user. - * - * It allows some ambiguity so validation is not strict. - * - * @param {string} value - Value as typed by the user - * @returns true if the value can be parsed as a valid date, false otherwise - */ -export const isValidInputString = (value) => { - try { - // dateformat throws error that can be caught. - // This is better than using `new Date()` - if (value && value.trim()) { - dateformat(value, 'isoDateTime'); - return true; - } - return false; - } catch (e) { - return false; - } -}; - -/** - * Convert the input in time picker component to an ISO date. - * - * @param {string} value - * @param {Boolean} utc - If true, it forces the date to by - * formatted using UTC format, ignoring the local time. - * @returns {Date} - */ -export const inputStringToIsoDate = (value, utc = false) => { - let date = new Date(value); - if (utc) { - // Forces date to be interpreted as UTC by stripping the timezone - // by formatting to a string with 'Z' and skipping timezone - date = dateformat(date, dateFormats.stripTimezoneFormat); - } - return dateformat(date, 'isoUtcDateTime'); -}; - -/** - * Converts a iso date string to a formatted string for the Time picker component. - * - * @param {String} ISO Formatted date - * @returns {string} - */ -export const isoDateToInputString = (date, utc = false) => - dateformat(date, dateFormats.inputFormat, utc); diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue deleted file mode 100644 index c0aef42b0f2..00000000000 --- a/app/assets/javascripts/vue_shared/components/split_button.vue +++ /dev/null @@ -1,85 +0,0 @@ -<script> -import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui'; -import { isString } from 'lodash'; - -const isValidItem = (item) => - isString(item.eventName) && isString(item.title) && isString(item.description); - -export default { - components: { - GlDropdown, - GlDropdownDivider, - GlDropdownItem, - }, - - props: { - actionItems: { - type: Array, - required: true, - validator(value) { - return value.length > 1 && value.every(isValidItem); - }, - }, - menuClass: { - type: String, - required: false, - default: '', - }, - variant: { - type: String, - required: false, - default: 'default', - }, - }, - - data() { - return { - selectedItem: this.actionItems[0], - }; - }, - - computed: { - dropdownToggleText() { - return this.selectedItem.title; - }, - }, - - methods: { - triggerEvent() { - this.$emit(this.selectedItem.eventName); - }, - changeSelectedItem(item) { - this.selectedItem = item; - this.$emit('change', item); - }, - }, -}; -</script> - -<template> - <gl-dropdown - :menu-class="menuClass" - split - :text="dropdownToggleText" - :variant="variant" - v-bind="$attrs" - @click="triggerEvent" - > - <template v-for="(item, itemIndex) in actionItems"> - <gl-dropdown-item - :key="item.eventName" - is-check-item - :is-checked="selectedItem === item" - @click="changeSelectedItem(item)" - > - <strong>{{ item.title }}</strong> - <div>{{ item.description }}</div> - </gl-dropdown-item> - - <gl-dropdown-divider - v-if="itemIndex < actionItems.length - 1" - :key="`${item.eventName}-divider`" - /> - </template> - </gl-dropdown> -</template> diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index b02a636ff74..0fe31d74ae8 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -20,22 +20,6 @@ module IssuableCollections set_pagination return if redirect_out_of_range(@issuables, @total_pages) - - if params[:label_name].present? && @project - labels_params = { project_id: @project.id, title: params[:label_name] } - @labels = LabelsFinder.new(current_user, labels_params).execute - end - - @users = [] - if params[:assignee_id].present? - assignee = User.find_by_id(params[:assignee_id]) - @users.push(assignee) if assignee - end - - if params[:author_id].present? - author = User.find_by_id(params[:author_id]) - @users.push(author) if author - end end def set_pagination diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 86e914f3447..9abcc108ace 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -113,12 +113,6 @@ class Projects::IssuesController < Projects::ApplicationController respond_to do |format| format.html format.atom { render layout: 'xml' } - format.json do - render json: { - html: view_to_html_string("projects/issues/_issues"), - labels: @labels.as_json(methods: :text_color) - } - end end end @@ -281,7 +275,6 @@ class Projects::IssuesController < Projects::ApplicationController def service_desk @issues = @issuables - @users.push(Users::Internal.support_bot) end protected diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d5a2213eda9..53fd7256b19 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -104,11 +104,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo respond_to do |format| format.html format.atom { render layout: 'xml' } - format.json do - render json: { - html: view_to_html_string("projects/merge_requests/_merge_requests") - } - end end end diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index f78ddade822..624f5a48c3a 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -19,6 +19,8 @@ = s_('AdminSettings|Restricted visibility levels') %small.form-text.text-gl-muted = s_('AdminSettings|Prevent non-administrators from using the selected visibility levels for groups, projects and snippets.') + = s_('AdminSettings|The selected level must be different from the selected default group and project visibility.') + = link_to _('Learn more.'), help_page_path('administration/settings/visibility_and_access_controls', anchor: 'restrict-visibility-levels'), target: '_blank', rel: 'noopener noreferrer' = hidden_field_tag 'application_setting[restricted_visibility_levels][]' .gl-form-checkbox-group - restricted_level_checkboxes(f).each do |checkbox| diff --git a/config/feature_flags/development/prevent_visibility_restriction.yml b/config/feature_flags/development/prevent_visibility_restriction.yml index 9f5b82b6f69..09b082952c3 100644 --- a/config/feature_flags/development/prevent_visibility_restriction.yml +++ b/config/feature_flags/development/prevent_visibility_restriction.yml @@ -5,4 +5,4 @@ rollout_issue_url: milestone: '16.3' type: development group: group::acquisition -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/super_sidebar_flyout_menus.yml b/config/feature_flags/development/super_sidebar_flyout_menus.yml deleted file mode 100644 index 6bec0ef60df..00000000000 --- a/config/feature_flags/development/super_sidebar_flyout_menus.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: super_sidebar_flyout_menus -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124863 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/417237 -milestone: '16.2' -type: development -group: group::foundations -default_enabled: false diff --git a/db/docs/batched_background_migrations/backfill_workspace_personal_access_token.yml b/db/docs/batched_background_migrations/backfill_workspace_personal_access_token.yml new file mode 100644 index 00000000000..53433fbb1c7 --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_workspace_personal_access_token.yml @@ -0,0 +1,5 @@ +migration_job_name: BackfillWorkspacePersonalAccessToken +description: Create personal access token for workspaces without one +feature_category: remote_development +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131516 +milestone: 16.4 diff --git a/db/post_migrate/20230909120000_queue_backfill_workspace_personal_access_token.rb b/db/post_migrate/20230909120000_queue_backfill_workspace_personal_access_token.rb new file mode 100644 index 00000000000..5a746d97493 --- /dev/null +++ b/db/post_migrate/20230909120000_queue_backfill_workspace_personal_access_token.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class QueueBackfillWorkspacePersonalAccessToken < Gitlab::Database::Migration[2.1] + MIGRATION = "BackfillWorkspacePersonalAccessToken" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 100 + SUB_BATCH_SIZE = 10 + + restrict_gitlab_migration gitlab_schema: :gitlab_main + disable_ddl_transaction! + + def up + queue_batched_background_migration( + MIGRATION, + :workspaces, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :workspaces, :id, []) + end +end diff --git a/db/schema_migrations/20230909120000 b/db/schema_migrations/20230909120000 new file mode 100644 index 00000000000..414065b3693 --- /dev/null +++ b/db/schema_migrations/20230909120000 @@ -0,0 +1 @@ +75402594bdc333a34f7b49db4d5008fddad10f346dd15d65e4552cac20b442fb
\ No newline at end of file diff --git a/doc/administration/settings/visibility_and_access_controls.md b/doc/administration/settings/visibility_and_access_controls.md index d8dddfe281b..93dbfaaf990 100644 --- a/doc/administration/settings/visibility_and_access_controls.md +++ b/doc/administration/settings/visibility_and_access_controls.md @@ -132,6 +132,9 @@ To set the default [visibility levels for new projects](../../user/public_access - **Public** - The project can be accessed without any authentication. 1. Select **Save changes**. +For more details on project visibility, see +[Project visibility](../../user/public_access.md). + ## Configure snippet visibility defaults To set the default visibility levels for new [snippets](../../user/snippets.md): @@ -145,7 +148,7 @@ To set the default visibility levels for new [snippets](../../user/snippets.md): 1. Select **Save changes**. For more details on snippet visibility, read -[Project visibility](../../user/public_access.md). +[Snippet visibility](../../user/snippets.md). ## Configure group visibility defaults @@ -167,6 +170,9 @@ For more details on group visibility, see ## Restrict visibility levels +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124649) in GitLab 16.3 to prevent restricting default project and group visibility, [with a flag](../feature_flags.md) named `prevent_visibility_restriction`. Disabled by default. +> - `prevent_visibility_restriction` [enabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131203) by default in GitLab 16.4. + When restricting visibility levels, consider how these restrictions interact with permissions for subgroups and projects that inherit their visibility from the item you're changing. @@ -191,8 +197,8 @@ To restrict visibility levels for groups, projects, snippets, and selected pages - Only administrators are able to create private groups, projects, and snippets. 1. Select **Save changes**. -For more details on project visibility, see -[Project visibility](../../user/public_access.md). +NOTE: +You cannot select the restricted default visibility level for new projects and groups. ## Configure enabled Git access protocols diff --git a/doc/api/settings.md b/doc/api/settings.md index a8c3afa3305..5182969f365 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -347,10 +347,10 @@ listed in the descriptions of the relevant settings. | `default_branch_name` | string | no | [Instance-level custom initial branch name](../user/project/repository/branches/default.md#instance-level-custom-initial-branch-name). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225258) in GitLab 13.2. | | `default_branch_protection` | integer | no | Determine if developers can push to the default branch. Can take: `0` _(not protected, both users with the Developer role or Maintainer role can push new commits and force push)_, `1` _(partially protected, users with the Developer role or Maintainer role can push new commits, but cannot force push)_ or `2` _(fully protected, users with the Developer or Maintainer role cannot push new commits, but users with the Developer or Maintainer role can; no one can force push)_ as a parameter. Default is `2`. | | `default_ci_config_path` | string | no | Default CI/CD configuration file and path for new projects (`.gitlab-ci.yml` if not set). | -| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | +| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131203) in GitLab 16.4: cannot be set to any levels in `restricted_visibility_levels`.| | `default_preferred_language` | string | no | Default preferred language for users who are not logged in. | | `default_project_creation` | integer | no | Default project creation protection. Can take: `0` _(No one)_, `1` _(Maintainers)_ or `2` _(Developers + Maintainers)_| -| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | +| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131203) in GitLab 16.4: cannot be set to any levels in `restricted_visibility_levels`.| | `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. | | `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_syntax_highlighting_theme` | integer | no | Default syntax highlighting theme for users who are new or not signed in. See [IDs of available themes](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/themes.rb#L16). | @@ -528,7 +528,7 @@ listed in the descriptions of the relevant settings. | `repository_storages` | array of strings | no | (GitLab 13.0 and earlier) List of names of enabled storage paths, taken from `gitlab.yml`. New projects are created in one of these stores, chosen at random. | | `require_admin_approval_after_user_signup` | boolean | no | When enabled, any user that signs up for an account using the registration form is placed under a **Pending approval** state and has to be explicitly [approved](../administration/moderate_users.md) by an administrator. | | `require_two_factor_authentication` | boolean | no | (**If enabled, requires:** `two_factor_grace_period`) Require all users to set up Two-factor authentication. | -| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-Administrator users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction. | +| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-Administrator users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction.[Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131203) in GitLab 16.4: cannot select levels that are set as `default_project_visibility` and `default_group_visibility`. | | `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. | | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes. | | `security_policy_global_group_approvers_enabled` | boolean | no | Whether to look up scan result policy approval groups globally or within project hierarchies. | diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 193320f48d5..c7633b2c664 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -167,6 +167,8 @@ On self-managed GitLab, by default this feature is available. On GitLab.com this You can use Cisco Duo as an OTP provider in GitLab. +DUO® is a registered trademark of Cisco Systems, Inc., and/or its affiliates in the United States and certain other countries. + #### Prerequisites To use Cisco Duo as an OTP provider: diff --git a/lib/gitlab/background_migration/backfill_workspace_personal_access_token.rb b/lib/gitlab/background_migration/backfill_workspace_personal_access_token.rb new file mode 100644 index 00000000000..f71759dc8dd --- /dev/null +++ b/lib/gitlab/background_migration/backfill_workspace_personal_access_token.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # No op on ce + class BackfillWorkspacePersonalAccessToken < BatchedMigrationJob + feature_category :remote_development + def perform; end + end + end +end + +Gitlab::BackgroundMigration::BackfillWorkspacePersonalAccessToken.prepend_mod_with('Gitlab::BackgroundMigration::BackfillWorkspacePersonalAccessToken') # rubocop:disable Layout/LineLength diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index f1434158a85..eefa23142af 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -80,7 +80,6 @@ module Gitlab push_frontend_feature_flag(:remove_monitor_metrics) push_frontend_feature_flag(:gitlab_duo, current_user) push_frontend_feature_flag(:custom_emoji) - push_frontend_feature_flag(:super_sidebar_flyout_menus, current_user) end # Exposes the state of a feature flag to the frontend code. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 363614d3f42..ba985e1c35a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1133,9 +1133,6 @@ msgstr "" msgid "%{startDate} – No due date" msgstr "" -msgid "%{start} to %{end}" -msgstr "" - msgid "%{statusStart}Dismissed%{statusEnd}" msgstr "" @@ -1686,12 +1683,6 @@ msgstr "" msgid "2FADevice|Registered On" msgstr "" -msgid "3 hours" -msgstr "" - -msgid "30 minutes" -msgstr "" - msgid "30+ contributions" msgstr "" @@ -1719,9 +1710,6 @@ msgstr "" msgid "409|There was a conflict with your request." msgstr "" -msgid "8 hours" -msgstr "" - msgid ":%{startLine} to %{endLine}" msgstr "" @@ -3696,6 +3684,9 @@ msgstr "" msgid "AdminSettings|The maximum number of included files per pipeline." msgstr "" +msgid "AdminSettings|The selected level must be different from the selected default group and project visibility." +msgstr "" + msgid "AdminSettings|The template for the required pipeline configuration can be one of the GitLab-provided templates, or a custom template added to an instance template repository. %{link_start}How do I create an instance template repository?%{link_end}" msgstr "" @@ -14416,9 +14407,6 @@ msgstr "" msgid "Custom range" msgstr "" -msgid "Custom range (UTC)" -msgstr "" - msgid "Customer contacts" msgstr "" @@ -20655,9 +20643,6 @@ msgstr "" msgid "ForksDivergence|View merge request" msgstr "" -msgid "Format: %{dateFormat}" -msgstr "" - msgid "Framework successfully deleted" msgstr "" @@ -25413,9 +25398,6 @@ msgstr "" msgid "Invalid date format. Please use UTC format as YYYY-MM-DD" msgstr "" -msgid "Invalid date range" -msgstr "" - msgid "Invalid dates set" msgstr "" @@ -38595,9 +38577,6 @@ msgstr "" msgid "Quick help" msgstr "" -msgid "Quick range" -msgstr "" - msgid "README" msgstr "" diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 09e9e0ba721..057146b31dd 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1756,7 +1756,7 @@ RSpec.describe Projects::IssuesController, :request_store, feature_category: :te it 'allows an assignee to be specified by id' do get_service_desk(assignee_id: other_user.id) - expect(assigns(:users)).to contain_exactly(other_user, support_bot) + expect(assigns(:issues)).to contain_exactly(service_desk_issue_2) end end diff --git a/spec/frontend/lib/utils/datetime_range_spec.js b/spec/frontend/lib/utils/datetime_range_spec.js deleted file mode 100644 index 996a8e2e47b..00000000000 --- a/spec/frontend/lib/utils/datetime_range_spec.js +++ /dev/null @@ -1,382 +0,0 @@ -import _ from 'lodash'; -import { - getRangeType, - convertToFixedRange, - isEqualTimeRanges, - findTimeRange, - timeRangeToParams, - timeRangeFromParams, -} from '~/lib/utils/datetime_range'; - -const MOCK_NOW = Date.UTC(2020, 0, 23, 20); - -const MOCK_NOW_ISO_STRING = new Date(MOCK_NOW).toISOString(); - -const mockFixedRange = { - label: 'January 2020', - start: '2020-01-01T00:00:00.000Z', - end: '2020-01-31T23:59:00.000Z', -}; - -const mockAnchoredRange = { - label: 'First two minutes of 2020', - anchor: '2020-01-01T00:00:00.000Z', - direction: 'after', - duration: { - seconds: 60 * 2, - }, -}; - -const mockRollingRange = { - label: 'Next 2 minutes', - direction: 'after', - duration: { - seconds: 60 * 2, - }, -}; - -const mockOpenRange = { - label: '2020 so far', - anchor: '2020-01-01T00:00:00.000Z', - direction: 'after', -}; - -describe('Date time range utils', () => { - describe('getRangeType', () => { - it('infers correctly the range type from the input object', () => { - const rangeTypes = { - fixed: [{ start: MOCK_NOW_ISO_STRING, end: MOCK_NOW_ISO_STRING }], - anchored: [{ anchor: MOCK_NOW_ISO_STRING, duration: { seconds: 0 } }], - rolling: [{ duration: { seconds: 0 } }], - open: [{ anchor: MOCK_NOW_ISO_STRING }], - invalid: [ - {}, - { start: MOCK_NOW_ISO_STRING }, - { end: MOCK_NOW_ISO_STRING }, - { start: 'NOT_A_DATE', end: 'NOT_A_DATE' }, - { duration: { seconds: 'NOT_A_NUMBER' } }, - { duration: { seconds: Infinity } }, - { duration: { minutes: 20 } }, - { anchor: MOCK_NOW_ISO_STRING, duration: { seconds: 'NOT_A_NUMBER' } }, - { anchor: MOCK_NOW_ISO_STRING, duration: { seconds: Infinity } }, - { junk: 'exists' }, - ], - }; - - Object.entries(rangeTypes).forEach(([type, examples]) => { - examples.forEach((example) => expect(getRangeType(example)).toEqual(type)); - }); - }); - }); - - describe('convertToFixedRange', () => { - beforeEach(() => { - jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW); - }); - - afterEach(() => { - Date.now.mockRestore(); - }); - - describe('When a fixed range is input', () => { - it('converts a fixed range to an equal fixed range', () => { - expect(convertToFixedRange(mockFixedRange)).toEqual({ - start: mockFixedRange.start, - end: mockFixedRange.end, - }); - }); - - it('throws an error when fixed range does not contain an end time', () => { - const aFixedRangeMissingEnd = _.omit(mockFixedRange, 'end'); - - expect(() => convertToFixedRange(aFixedRangeMissingEnd)).toThrow(); - }); - - it('throws an error when fixed range does not contain a start time', () => { - const aFixedRangeMissingStart = _.omit(mockFixedRange, 'start'); - - expect(() => convertToFixedRange(aFixedRangeMissingStart)).toThrow(); - }); - - it('throws an error when the dates cannot be parsed', () => { - const wrongStart = { ...mockFixedRange, start: 'I_CANNOT_BE_PARSED' }; - const wrongEnd = { ...mockFixedRange, end: 'I_CANNOT_BE_PARSED' }; - - expect(() => convertToFixedRange(wrongStart)).toThrow(); - expect(() => convertToFixedRange(wrongEnd)).toThrow(); - }); - }); - - describe('When an anchored range is input', () => { - it('converts to a fixed range', () => { - expect(convertToFixedRange(mockAnchoredRange)).toEqual({ - start: '2020-01-01T00:00:00.000Z', - end: '2020-01-01T00:02:00.000Z', - }); - }); - - it('converts to a fixed range with a `before` direction', () => { - expect(convertToFixedRange({ ...mockAnchoredRange, direction: 'before' })).toEqual({ - start: '2019-12-31T23:58:00.000Z', - end: '2020-01-01T00:00:00.000Z', - }); - }); - - it('converts to a fixed range without an explicit direction, defaulting to `before`', () => { - const defaultDirectionRange = _.omit(mockAnchoredRange, 'direction'); - - expect(convertToFixedRange(defaultDirectionRange)).toEqual({ - start: '2019-12-31T23:58:00.000Z', - end: '2020-01-01T00:00:00.000Z', - }); - }); - - it('throws an error when the anchor cannot be parsed', () => { - const wrongAnchor = { ...mockAnchoredRange, anchor: 'I_CANNOT_BE_PARSED' }; - - expect(() => convertToFixedRange(wrongAnchor)).toThrow(); - }); - }); - - describe('when a rolling range is input', () => { - it('converts to a fixed range', () => { - expect(convertToFixedRange(mockRollingRange)).toEqual({ - start: '2020-01-23T20:00:00.000Z', - end: '2020-01-23T20:02:00.000Z', - }); - }); - - it('converts to a fixed range with an implicit `before` direction', () => { - const noDirection = _.omit(mockRollingRange, 'direction'); - - expect(convertToFixedRange(noDirection)).toEqual({ - start: '2020-01-23T19:58:00.000Z', - end: '2020-01-23T20:00:00.000Z', - }); - }); - - it('throws an error when the duration is not in the right format', () => { - const wrongDuration = { ...mockRollingRange, duration: { minutes: 20 } }; - - expect(() => convertToFixedRange(wrongDuration)).toThrow(); - }); - - it('throws an error when the anchor is not valid', () => { - const wrongAnchor = { ...mockRollingRange, anchor: 'CAN_T_PARSE_THIS' }; - - expect(() => convertToFixedRange(wrongAnchor)).toThrow(); - }); - }); - - describe('when an open range is input', () => { - it('converts to a fixed range with an `after` direction', () => { - expect(convertToFixedRange(mockOpenRange)).toEqual({ - start: '2020-01-01T00:00:00.000Z', - end: '2020-01-23T20:00:00.000Z', - }); - }); - - it('converts to a fixed range with the explicit `before` direction', () => { - const beforeOpenRange = { ...mockOpenRange, direction: 'before' }; - - expect(convertToFixedRange(beforeOpenRange)).toEqual({ - start: '1970-01-01T00:00:00.000Z', - end: '2020-01-01T00:00:00.000Z', - }); - }); - - it('converts to a fixed range with the implicit `before` direction', () => { - const noDirectionOpenRange = _.omit(mockOpenRange, 'direction'); - - expect(convertToFixedRange(noDirectionOpenRange)).toEqual({ - start: '1970-01-01T00:00:00.000Z', - end: '2020-01-01T00:00:00.000Z', - }); - }); - - it('throws an error when the anchor cannot be parsed', () => { - const wrongAnchor = { ...mockOpenRange, anchor: 'CAN_T_PARSE_THIS' }; - - expect(() => convertToFixedRange(wrongAnchor)).toThrow(); - }); - }); - }); - - describe('isEqualTimeRanges', () => { - it('equal only compares relevant properies', () => { - expect( - isEqualTimeRanges( - { - ...mockFixedRange, - label: 'A label', - default: true, - }, - { - ...mockFixedRange, - label: 'Another label', - default: false, - anotherKey: 'anotherValue', - }, - ), - ).toBe(true); - - expect( - isEqualTimeRanges( - { - ...mockAnchoredRange, - label: 'A label', - default: true, - }, - { - ...mockAnchoredRange, - anotherKey: 'anotherValue', - }, - ), - ).toBe(true); - }); - }); - - describe('findTimeRange', () => { - const timeRanges = [ - { - label: 'Before 2020', - anchor: '2020-01-01T00:00:00.000Z', - }, - { - label: 'Last 30 minutes', - duration: { seconds: 60 * 30 }, - }, - { - label: 'In 2019', - start: '2019-01-01T00:00:00.000Z', - end: '2019-12-31T12:59:59.999Z', - }, - { - label: 'Next 2 minutes', - direction: 'after', - duration: { - seconds: 60 * 2, - }, - }, - ]; - - it('finds a time range', () => { - const tr0 = { - anchor: '2020-01-01T00:00:00.000Z', - }; - expect(findTimeRange(tr0, timeRanges)).toBe(timeRanges[0]); - - const tr1 = { - duration: { seconds: 60 * 30 }, - }; - expect(findTimeRange(tr1, timeRanges)).toBe(timeRanges[1]); - - const tr1Direction = { - direction: 'before', - duration: { - seconds: 60 * 30, - }, - }; - expect(findTimeRange(tr1Direction, timeRanges)).toBe(timeRanges[1]); - - const tr2 = { - someOtherLabel: 'Added arbitrarily', - start: '2019-01-01T00:00:00.000Z', - end: '2019-12-31T12:59:59.999Z', - }; - expect(findTimeRange(tr2, timeRanges)).toBe(timeRanges[2]); - - const tr3 = { - direction: 'after', - duration: { - seconds: 60 * 2, - }, - }; - expect(findTimeRange(tr3, timeRanges)).toBe(timeRanges[3]); - }); - - it('doesnot finds a missing time range', () => { - const nonExistant = { - direction: 'before', - duration: { - seconds: 200, - }, - }; - expect(findTimeRange(nonExistant, timeRanges)).toBeUndefined(); - }); - }); - - describe('conversion to/from params', () => { - const mockFixedParams = { - start: '2020-01-01T00:00:00.000Z', - end: '2020-01-31T23:59:00.000Z', - }; - - const mockAnchoredParams = { - anchor: '2020-01-01T00:00:00.000Z', - direction: 'after', - duration_seconds: '120', - }; - - const mockRollingParams = { - direction: 'after', - duration_seconds: '120', - }; - - describe('timeRangeToParams', () => { - it('converts fixed ranges to params', () => { - expect(timeRangeToParams(mockFixedRange)).toEqual(mockFixedParams); - }); - - it('converts anchored ranges to params', () => { - expect(timeRangeToParams(mockAnchoredRange)).toEqual(mockAnchoredParams); - }); - - it('converts rolling ranges to params', () => { - expect(timeRangeToParams(mockRollingRange)).toEqual(mockRollingParams); - }); - }); - - describe('timeRangeFromParams', () => { - it('converts fixed ranges from params', () => { - const params = { ...mockFixedParams, other_param: 'other_value' }; - const expectedRange = _.omit(mockFixedRange, 'label'); - - expect(timeRangeFromParams(params)).toEqual(expectedRange); - }); - - it('converts anchored ranges to params', () => { - const expectedRange = _.omit(mockRollingRange, 'label'); - - expect(timeRangeFromParams(mockRollingParams)).toEqual(expectedRange); - }); - - it('converts rolling ranges from params', () => { - const params = { ...mockRollingParams, other_param: 'other_value' }; - const expectedRange = _.omit(mockRollingRange, 'label'); - - expect(timeRangeFromParams(params)).toEqual(expectedRange); - }); - - it('converts rolling ranges from params with a default direction', () => { - const params = { - ...mockRollingParams, - direction: 'before', - other_param: 'other_value', - }; - const expectedRange = _.omit(mockRollingRange, 'label', 'direction'); - - expect(timeRangeFromParams(params)).toEqual(expectedRange); - }); - - it('converts to null when for no relevant params', () => { - const range = { - useless_param_1: 'value1', - useless_param_2: 'value2', - }; - - expect(timeRangeFromParams(range)).toBe(null); - }); - }); - }); -}); diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js index dd780b172f5..c85a6609e6f 100644 --- a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js +++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js @@ -16,13 +16,8 @@ const menuItems = [ describe('Sidebar Menu', () => { let wrapper; - let flyoutFlag = false; - const createWrapper = (extraProps = {}) => { wrapper = shallowMountExtended(SidebarMenu, { - provide: { - glFeatures: { superSidebarFlyoutMenus: flyoutFlag }, - }, propsData: { items: sidebarData.current_menu_items, isLoggedIn: sidebarData.is_logged_in, @@ -125,8 +120,11 @@ describe('Sidebar Menu', () => { }); describe('flyout menus', () => { - describe('when feature is disabled', () => { + describe('when screen width is smaller than "md" breakpoint', () => { beforeEach(() => { + jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => { + return 767; + }); createWrapper({ items: menuItems, }); @@ -140,45 +138,21 @@ describe('Sidebar Menu', () => { }); }); - describe('when feature is enabled', () => { + describe('when screen width is equal or larger than "md" breakpoint', () => { beforeEach(() => { - flyoutFlag = true; - }); - - describe('when screen width is smaller than "md" breakpoint', () => { - beforeEach(() => { - jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => { - return 767; - }); - createWrapper({ - items: menuItems, - }); + jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => { + return 768; }); - - it('does not add flyout menus to sections', () => { - expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([ - false, - false, - ]); + createWrapper({ + items: menuItems, }); }); - describe('when screen width is equal or larger than "md" breakpoint', () => { - beforeEach(() => { - jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => { - return 768; - }); - createWrapper({ - items: menuItems, - }); - }); - - it('adds flyout menus to sections', () => { - expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([ - true, - true, - ]); - }); + it('adds flyout menus to sections', () => { + expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([ + true, + true, + ]); }); }); }); diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap deleted file mode 100644 index 98adcb5b178..00000000000 --- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap +++ /dev/null @@ -1,54 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SplitButton renders actionItems 1`] = ` -<gl-dropdown-stub - category="primary" - clearalltext="Clear all" - clearalltextclass="gl-px-5" - headertext="" - hideheaderborder="true" - highlighteditemstitle="Selected" - highlighteditemstitleclass="gl-px-5" - menu-class="" - size="medium" - split="true" - splithref="" - text="professor" - variant="default" -> - <gl-dropdown-item-stub - avatarurl="" - iconcolor="" - iconname="" - iconrightarialabel="" - iconrightname="" - ischecked="true" - ischeckitem="true" - secondarytext="" - > - <strong> - professor - </strong> - <div> - very symphonic - </div> - </gl-dropdown-item-stub> - <gl-dropdown-divider-stub /> - <gl-dropdown-item-stub - avatarurl="" - iconcolor="" - iconname="" - iconrightarialabel="" - iconrightname="" - ischeckitem="true" - secondarytext="" - > - <strong> - captain - </strong> - <div> - warp drive - </div> - </gl-dropdown-item-stub> -</gl-dropdown-stub> -`; diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js deleted file mode 100644 index a3e5f187f9b..00000000000 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import { mount } from '@vue/test-utils'; -import DateTimePickerInput from '~/vue_shared/components/date_time_picker/date_time_picker_input.vue'; - -const inputLabel = 'This is a label'; -const inputValue = 'something'; - -describe('DateTimePickerInput', () => { - let wrapper; - - const createComponent = (propsData = {}) => { - wrapper = mount(DateTimePickerInput, { - propsData: { - state: null, - value: '', - label: '', - ...propsData, - }, - }); - }; - - it('renders label above the input', () => { - createComponent({ - label: inputLabel, - }); - - expect(wrapper.find('.gl-form-group label').text()).toBe(inputLabel); - }); - - it('renders the same `ID` for input and `for` for label', () => { - createComponent({ label: inputLabel }); - - expect(wrapper.find('.gl-form-group label').attributes('for')).toBe( - wrapper.find('input').attributes('id'), - ); - }); - - it('renders valid input in gray color instead of green', () => { - createComponent({ - state: true, - }); - - expect(wrapper.find('input').classes('is-valid')).toBe(false); - }); - - it('renders invalid input in red color', () => { - createComponent({ - state: false, - }); - - expect(wrapper.find('input').classes('is-invalid')).toBe(true); - }); - - it('input event is emitted when focus is lost', () => { - createComponent(); - - const input = wrapper.find('input'); - input.setValue(inputValue); - input.trigger('blur'); - - expect(wrapper.emitted('input')[0][0]).toEqual(inputValue); - }); -}); diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js deleted file mode 100644 index 7a8f94b3746..00000000000 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js +++ /dev/null @@ -1,190 +0,0 @@ -import timezoneMock from 'timezone-mock'; - -import { - isValidInputString, - inputStringToIsoDate, - isoDateToInputString, -} from '~/vue_shared/components/date_time_picker/date_time_picker_lib'; - -describe('date time picker lib', () => { - describe('isValidInputString', () => { - [ - { - input: '2019-09-09T00:00:00.000Z', - output: true, - }, - { - input: '2019-09-09T000:00.000Z', - output: false, - }, - { - input: 'a2019-09-09T000:00.000Z', - output: false, - }, - { - input: '2019-09-09T', - output: false, - }, - { - input: '2019-09-09', - output: true, - }, - { - input: '2019-9-9', - output: true, - }, - { - input: '2019-9-', - output: true, - }, - { - input: '2019--', - output: false, - }, - { - input: '2019', - output: true, - }, - { - input: '', - output: false, - }, - { - input: null, - output: false, - }, - ].forEach(({ input, output }) => { - it(`isValidInputString return ${output} for ${input}`, () => { - expect(isValidInputString(input)).toBe(output); - }); - }); - }); - - describe('inputStringToIsoDate', () => { - [ - '', - 'null', - undefined, - 'abc', - 'xxxx-xx-xx', - '9999-99-19', - '2019-19-23', - '2019-09-23 x', - '2019-09-29 24:24:24', - ].forEach((input) => { - it(`throws error for invalid input like ${input}`, () => { - expect(() => inputStringToIsoDate(input)).toThrow(); - }); - }); - - [ - { - input: '2019-09-08 01:01:01', - output: '2019-09-08T01:01:01Z', - }, - { - input: '2019-09-08 00:00:00', - output: '2019-09-08T00:00:00Z', - }, - { - input: '2019-09-08 23:59:59', - output: '2019-09-08T23:59:59Z', - }, - { - input: '2019-09-08', - output: '2019-09-08T00:00:00Z', - }, - { - input: '2019-09-08', - output: '2019-09-08T00:00:00Z', - }, - { - input: '2019-09-08 00:00:00', - output: '2019-09-08T00:00:00Z', - }, - { - input: '2019-09-08 23:24:24', - output: '2019-09-08T23:24:24Z', - }, - { - input: '2019-09-08 0:0:0', - output: '2019-09-08T00:00:00Z', - }, - ].forEach(({ input, output }) => { - it(`returns ${output} from ${input}`, () => { - expect(inputStringToIsoDate(input)).toBe(output); - }); - }); - - describe('timezone formatting', () => { - const value = '2019-09-08 01:01:01'; - const utcResult = '2019-09-08T01:01:01Z'; - const localResult = '2019-09-08T08:01:01Z'; - - it.each` - val | locatTimezone | utc | result - ${value} | ${'UTC'} | ${undefined} | ${utcResult} - ${value} | ${'UTC'} | ${false} | ${utcResult} - ${value} | ${'UTC'} | ${true} | ${utcResult} - ${value} | ${'US/Pacific'} | ${undefined} | ${localResult} - ${value} | ${'US/Pacific'} | ${false} | ${localResult} - ${value} | ${'US/Pacific'} | ${true} | ${utcResult} - `( - 'when timezone is $locatTimezone, formats $result for utc = $utc', - ({ val, locatTimezone, utc, result }) => { - timezoneMock.register(locatTimezone); - - expect(inputStringToIsoDate(val, utc)).toBe(result); - - timezoneMock.unregister(); - }, - ); - }); - }); - - describe('isoDateToInputString', () => { - [ - { - input: '2019-09-08T01:01:01Z', - output: '2019-09-08 01:01:01', - }, - { - input: '2019-09-08T01:01:01.999Z', - output: '2019-09-08 01:01:01', - }, - { - input: '2019-09-08T00:00:00Z', - output: '2019-09-08 00:00:00', - }, - ].forEach(({ input, output }) => { - it(`returns ${output} for ${input}`, () => { - expect(isoDateToInputString(input)).toBe(output); - }); - }); - - describe('timezone formatting', () => { - const value = '2019-09-08T08:01:01Z'; - const utcResult = '2019-09-08 08:01:01'; - const localResult = '2019-09-08 01:01:01'; - - it.each` - val | locatTimezone | utc | result - ${value} | ${'UTC'} | ${undefined} | ${utcResult} - ${value} | ${'UTC'} | ${false} | ${utcResult} - ${value} | ${'UTC'} | ${true} | ${utcResult} - ${value} | ${'US/Pacific'} | ${undefined} | ${localResult} - ${value} | ${'US/Pacific'} | ${false} | ${localResult} - ${value} | ${'US/Pacific'} | ${true} | ${utcResult} - `( - 'when timezone is $locatTimezone, formats $result for utc = $utc', - ({ val, locatTimezone, utc, result }) => { - timezoneMock.register(locatTimezone); - - expect(isoDateToInputString(val, utc)).toBe(result); - - timezoneMock.unregister(); - }, - ); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js deleted file mode 100644 index 5620b569409..00000000000 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js +++ /dev/null @@ -1,326 +0,0 @@ -import { mount } from '@vue/test-utils'; -import timezoneMock from 'timezone-mock'; -import { nextTick } from 'vue'; -import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; -import { - defaultTimeRanges, - defaultTimeRange, -} from '~/vue_shared/components/date_time_picker/date_time_picker_lib'; - -const optionsCount = defaultTimeRanges.length; - -describe('DateTimePicker', () => { - let wrapper; - - const dropdownToggle = () => wrapper.find('.dropdown-toggle'); - const dropdownMenu = () => wrapper.find('.dropdown-menu'); - const cancelButton = () => wrapper.find('[data-testid="cancelButton"]'); - const applyButtonElement = () => wrapper.find('button.btn-confirm').element; - const findQuickRangeItems = () => wrapper.findAll('.dropdown-item'); - - const createComponent = (props) => { - wrapper = mount(DateTimePicker, { - propsData: { - ...props, - }, - }); - }; - - it('renders dropdown toggle button with selected text', async () => { - createComponent(); - await nextTick(); - expect(dropdownToggle().text()).toBe(defaultTimeRange.label); - }); - - it('renders dropdown toggle button with selected text and utc label', async () => { - createComponent({ utc: true }); - await nextTick(); - expect(dropdownToggle().text()).toContain(defaultTimeRange.label); - expect(dropdownToggle().text()).toContain('UTC'); - }); - - it('renders dropdown with 2 custom time range inputs', async () => { - createComponent(); - await nextTick(); - expect(wrapper.findAll('input').length).toBe(2); - }); - - describe('renders label with h/m/s truncated if possible', () => { - [ - { - start: '2019-10-10T00:00:00.000Z', - end: '2019-10-10T00:00:00.000Z', - label: '2019-10-10 to 2019-10-10', - }, - { - start: '2019-10-10T00:00:00.000Z', - end: '2019-10-14T00:10:00.000Z', - label: '2019-10-10 to 2019-10-14 00:10:00', - }, - { - start: '2019-10-10T00:00:00.000Z', - end: '2019-10-10T00:00:01.000Z', - label: '2019-10-10 to 2019-10-10 00:00:01', - }, - { - start: '2019-10-10T00:00:01.000Z', - end: '2019-10-10T00:00:01.000Z', - label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01', - }, - { - start: '2019-10-10T00:00:01.000Z', - end: '2019-10-10T00:00:01.000Z', - utc: true, - label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01 UTC', - }, - ].forEach(({ start, end, utc, label }) => { - it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, async () => { - createComponent({ - value: { start, end }, - utc, - }); - await nextTick(); - expect(dropdownToggle().text()).toBe(label); - }); - }); - }); - - it(`renders dropdown with ${optionsCount} (default) items in quick range`, async () => { - createComponent(); - dropdownToggle().trigger('click'); - await nextTick(); - expect(findQuickRangeItems().length).toBe(optionsCount); - }); - - it('renders dropdown with a default quick range item selected', async () => { - createComponent(); - dropdownToggle().trigger('click'); - await nextTick(); - expect(wrapper.find('.dropdown-item.active').exists()).toBe(true); - expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label); - }); - - it('renders a disabled apply button on wrong input', () => { - createComponent({ - start: 'invalid-input-date', - }); - - expect(applyButtonElement().getAttribute('disabled')).toBe('disabled'); - }); - - describe('user input', () => { - const fillInputAndBlur = async (input, val) => { - wrapper.find(input).setValue(val); - await nextTick(); - wrapper.find(input).trigger('blur'); - await nextTick(); - }; - - beforeEach(async () => { - createComponent(); - await nextTick(); - }); - - it('displays inline error message if custom time range inputs are invalid', async () => { - await fillInputAndBlur('#custom-time-from', '2019-10-01abc'); - await fillInputAndBlur('#custom-time-to', '2019-10-10abc'); - expect(wrapper.findAll('.invalid-feedback').length).toBe(2); - }); - - it('keeps apply button disabled with invalid custom time range inputs', async () => { - await fillInputAndBlur('#custom-time-from', '2019-10-01abc'); - await fillInputAndBlur('#custom-time-to', '2019-09-19'); - expect(applyButtonElement().getAttribute('disabled')).toBe('disabled'); - }); - - it('enables apply button with valid custom time range inputs', async () => { - await fillInputAndBlur('#custom-time-from', '2019-10-01'); - await fillInputAndBlur('#custom-time-to', '2019-10-19'); - expect(applyButtonElement().getAttribute('disabled')).toBeNull(); - }); - - describe('when "apply" is clicked', () => { - it('emits iso dates', async () => { - await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); - await fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00'); - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - end: '2019-10-19T00:00:00Z', - start: '2019-10-01T00:00:00Z', - }, - ]); - }); - - it('emits iso dates, for dates without time of day', async () => { - await fillInputAndBlur('#custom-time-from', '2019-10-01'); - await fillInputAndBlur('#custom-time-to', '2019-10-19'); - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - end: '2019-10-19T00:00:00Z', - start: '2019-10-01T00:00:00Z', - }, - ]); - }); - - describe('when timezone is different', () => { - beforeAll(() => { - timezoneMock.register('US/Pacific'); - }); - afterAll(() => { - timezoneMock.unregister(); - }); - - it('emits iso dates', async () => { - await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); - await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'); - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - start: '2019-10-01T07:00:00Z', - end: '2019-10-19T19:00:00Z', - }, - ]); - }); - - it('emits iso dates with utc format', async () => { - wrapper.setProps({ utc: true }); - await nextTick(); - await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); - await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'); - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - start: '2019-10-01T00:00:00Z', - end: '2019-10-19T12:00:00Z', - }, - ]); - }); - }); - }); - - it('unchecks quick range when text is input is clicked', async () => { - const findActiveItems = () => - findQuickRangeItems().filter((w) => w.classes().includes('active')); - - expect(findActiveItems().length).toBe(1); - - await fillInputAndBlur('#custom-time-from', '2019-10-01'); - expect(findActiveItems().length).toBe(0); - }); - - it('emits dates in an object when a is clicked', () => { - findQuickRangeItems() - .at(3) // any item - .trigger('click'); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0][0]).toMatchObject({ - duration: { - seconds: expect.any(Number), - }, - }); - }); - - it('hides the popover with cancel button', async () => { - dropdownToggle().trigger('click'); - - await nextTick(); - cancelButton().trigger('click'); - - await nextTick(); - expect(dropdownMenu().classes('show')).toBe(false); - }); - }); - - describe('when using non-default time windows', () => { - const MOCK_NOW = Date.UTC(2020, 0, 23, 20); - - const otherTimeRanges = [ - { - label: '1 minute', - duration: { seconds: 60 }, - }, - { - label: '2 minutes', - duration: { seconds: 60 * 2 }, - default: true, - }, - { - label: '5 minutes', - duration: { seconds: 60 * 5 }, - }, - ]; - - beforeEach(() => { - jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW); - }); - - it('renders dropdown with a label in the quick range', async () => { - createComponent({ - value: { - duration: { seconds: 60 * 5 }, - }, - options: otherTimeRanges, - }); - dropdownToggle().trigger('click'); - await nextTick(); - expect(dropdownToggle().text()).toBe('5 minutes'); - }); - - it('renders dropdown with a label in the quick range and utc label', async () => { - createComponent({ - value: { - duration: { seconds: 60 * 5 }, - }, - utc: true, - options: otherTimeRanges, - }); - dropdownToggle().trigger('click'); - await nextTick(); - expect(dropdownToggle().text()).toBe('5 minutes UTC'); - }); - - it('renders dropdown with quick range items', async () => { - createComponent({ - value: { - duration: { seconds: 60 * 2 }, - }, - options: otherTimeRanges, - }); - dropdownToggle().trigger('click'); - await nextTick(); - const items = findQuickRangeItems(); - - expect(items.length).toBe(Object.keys(otherTimeRanges).length); - expect(items.at(0).text()).toBe('1 minute'); - expect(items.at(0).classes()).not.toContain('active'); - - expect(items.at(1).text()).toBe('2 minutes'); - expect(items.at(1).classes()).toContain('active'); - - expect(items.at(2).text()).toBe('5 minutes'); - expect(items.at(2).classes()).not.toContain('active'); - }); - - it('renders dropdown with a label not in the quick range', async () => { - createComponent({ - value: { - duration: { seconds: 60 * 4 }, - }, - }); - dropdownToggle().trigger('click'); - await nextTick(); - expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00'); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js deleted file mode 100644 index ffa25ae8448..00000000000 --- a/spec/frontend/vue_shared/components/split_button_spec.js +++ /dev/null @@ -1,117 +0,0 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; - -import { nextTick } from 'vue'; -import { assertProps } from 'helpers/assert_props'; -import SplitButton from '~/vue_shared/components/split_button.vue'; - -const mockActionItems = [ - { - eventName: 'concert', - title: 'professor', - description: 'very symphonic', - }, - { - eventName: 'apocalypse', - title: 'captain', - description: 'warp drive', - }, -]; - -describe('SplitButton', () => { - let wrapper; - - const createComponent = (propsData) => { - wrapper = shallowMount(SplitButton, { - propsData, - }); - }; - - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownItem = (index = 0) => - findDropdown().findAllComponents(GlDropdownItem).at(index); - const selectItem = async (index) => { - findDropdownItem(index).vm.$emit('click'); - - await nextTick(); - }; - const clickToggleButton = async () => { - findDropdown().vm.$emit('click'); - - await nextTick(); - }; - - it('fails for empty actionItems', () => { - const actionItems = []; - expect(() => assertProps(SplitButton, { actionItems })).toThrow(); - }); - - it('fails for single actionItems', () => { - const actionItems = [mockActionItems[0]]; - expect(() => assertProps(SplitButton, { actionItems })).toThrow(); - }); - - it('renders actionItems', () => { - createComponent({ actionItems: mockActionItems }); - - expect(wrapper.element).toMatchSnapshot(); - }); - - describe('toggle button text', () => { - beforeEach(() => { - createComponent({ actionItems: mockActionItems }); - }); - - it('defaults to first actionItems title', () => { - expect(findDropdown().props().text).toBe(mockActionItems[0].title); - }); - - it('changes to selected actionItems title', () => - selectItem(1).then(() => { - expect(findDropdown().props().text).toBe(mockActionItems[1].title); - })); - }); - - describe('emitted event', () => { - let eventHandler; - let changeEventHandler; - - beforeEach(() => { - createComponent({ actionItems: mockActionItems }); - }); - - const addEventHandler = ({ eventName }) => { - eventHandler = jest.fn(); - wrapper.vm.$once(eventName, () => eventHandler()); - }; - - const addChangeEventHandler = () => { - changeEventHandler = jest.fn(); - wrapper.vm.$once('change', (item) => changeEventHandler(item)); - }; - - it('defaults to first actionItems event', () => { - addEventHandler(mockActionItems[0]); - - return clickToggleButton().then(() => { - expect(eventHandler).toHaveBeenCalled(); - }); - }); - - it('changes to selected actionItems event', () => - selectItem(1) - .then(() => addEventHandler(mockActionItems[1])) - .then(clickToggleButton) - .then(() => { - expect(eventHandler).toHaveBeenCalled(); - })); - - it('change to selected actionItem emits change event', () => { - addChangeEventHandler(); - - return selectItem(1).then(() => { - expect(changeEventHandler).toHaveBeenCalledWith(mockActionItems[1]); - }); - }); - }); -}); |