Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/lib/utils/constants.js9
-rw-r--r--app/assets/javascripts/lib/utils/datetime_range.js309
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue283
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue77
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js91
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue85
-rw-r--r--app/controllers/concerns/issuable_collections.rb16
-rw-r--r--app/controllers/projects/issues_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb5
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml2
-rw-r--r--config/feature_flags/development/prevent_visibility_restriction.yml2
-rw-r--r--config/feature_flags/development/super_sidebar_flyout_menus.yml8
-rw-r--r--db/docs/batched_background_migrations/backfill_workspace_personal_access_token.yml5
-rw-r--r--db/post_migrate/20230909120000_queue_backfill_workspace_personal_access_token.rb26
-rw-r--r--db/schema_migrations/202309091200001
-rw-r--r--doc/administration/settings/visibility_and_access_controls.md12
-rw-r--r--doc/api/settings.md6
-rw-r--r--doc/user/profile/account/two_factor_authentication.md2
-rw-r--r--lib/gitlab/background_migration/backfill_workspace_personal_access_token.rb13
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--locale/gitlab.pot27
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/frontend/lib/utils/datetime_range_spec.js382
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_menu_spec.js54
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap54
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js190
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js326
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js117
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]);
- });
- });
- });
-});