Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b83c1bf235
commit
3107fe7203
|
@ -6,26 +6,6 @@ Layout/FirstArrayElementIndentation:
|
|||
- 'ee/spec/graphql/mutations/incident_management/escalation_policy/create_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/graphql/loaders/bulk_epic_aggregate_loader_spec.rb'
|
||||
- 'ee/spec/models/snippet_repository_spec.rb'
|
||||
- 'ee/spec/policies/project_policy_spec.rb'
|
||||
- 'ee/spec/requests/admin/user_permission_exports_controller_spec.rb'
|
||||
- 'ee/spec/requests/api/analytics/project_deployment_frequency_spec.rb'
|
||||
- 'ee/spec/requests/api/experiments_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/analytics/devops_adoption/enabled_namespaces_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/group/epics_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/quality_management/test_cases/create_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/releases/create_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/releases/update_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/project/alert_management/payload_fields_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/project/incident_management/escalation_policy/rules_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/project/merge_requests_spec.rb'
|
||||
- 'ee/spec/requests/api/internal/kubernetes_spec.rb'
|
||||
- 'ee/spec/requests/api/ldap_group_links_spec.rb'
|
||||
- 'ee/spec/requests/api/members_spec.rb'
|
||||
- 'ee/spec/services/analytics/cycle_analytics/value_streams/update_service_spec.rb'
|
||||
- 'ee/spec/services/audit_events/export_csv_service_spec.rb'
|
||||
- 'ee/spec/services/gitlab_subscriptions/fetch_purchase_eligible_namespaces_service_spec.rb'
|
||||
- 'ee/spec/services/groups/seat_usage_export_service_spec.rb'
|
||||
- 'ee/spec/services/iterations/cadences/create_iterations_in_advance_service_spec.rb'
|
||||
- 'ee/spec/services/protected_environments/base_service_spec.rb'
|
||||
- 'ee/spec/services/search_service_spec.rb'
|
||||
- 'ee/spec/services/security/ingestion/tasks/hooks_execution_spec.rb'
|
||||
|
|
|
@ -5,7 +5,7 @@ import AccessorUtilities from './lib/utils/accessor';
|
|||
export default class Autosave {
|
||||
constructor(field, key, fallbackKey, lockVersion) {
|
||||
this.field = field;
|
||||
|
||||
this.type = this.field.prop('type');
|
||||
this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
|
||||
if (key.join != null) {
|
||||
key = key.join('/');
|
||||
|
@ -22,11 +22,12 @@ export default class Autosave {
|
|||
restore() {
|
||||
if (!this.isLocalStorageAvailable) return;
|
||||
if (!this.field.length) return;
|
||||
|
||||
const text = window.localStorage.getItem(this.key);
|
||||
const fallbackText = window.localStorage.getItem(this.fallbackKey);
|
||||
|
||||
if (text) {
|
||||
if (this.type === 'checkbox') {
|
||||
this.field.prop('checked', text || fallbackText);
|
||||
} else if (text) {
|
||||
this.field.val(text);
|
||||
} else if (fallbackText) {
|
||||
this.field.val(fallbackText);
|
||||
|
@ -49,17 +50,16 @@ export default class Autosave {
|
|||
|
||||
save() {
|
||||
if (!this.field.length) return;
|
||||
const value = this.type === 'checkbox' ? this.field.is(':checked') : this.field.val();
|
||||
|
||||
const text = this.field.val();
|
||||
|
||||
if (this.isLocalStorageAvailable && text) {
|
||||
if (this.isLocalStorageAvailable && value) {
|
||||
if (this.fallbackKey) {
|
||||
window.localStorage.setItem(this.fallbackKey, text);
|
||||
window.localStorage.setItem(this.fallbackKey, value);
|
||||
}
|
||||
if (this.lockVersion !== undefined) {
|
||||
window.localStorage.setItem(this.lockVersionKey, this.lockVersion);
|
||||
}
|
||||
return window.localStorage.setItem(this.key, text);
|
||||
return window.localStorage.setItem(this.key, value);
|
||||
}
|
||||
|
||||
return this.reset();
|
||||
|
|
|
@ -64,6 +64,11 @@ export default {
|
|||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
codeQualityExpanded: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
classNameMap: memoize(
|
||||
(props) => {
|
||||
|
@ -272,6 +277,7 @@ export default {
|
|||
<component
|
||||
:is="$options.CodeQualityGutterIcon"
|
||||
v-if="$options.showCodequalityLeft(props)"
|
||||
:code-quality-expanded="props.codeQualityExpanded"
|
||||
:codequality="props.line.left.codequality"
|
||||
:file-path="props.filePath"
|
||||
@showCodeQualityFindings="
|
||||
|
|
|
@ -230,6 +230,7 @@ export default {
|
|||
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
|
||||
:inline="inline"
|
||||
:index="index"
|
||||
:code-quality-expanded="codeQualityExpandedLines.includes(getCodeQualityLine(line))"
|
||||
:is-highlighted="isHighlighted(line)"
|
||||
:file-line-coverage="fileLineCoverage"
|
||||
:coverage-loaded="coverageLoaded"
|
||||
|
|
|
@ -142,6 +142,13 @@ export default class IssuableForm {
|
|||
this.searchTerm,
|
||||
this.fallbackKey,
|
||||
);
|
||||
IssuableForm.addAutosave(
|
||||
autosaveMap,
|
||||
'confidential',
|
||||
this.form.find('input:checkbox[name*="[confidential]"]'),
|
||||
this.searchTerm,
|
||||
this.fallbackKey,
|
||||
);
|
||||
IssuableForm.addAutosave(
|
||||
autosaveMap,
|
||||
'due_date',
|
||||
|
|
|
@ -215,6 +215,19 @@ export const newDateAsLocaleTime = (date) => {
|
|||
return new Date(`${date}${suffix}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a Date object (where timezone could be GMT or EST) and
|
||||
* returns a Date object with the same date but in UTC.
|
||||
*
|
||||
* @param {Date} date A Date object
|
||||
* @returns {Date|null} A Date object with the same date but in UTC
|
||||
*/
|
||||
export const getDateWithUTC = (date) => {
|
||||
return date instanceof Date
|
||||
? new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
||||
: null;
|
||||
};
|
||||
|
||||
export const beginOfDayTime = 'T00:00:00Z';
|
||||
export const endOfDayTime = 'T23:59:59Z';
|
||||
|
||||
|
|
|
@ -16,12 +16,14 @@ import {
|
|||
WIDGET_TYPE_ASSIGNEES,
|
||||
WIDGET_TYPE_LABELS,
|
||||
WIDGET_TYPE_DESCRIPTION,
|
||||
WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
WORK_ITEM_VIEWED_STORAGE_KEY,
|
||||
} from '../constants';
|
||||
|
||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
||||
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
|
||||
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
|
||||
|
@ -30,6 +32,7 @@ import WorkItemActions from './work_item_actions.vue';
|
|||
import WorkItemState from './work_item_state.vue';
|
||||
import WorkItemTitle from './work_item_title.vue';
|
||||
import WorkItemDescription from './work_item_description.vue';
|
||||
import WorkItemDueDate from './work_item_due_date.vue';
|
||||
import WorkItemAssignees from './work_item_assignees.vue';
|
||||
import WorkItemLabels from './work_item_labels.vue';
|
||||
import WorkItemInformation from './work_item_information.vue';
|
||||
|
@ -49,6 +52,7 @@ export default {
|
|||
WorkItemAssignees,
|
||||
WorkItemActions,
|
||||
WorkItemDescription,
|
||||
WorkItemDueDate,
|
||||
WorkItemLabels,
|
||||
WorkItemTitle,
|
||||
WorkItemState,
|
||||
|
@ -106,14 +110,27 @@ export default {
|
|||
document.title = `${this.workItem.title} · ${this.workItem?.workItemType?.name}${path}`;
|
||||
}
|
||||
},
|
||||
subscribeToMore: {
|
||||
document: workItemTitleSubscription,
|
||||
variables() {
|
||||
return {
|
||||
issuableId: this.workItemId,
|
||||
};
|
||||
subscribeToMore: [
|
||||
{
|
||||
document: workItemTitleSubscription,
|
||||
variables() {
|
||||
return {
|
||||
issuableId: this.workItemId,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
document: workItemDatesSubscription,
|
||||
variables() {
|
||||
return {
|
||||
issuableId: this.workItemId,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.workItemDueDate;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -144,6 +161,11 @@ export default {
|
|||
workItemLabels() {
|
||||
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
|
||||
},
|
||||
workItemDueDate() {
|
||||
return this.workItem?.widgets?.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
);
|
||||
},
|
||||
workItemWeight() {
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
|
||||
},
|
||||
|
@ -348,6 +370,15 @@ export default {
|
|||
:full-path="fullPath"
|
||||
@error="error = $event"
|
||||
/>
|
||||
<work-item-due-date
|
||||
v-if="workItemDueDate"
|
||||
:can-update="canUpdate"
|
||||
:due-date="workItemDueDate.dueDate"
|
||||
:start-date="workItemDueDate.startDate"
|
||||
:work-item-id="workItem.id"
|
||||
:work-item-type="workItemType"
|
||||
@error="error = $event"
|
||||
/>
|
||||
</template>
|
||||
<work-item-weight
|
||||
v-if="workItemWeight"
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
<script>
|
||||
import { GlButton, GlDatepicker, GlFormGroup } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import {
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
sprintfWorkItem,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
} from '~/work_items/constants';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
|
||||
const nullObjectDate = new Date(0);
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
addDueDate: s__('WorkItem|Add due date'),
|
||||
addStartDate: s__('WorkItem|Add start date'),
|
||||
dates: s__('WorkItem|Dates'),
|
||||
dueDate: s__('WorkItem|Due date'),
|
||||
none: s__('WorkItem|None'),
|
||||
startDate: s__('WorkItem|Start date'),
|
||||
},
|
||||
dueDateInputId: 'due-date-input',
|
||||
startDateInputId: 'start-date-input',
|
||||
components: {
|
||||
GlButton,
|
||||
GlDatepicker,
|
||||
GlFormGroup,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
dueDate: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
startDate: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dirtyDueDate: null,
|
||||
dirtyStartDate: null,
|
||||
isUpdating: false,
|
||||
showDueDateInput: false,
|
||||
showStartDateInput: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
datesUnchanged() {
|
||||
const dirtyDueDate = this.dirtyDueDate || nullObjectDate;
|
||||
const dirtyStartDate = this.dirtyStartDate || nullObjectDate;
|
||||
const dueDate = this.dueDate ? newDateAsLocaleTime(this.dueDate) : nullObjectDate;
|
||||
const startDate = this.startDate ? newDateAsLocaleTime(this.startDate) : nullObjectDate;
|
||||
return (
|
||||
dirtyDueDate.getTime() === dueDate.getTime() &&
|
||||
dirtyStartDate.getTime() === startDate.getTime()
|
||||
);
|
||||
},
|
||||
isDatepickerDisabled() {
|
||||
return !this.canUpdate || this.isUpdating;
|
||||
},
|
||||
isReadonlyWithOnlyDueDate() {
|
||||
return !this.canUpdate && this.dueDate && !this.startDate;
|
||||
},
|
||||
isReadonlyWithOnlyStartDate() {
|
||||
return !this.canUpdate && !this.dueDate && this.startDate;
|
||||
},
|
||||
isReadonlyWithNoDates() {
|
||||
return !this.canUpdate && !this.dueDate && !this.startDate;
|
||||
},
|
||||
labelClass() {
|
||||
return this.isReadonlyWithNoDates ? 'gl-align-self-center gl-pb-0!' : 'gl-mt-3 gl-pb-0!';
|
||||
},
|
||||
showDueDateButton() {
|
||||
return this.canUpdate && !this.showDueDateInput;
|
||||
},
|
||||
showStartDateButton() {
|
||||
return this.canUpdate && !this.showStartDateInput;
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
label: 'item_dates',
|
||||
property: `type_${this.workItemType}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dueDate: {
|
||||
handler(newDueDate) {
|
||||
this.dirtyDueDate = newDateAsLocaleTime(newDueDate);
|
||||
this.showDueDateInput = Boolean(newDueDate);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
startDate: {
|
||||
handler(newStartDate) {
|
||||
this.dirtyStartDate = newDateAsLocaleTime(newStartDate);
|
||||
this.showStartDateInput = Boolean(newStartDate);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clearDueDatePicker() {
|
||||
this.dirtyDueDate = null;
|
||||
this.showDueDateInput = false;
|
||||
this.updateDates();
|
||||
},
|
||||
clearStartDatePicker() {
|
||||
this.dirtyStartDate = null;
|
||||
this.showStartDateInput = false;
|
||||
this.updateDates();
|
||||
},
|
||||
async clickShowDueDate() {
|
||||
this.showDueDateInput = true;
|
||||
await this.$nextTick();
|
||||
this.$refs.dueDatePicker.calendar.show();
|
||||
},
|
||||
async clickShowStartDate() {
|
||||
this.showStartDateInput = true;
|
||||
await this.$nextTick();
|
||||
this.$refs.startDatePicker.calendar.show();
|
||||
},
|
||||
handleStartDateInput() {
|
||||
if (this.dirtyDueDate && this.dirtyStartDate > this.dirtyDueDate) {
|
||||
this.dirtyDueDate = this.dirtyStartDate;
|
||||
this.clickShowDueDate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateDates();
|
||||
},
|
||||
updateDates() {
|
||||
if (!this.canUpdate || this.datesUnchanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.track('updated_dates');
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.workItemId,
|
||||
startAndDueDateWidget: {
|
||||
dueDate: getDateWithUTC(this.dirtyDueDate),
|
||||
startDate: getDateWithUTC(this.dirtyStartDate),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.workItemUpdate.errors.length) {
|
||||
throw new Error(data.workItemUpdate.errors.join('; '));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
|
||||
this.$emit('error', message);
|
||||
Sentry.captureException(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isUpdating = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-group
|
||||
class="work-item-due-date"
|
||||
:label="$options.i18n.dates"
|
||||
:label-class="labelClass"
|
||||
label-cols="3"
|
||||
label-cols-lg="2"
|
||||
>
|
||||
<span v-if="isReadonlyWithNoDates" class="gl-text-gray-400 gl-ml-4">
|
||||
{{ $options.i18n.none }}
|
||||
</span>
|
||||
<div v-else class="gl-display-flex gl-flex-wrap gl-gap-5">
|
||||
<gl-form-group
|
||||
class="gl-display-flex gl-align-items-center gl-m-0"
|
||||
:class="{ 'gl-ml-n3': isReadonlyWithOnlyDueDate }"
|
||||
:label="$options.i18n.startDate"
|
||||
:label-for="$options.startDateInputId"
|
||||
:label-sr-only="!showStartDateInput"
|
||||
label-class="gl-flex-shrink-0 gl-text-secondary gl-font-weight-normal! gl-pb-0! gl-ml-4 gl-mr-3"
|
||||
>
|
||||
<gl-datepicker
|
||||
v-if="showStartDateInput"
|
||||
ref="startDatePicker"
|
||||
v-model="dirtyStartDate"
|
||||
container="body"
|
||||
:disabled="isDatepickerDisabled"
|
||||
:input-id="$options.startDateInputId"
|
||||
show-clear-button
|
||||
:target="null"
|
||||
@clear="clearStartDatePicker"
|
||||
@close="handleStartDateInput"
|
||||
/>
|
||||
<gl-button v-if="showStartDateButton" category="tertiary" @click="clickShowStartDate">
|
||||
{{ $options.i18n.addStartDate }}
|
||||
</gl-button>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
v-if="!isReadonlyWithOnlyStartDate"
|
||||
class="gl-display-flex gl-align-items-center gl-m-0"
|
||||
:class="{ 'gl-ml-n3': isReadonlyWithOnlyDueDate }"
|
||||
:label="$options.i18n.dueDate"
|
||||
:label-for="$options.dueDateInputId"
|
||||
:label-sr-only="!showDueDateInput"
|
||||
label-class="gl-flex-shrink-0 gl-text-secondary gl-font-weight-normal! gl-pb-0! gl-ml-4 gl-mr-3"
|
||||
>
|
||||
<gl-datepicker
|
||||
v-if="showDueDateInput"
|
||||
ref="dueDatePicker"
|
||||
v-model="dirtyDueDate"
|
||||
container="body"
|
||||
:disabled="isDatepickerDisabled"
|
||||
:input-id="$options.dueDateInputId"
|
||||
:min-date="dirtyStartDate"
|
||||
show-clear-button
|
||||
:target="null"
|
||||
@clear="clearDueDatePicker"
|
||||
@close="updateDates"
|
||||
/>
|
||||
<gl-button v-if="showDueDateButton" category="tertiary" @click="clickShowDueDate">
|
||||
{{ $options.i18n.addDueDate }}
|
||||
</gl-button>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
</template>
|
|
@ -14,6 +14,7 @@ export const TASK_TYPE_NAME = 'Task';
|
|||
export const WIDGET_TYPE_ASSIGNEES = 'ASSIGNEES';
|
||||
export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
|
||||
export const WIDGET_TYPE_LABELS = 'LABELS';
|
||||
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
|
||||
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
|
||||
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
|
||||
export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
subscription issuableDatesUpdated($issuableId: IssuableID!) {
|
||||
issuableDatesUpdated(issuableId: $issuableId) {
|
||||
... on WorkItem {
|
||||
id
|
||||
widgets {
|
||||
... on WorkItemWidgetStartAndDueDate {
|
||||
dueDate
|
||||
startDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,11 @@ fragment WorkItemWidgets on WorkItemWidget {
|
|||
}
|
||||
}
|
||||
}
|
||||
... on WorkItemWidgetStartAndDueDate {
|
||||
type
|
||||
dueDate
|
||||
startDate
|
||||
}
|
||||
... on WorkItemWidgetHierarchy {
|
||||
type
|
||||
parent {
|
||||
|
|
|
@ -793,11 +793,13 @@ table.code {
|
|||
}
|
||||
|
||||
.diff-comments-more-count,
|
||||
.diff-notes-collapse {
|
||||
.diff-notes-collapse,
|
||||
.diff-codequality-collapse {
|
||||
@include avatar-counter(50%);
|
||||
}
|
||||
|
||||
.diff-notes-collapse {
|
||||
.diff-notes-collapse,
|
||||
.diff-codequality-collapse {
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
|
@ -982,7 +984,8 @@ table.code {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.diff-notes-collapse {
|
||||
.diff-notes-collapse,
|
||||
.diff-codequality-collapse {
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
}
|
||||
|
@ -1112,6 +1115,7 @@ table.code {
|
|||
}
|
||||
|
||||
.diff-notes-collapse,
|
||||
.diff-codequality-collapse,
|
||||
.note,
|
||||
.discussion-reply-holder {
|
||||
display: none;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
#weight-widget-input:not(:hover, :focus),
|
||||
#weight-widget-input[readonly] {
|
||||
box-shadow: inset 0 0 0 $gl-border-size-1 var(--white, $white);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#weight-widget-input[readonly] {
|
||||
|
@ -26,6 +26,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
.work-item-due-date {
|
||||
.gl-datepicker-input.gl-form-input.form-control {
|
||||
width: 10rem;
|
||||
|
||||
&:not(:focus, :hover) {
|
||||
box-shadow: none;
|
||||
|
||||
~ .gl-datepicker-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background-color: var(--white, $white);
|
||||
box-shadow: none;
|
||||
|
||||
~ .gl-datepicker-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gl-datepicker-actions:focus,
|
||||
.gl-datepicker-actions:hover {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.work-item-labels {
|
||||
.gl-token {
|
||||
padding-left: $gl-spacing-scale-1;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
# rubocop: disable Graphql/AuthorizeTypes
|
||||
class GroupVariableConnectionType < GraphQL::Types::Relay::BaseConnection
|
||||
field :limit, GraphQL::Types::Int,
|
||||
null: false,
|
||||
description: 'Maximum amount of group CI/CD variables.'
|
||||
|
||||
def limit
|
||||
::Plan.default.actual_limits.group_ci_variables
|
||||
end
|
||||
end
|
||||
# rubocop: enable Graphql/AuthorizeTypes
|
||||
end
|
||||
end
|
|
@ -7,6 +7,7 @@ module Types
|
|||
graphql_name 'CiGroupVariable'
|
||||
description 'CI/CD variables for a group.'
|
||||
|
||||
connection_type_class(Types::Ci::GroupVariableConnectionType)
|
||||
implements(VariableInterface)
|
||||
|
||||
field :environment_scope, GraphQL::Types::String,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
# rubocop: disable Graphql/AuthorizeTypes
|
||||
class ProjectVariableConnectionType < GraphQL::Types::Relay::BaseConnection
|
||||
field :limit, GraphQL::Types::Int,
|
||||
null: false,
|
||||
description: 'Maximum amount of project CI/CD variables.'
|
||||
|
||||
def limit
|
||||
::Plan.default.actual_limits.project_ci_variables
|
||||
end
|
||||
end
|
||||
# rubocop: enable Graphql/AuthorizeTypes
|
||||
end
|
||||
end
|
|
@ -7,6 +7,7 @@ module Types
|
|||
graphql_name 'CiProjectVariable'
|
||||
description 'CI/CD variables for a project.'
|
||||
|
||||
connection_type_class(Types::Ci::ProjectVariableConnectionType)
|
||||
implements(VariableInterface)
|
||||
|
||||
field :environment_scope, GraphQL::Types::String,
|
||||
|
|
|
@ -107,16 +107,14 @@ module CounterAttribute
|
|||
end
|
||||
|
||||
def delayed_increment_counter(attribute, increment)
|
||||
raise ArgumentError, "#{attribute} is not a counter attribute" unless counter_attribute_enabled?(attribute)
|
||||
|
||||
return if increment == 0
|
||||
|
||||
run_after_commit_or_now do
|
||||
if counter_attribute_enabled?(attribute)
|
||||
increment_counter(attribute, increment)
|
||||
increment_counter(attribute, increment)
|
||||
|
||||
FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
|
||||
else
|
||||
legacy_increment!(attribute, increment)
|
||||
end
|
||||
FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
|
||||
end
|
||||
|
||||
true
|
||||
|
@ -172,10 +170,6 @@ module CounterAttribute
|
|||
end
|
||||
end
|
||||
|
||||
def legacy_increment!(attribute, increment)
|
||||
increment!(attribute, increment)
|
||||
end
|
||||
|
||||
def unsafe_update_counters(id, increments)
|
||||
self.class.update_counters(id, increments)
|
||||
end
|
||||
|
|
|
@ -10,6 +10,6 @@ module RequestAwareEntity
|
|||
end
|
||||
|
||||
def request
|
||||
options.fetch(:request)
|
||||
options.fetch(:request, nil)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ci_cost_factors_narrow_os_contribution_by_plan
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96273
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372263
|
||||
milestone: '15.4'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -6323,6 +6323,7 @@ The connection type for [`CiGroupVariable`](#cigroupvariable).
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="cigroupvariableconnectionedges"></a>`edges` | [`[CiGroupVariableEdge]`](#cigroupvariableedge) | A list of edges. |
|
||||
| <a id="cigroupvariableconnectionlimit"></a>`limit` | [`Int!`](#int) | Maximum amount of group CI/CD variables. |
|
||||
| <a id="cigroupvariableconnectionnodes"></a>`nodes` | [`[CiGroupVariable]`](#cigroupvariable) | A list of nodes. |
|
||||
| <a id="cigroupvariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
||||
|
@ -6498,6 +6499,7 @@ The connection type for [`CiProjectVariable`](#ciprojectvariable).
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="ciprojectvariableconnectionedges"></a>`edges` | [`[CiProjectVariableEdge]`](#ciprojectvariableedge) | A list of edges. |
|
||||
| <a id="ciprojectvariableconnectionlimit"></a>`limit` | [`Int!`](#int) | Maximum amount of project CI/CD variables. |
|
||||
| <a id="ciprojectvariableconnectionnodes"></a>`nodes` | [`[CiProjectVariable]`](#ciprojectvariable) | A list of nodes. |
|
||||
| <a id="ciprojectvariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
||||
|
|
|
@ -1114,6 +1114,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
|
|||
|
||||
> - Immediately deleting subgroups was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/360008) in GitLab 15.3 [with a flag](../administration/feature_flags.md) named `immediate_delete_subgroup_api`. Disabled by default.
|
||||
> - Immediately deleting subgroups was [enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/368276) in GitLab 15.4.
|
||||
> - Immediately deleting subgroups was [enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/368276) by default in GitLab 15.4.
|
||||
|
||||
Only available to group owners and administrators.
|
||||
|
||||
|
|
|
@ -203,7 +203,7 @@ The cost factors for jobs running on shared runners on GitLab.com are:
|
|||
|
||||
- `1` for internal and private projects.
|
||||
- `0.5` for public projects in the [GitLab for Open Source program](../../subscriptions/index.md#gitlab-for-open-source).
|
||||
- `0.008` for public forks of public projects. For every 125 minutes of job execution time,
|
||||
- `0.008` for public forks of public projects in the [GitLab for Open Source program](../../subscriptions/index.md#gitlab-for-open-source). For every 125 minutes of job execution time,
|
||||
you use 1 CI/CD minute.
|
||||
- `0.04` for other public projects, after September 1, 2022 (previously `0.008`).
|
||||
For every 25 minutes of job execution time, you use 1 CI/CD minute.
|
||||
|
|
|
@ -114,6 +114,35 @@ To change the assignee on a task:
|
|||
1. From the dropdown list, select the user(s) to add as an assignee.
|
||||
1. Select any area outside the dropdown list.
|
||||
|
||||
## Set a start and due date
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/365399) in GitLab 15.4.
|
||||
|
||||
You can set a [start and due date](project/issues/due_dates.md) on a task.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Reporter role for the project.
|
||||
|
||||
You can set start and due dates on a task to show when work should begin and end.
|
||||
|
||||
To set a due date:
|
||||
|
||||
1. In the issue description, in the **Tasks** section, select the title of the task you want to edit.
|
||||
The task window opens.
|
||||
1. If the task already has a due date next to **Due date**, select it. Otherwise, select **Add due date**.
|
||||
1. In the date picker, select the desired due date.
|
||||
|
||||
To set a start date:
|
||||
|
||||
1. In the issue description, in the **Tasks** section, select the title of the task you want to edit.
|
||||
The task window opens.
|
||||
1. If the task already has a start date next to **Start date**, select it. Otherwise, select **Add start date**.
|
||||
1. In the date picker, select the desired due date.
|
||||
|
||||
The due date must be the same or later than the start date.
|
||||
If you select a start date to be later than the due date, the due date is then changed to the same day.
|
||||
|
||||
## Set task weight **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362550) in GitLab 15.3.
|
||||
|
|
|
@ -3,9 +3,13 @@
|
|||
module API
|
||||
module Entities
|
||||
class UserSafe < Grape::Entity
|
||||
include RequestAwareEntity
|
||||
|
||||
expose :id, :username
|
||||
expose :name do |user|
|
||||
user.redacted_name(options[:current_user])
|
||||
current_user = request.respond_to?(:current_user) ? request.current_user : options.fetch(:current_user, nil)
|
||||
|
||||
user.redacted_name(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,19 +36,12 @@ sast:
|
|||
|
||||
bandit-sast:
|
||||
extends: .sast-analyzer
|
||||
image:
|
||||
name: "$SAST_ANALYZER_IMAGE"
|
||||
variables:
|
||||
SAST_ANALYZER_IMAGE_TAG: 2
|
||||
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG"
|
||||
script:
|
||||
- echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.4"
|
||||
- echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
|
||||
- exit 1
|
||||
rules:
|
||||
- if: $SAST_DISABLED
|
||||
when: never
|
||||
- if: $SAST_EXCLUDED_ANALYZERS =~ /bandit/
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
exists:
|
||||
- '**/*.py'
|
||||
- when: never
|
||||
|
||||
brakeman-sast:
|
||||
extends: .sast-analyzer
|
||||
|
@ -69,23 +62,12 @@ brakeman-sast:
|
|||
|
||||
eslint-sast:
|
||||
extends: .sast-analyzer
|
||||
image:
|
||||
name: "$SAST_ANALYZER_IMAGE"
|
||||
variables:
|
||||
SAST_ANALYZER_IMAGE_TAG: 2
|
||||
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
|
||||
script:
|
||||
- echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.4"
|
||||
- echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
|
||||
- exit 1
|
||||
rules:
|
||||
- if: $SAST_DISABLED
|
||||
when: never
|
||||
- if: $SAST_EXCLUDED_ANALYZERS =~ /eslint/
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
exists:
|
||||
- '**/*.html'
|
||||
- '**/*.js'
|
||||
- '**/*.jsx'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- when: never
|
||||
|
||||
flawfinder-sast:
|
||||
extends: .sast-analyzer
|
||||
|
@ -125,19 +107,12 @@ kubesec-sast:
|
|||
|
||||
gosec-sast:
|
||||
extends: .sast-analyzer
|
||||
image:
|
||||
name: "$SAST_ANALYZER_IMAGE"
|
||||
variables:
|
||||
SAST_ANALYZER_IMAGE_TAG: 3
|
||||
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG"
|
||||
script:
|
||||
- echo "This job was deprecated in GitLab 14.8 and removed in GitLab 15.4"
|
||||
- echo "For more information see https://gitlab.com/gitlab-org/gitlab/-/issues/352554"
|
||||
- exit 1
|
||||
rules:
|
||||
- if: $SAST_DISABLED
|
||||
when: never
|
||||
- if: $SAST_EXCLUDED_ANALYZERS =~ /gosec/
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
exists:
|
||||
- '**/*.go'
|
||||
- when: never
|
||||
|
||||
.mobsf-sast:
|
||||
extends: .sast-analyzer
|
||||
|
@ -262,6 +237,7 @@ semgrep-sast:
|
|||
- '**/*.go'
|
||||
- '**/*.java'
|
||||
- '**/*.cs'
|
||||
- '**/*.html'
|
||||
|
||||
sobelow-sast:
|
||||
extends: .sast-analyzer
|
||||
|
@ -298,6 +274,5 @@ spotbugs-sast:
|
|||
- if: $CI_COMMIT_BRANCH
|
||||
exists:
|
||||
- '**/*.groovy'
|
||||
- '**/*.java'
|
||||
- '**/*.scala'
|
||||
- '**/*.kt'
|
||||
|
|
|
@ -45008,6 +45008,12 @@ msgstr ""
|
|||
msgid "WorkItem|Add assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add due date"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add start date"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add task"
|
||||
msgstr ""
|
||||
|
||||
|
@ -45040,9 +45046,15 @@ msgstr ""
|
|||
msgid "WorkItem|Create work item"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Dates"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Delete %{workItemType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Due date"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Expand tasks"
|
||||
msgstr ""
|
||||
|
||||
|
@ -45109,6 +45121,9 @@ msgstr ""
|
|||
msgid "WorkItem|Something went wrong while updating the work item. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Start date"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Task"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ describe('Autosave', () => {
|
|||
|
||||
let autosave;
|
||||
const field = $('<textarea></textarea>');
|
||||
const checkbox = $('<input type="checkbox">');
|
||||
const key = 'key';
|
||||
const fallbackKey = 'fallbackKey';
|
||||
const lockVersionKey = 'lockVersionKey';
|
||||
|
@ -90,6 +91,24 @@ describe('Autosave', () => {
|
|||
expect(eventHandler).toHaveBeenCalledTimes(1);
|
||||
fieldElement.removeEventListener('change', eventHandler);
|
||||
});
|
||||
|
||||
describe('if field type is checkbox', () => {
|
||||
beforeEach(() => {
|
||||
autosave = {
|
||||
field: checkbox,
|
||||
key,
|
||||
isLocalStorageAvailable: true,
|
||||
type: 'checkbox',
|
||||
};
|
||||
});
|
||||
|
||||
it('should restore', () => {
|
||||
window.localStorage.setItem(key, true);
|
||||
expect(checkbox.is(':checked')).toBe(false);
|
||||
Autosave.prototype.restore.call(autosave);
|
||||
expect(checkbox.is(':checked')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if field gets deleted from DOM', () => {
|
||||
|
@ -169,6 +188,31 @@ describe('Autosave', () => {
|
|||
expect(window.localStorage.setItem).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if field type is checkbox', () => {
|
||||
beforeEach(() => {
|
||||
autosave = {
|
||||
field: checkbox,
|
||||
key,
|
||||
isLocalStorageAvailable: true,
|
||||
type: 'checkbox',
|
||||
};
|
||||
});
|
||||
|
||||
it('should save true when checkbox on', () => {
|
||||
checkbox.prop('checked', true);
|
||||
Autosave.prototype.save.call(autosave);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(key, true);
|
||||
});
|
||||
|
||||
it('should call reset when checkbox off', () => {
|
||||
autosave.reset = jest.fn();
|
||||
checkbox.prop('checked', false);
|
||||
Autosave.prototype.save.call(autosave);
|
||||
expect(autosave.reset).toHaveBeenCalled();
|
||||
expect(window.localStorage.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('save with lockVersion', () => {
|
||||
|
|
|
@ -85,18 +85,25 @@ describe('IssuableForm', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('creates due_date autosave when due_date input exist', () => {
|
||||
$form.append(`<input type="text" name="issue[due_date]"/>`);
|
||||
const $dueDate = $form.find(`input[name*="[due_date]"]`);
|
||||
it.each([
|
||||
{
|
||||
id: 'confidential',
|
||||
input: '<input type="checkbox" name="issue[confidential]"/>',
|
||||
selector: 'input[name*="[confidential]"]',
|
||||
},
|
||||
{
|
||||
id: 'due_date',
|
||||
input: '<input type="text" name="issue[due_date]"/>',
|
||||
selector: 'input[name*="[due_date]"]',
|
||||
},
|
||||
])('creates $id autosave when $id input exist', ({ id, input, selector }) => {
|
||||
$form.append(input);
|
||||
const $input = $form.find(selector);
|
||||
const totalAutosaveFormFields = $form.children().length;
|
||||
createIssuable($form);
|
||||
|
||||
expect(Autosave).toHaveBeenCalledTimes(totalAutosaveFormFields);
|
||||
expect(Autosave).toHaveBeenLastCalledWith(
|
||||
$dueDate,
|
||||
['/', '', 'due_date'],
|
||||
`autosave///=due_date`,
|
||||
);
|
||||
expect(Autosave).toHaveBeenLastCalledWith($input, ['/', '', id], `autosave///=${id}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -129,12 +136,15 @@ describe('IssuableForm', () => {
|
|||
expect(resetAutosave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('creates due_date autosave when due_date input exist', () => {
|
||||
$form.append(`<input type="text" name="issue[due_date]"/>`);
|
||||
it.each([
|
||||
{ id: 'confidential', input: '<input type="checkbox" name="issue[confidential]"/>' },
|
||||
{ id: 'due_date', input: '<input type="text" name="issue[due_date]"/>' },
|
||||
])('calls reset on autosave $id when $id input exist', ({ id, input }) => {
|
||||
$form.append(input);
|
||||
instance = createIssuable($form);
|
||||
instance.resetAutosave();
|
||||
|
||||
expect(instance.autosaves.get('due_date').reset).toHaveBeenCalledTimes(1);
|
||||
expect(instance.autosaves.get(id).reset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
|
||||
import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
|
||||
|
||||
describe('newDateAsLocaleTime', () => {
|
||||
it.each`
|
||||
|
@ -15,3 +15,19 @@ describe('newDateAsLocaleTime', () => {
|
|||
expect(newDateAsLocaleTime(string)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDateWithUTC', () => {
|
||||
it.each`
|
||||
date | expected
|
||||
${new Date('2022-03-22T01:23:45.678Z')} | ${new Date('2022-03-22T00:00:00.000Z')}
|
||||
${new Date('1999-12-31T23:59:59.999Z')} | ${new Date('1999-12-31T00:00:00.000Z')}
|
||||
${2022} | ${null}
|
||||
${[]} | ${null}
|
||||
${{}} | ${null}
|
||||
${true} | ${null}
|
||||
${null} | ${null}
|
||||
${undefined} | ${null}
|
||||
`('returns $expected given $string', ({ date, expected }) => {
|
||||
expect(getDateWithUTC(date)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
|||
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
|
||||
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
|
||||
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
|
||||
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
|
||||
import WorkItemState from '~/work_items/components/work_item_state.vue';
|
||||
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
|
||||
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
||||
|
@ -16,15 +17,17 @@ import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
|
|||
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
|
||||
import { i18n } from '~/work_items/constants';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
|
||||
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
|
||||
import { temporaryConfig } from '~/graphql_shared/issuable_client';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import {
|
||||
workItemTitleSubscriptionResponse,
|
||||
workItemResponseFactory,
|
||||
mockParent,
|
||||
workItemDatesSubscriptionResponse,
|
||||
workItemResponseFactory,
|
||||
workItemTitleSubscriptionResponse,
|
||||
workItemWeightSubscriptionResponse,
|
||||
} from '../mock_data';
|
||||
|
||||
|
@ -41,7 +44,8 @@ describe('WorkItemDetail component', () => {
|
|||
canDelete: true,
|
||||
});
|
||||
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
|
||||
const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
|
||||
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
|
||||
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
|
||||
const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
@ -51,6 +55,7 @@ describe('WorkItemDetail component', () => {
|
|||
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
|
||||
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
|
||||
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
|
||||
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
|
||||
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
|
||||
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
|
||||
const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
|
||||
|
@ -65,7 +70,7 @@ describe('WorkItemDetail component', () => {
|
|||
updateInProgress = false,
|
||||
workItemId = workItemQueryResponse.data.workItem.id,
|
||||
handler = successHandler,
|
||||
subscriptionHandler = initialSubscriptionHandler,
|
||||
subscriptionHandler = titleSubscriptionHandler,
|
||||
confidentialityMock = [updateWorkItemMutation, jest.fn()],
|
||||
workItemsMvc2Enabled = false,
|
||||
includeWidgets = false,
|
||||
|
@ -74,6 +79,7 @@ describe('WorkItemDetail component', () => {
|
|||
const handlers = [
|
||||
[workItemQuery, handler],
|
||||
[workItemTitleSubscription, subscriptionHandler],
|
||||
[workItemDatesSubscription, datesSubscriptionHandler],
|
||||
confidentialityMock,
|
||||
];
|
||||
|
||||
|
@ -399,11 +405,37 @@ describe('WorkItemDetail component', () => {
|
|||
expect(findAlert().text()).toBe(updateError);
|
||||
});
|
||||
|
||||
it('calls the subscription', () => {
|
||||
createComponent();
|
||||
describe('subscriptions', () => {
|
||||
it('calls the title subscription', () => {
|
||||
createComponent();
|
||||
|
||||
expect(initialSubscriptionHandler).toHaveBeenCalledWith({
|
||||
issuableId: workItemQueryResponse.data.workItem.id,
|
||||
expect(titleSubscriptionHandler).toHaveBeenCalledWith({
|
||||
issuableId: workItemQueryResponse.data.workItem.id,
|
||||
});
|
||||
});
|
||||
|
||||
describe('dates subscription', () => {
|
||||
describe('when the due date widget exists', () => {
|
||||
it('calls the dates subscription', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(datesSubscriptionHandler).toHaveBeenCalledWith({
|
||||
issuableId: workItemQueryResponse.data.workItem.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the due date widget does not exist', () => {
|
||||
it('does not call the dates subscription', async () => {
|
||||
const response = workItemResponseFactory({ datesWidgetPresent: false });
|
||||
const handler = jest.fn().mockResolvedValue(response);
|
||||
createComponent({ handler, workItemsMvc2Enabled: true });
|
||||
await waitForPromises();
|
||||
|
||||
expect(datesSubscriptionHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -443,6 +475,34 @@ describe('WorkItemDetail component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('dates widget', () => {
|
||||
describe.each`
|
||||
description | datesWidgetPresent | exists
|
||||
${'when widget is returned from API'} | ${true} | ${true}
|
||||
${'when widget is not returned from API'} | ${false} | ${false}
|
||||
`('$description', ({ datesWidgetPresent, exists }) => {
|
||||
it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
|
||||
const response = workItemResponseFactory({ datesWidgetPresent });
|
||||
const handler = jest.fn().mockResolvedValue(response);
|
||||
createComponent({ handler, workItemsMvc2Enabled: true });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemDueDate().exists()).toBe(exists);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows an error message when it emits an `error` event', async () => {
|
||||
createComponent({ workItemsMvc2Enabled: true });
|
||||
await waitForPromises();
|
||||
const updateError = 'Failed to update';
|
||||
|
||||
findWorkItemDueDate().vm.$emit('error', updateError);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAlert().text()).toBe(updateError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('work item information', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
|
|
@ -0,0 +1,346 @@
|
|||
import { GlFormGroup, GlDatepicker } from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
|
||||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import { updateWorkItemMutationResponse, updateWorkItemMutationErrorResponse } from '../mock_data';
|
||||
|
||||
describe('WorkItemDueDate component', () => {
|
||||
let wrapper;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const workItemId = 'gid://gitlab/WorkItem/1';
|
||||
const updateWorkItemMutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
|
||||
|
||||
const findStartDateButton = () =>
|
||||
wrapper.findByRole('button', { name: WorkItemDueDate.i18n.addStartDate });
|
||||
const findStartDateInput = () => wrapper.findByLabelText(WorkItemDueDate.i18n.startDate);
|
||||
const findStartDatePicker = () => wrapper.findComponent(GlDatepicker);
|
||||
const findDueDateButton = () =>
|
||||
wrapper.findByRole('button', { name: WorkItemDueDate.i18n.addDueDate });
|
||||
const findDueDateInput = () => wrapper.findByLabelText(WorkItemDueDate.i18n.dueDate);
|
||||
const findDueDatePicker = () => wrapper.findAllComponents(GlDatepicker).at(1);
|
||||
const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
|
||||
|
||||
const createComponent = ({
|
||||
canUpdate = false,
|
||||
dueDate = null,
|
||||
startDate = null,
|
||||
mutationHandler = updateWorkItemMutationHandler,
|
||||
} = {}) => {
|
||||
wrapper = mountExtended(WorkItemDueDate, {
|
||||
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
|
||||
propsData: {
|
||||
canUpdate,
|
||||
dueDate,
|
||||
startDate,
|
||||
workItemId,
|
||||
workItemType: 'Task',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when can update', () => {
|
||||
describe('start date', () => {
|
||||
describe('`Add start date` button', () => {
|
||||
describe.each`
|
||||
description | startDate | exists
|
||||
${'when there is no start date'} | ${null} | ${true}
|
||||
${'when there is a start date'} | ${'2022-01-01'} | ${false}
|
||||
`('$description', ({ startDate, exists }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, startDate });
|
||||
});
|
||||
|
||||
it(exists ? 'renders' : 'does not render', () => {
|
||||
expect(findStartDateButton().exists()).toBe(exists);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it emits `click` event', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, startDate: null });
|
||||
findStartDateButton().vm.$emit('click');
|
||||
});
|
||||
|
||||
it('renders start date picker', () => {
|
||||
expect(findStartDateInput().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hides itself', () => {
|
||||
expect(findStartDateButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('date picker', () => {
|
||||
describe('when it emits a `clear` event', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
|
||||
findStartDatePicker().vm.$emit('clear');
|
||||
});
|
||||
|
||||
it('hides the date picker', () => {
|
||||
expect(findStartDateInput().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the `Add start date` button', () => {
|
||||
expect(findStartDateButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls a mutation to update the dates', () => {
|
||||
expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: workItemId,
|
||||
startAndDueDateWidget: {
|
||||
dueDate: new Date('2022-01-01T00:00:00.000Z'),
|
||||
startDate: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it emits a `close` event', () => {
|
||||
describe('when the start date is earlier than the due date', () => {
|
||||
const startDate = new Date('2022-01-01T00:00:00.000Z');
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
|
||||
findStartDatePicker().vm.$emit('input', startDate);
|
||||
findStartDatePicker().vm.$emit('close');
|
||||
});
|
||||
|
||||
it('calls a mutation to update the dates', () => {
|
||||
expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: workItemId,
|
||||
startAndDueDateWidget: {
|
||||
dueDate: new Date('2022-12-31T00:00:00.000Z'),
|
||||
startDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the start date is later than the due date', () => {
|
||||
const startDate = new Date('2030-01-01T00:00:00.000Z');
|
||||
let datePickerOpenSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
|
||||
datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker.calendar, 'show');
|
||||
findStartDatePicker().vm.$emit('input', startDate);
|
||||
findStartDatePicker().vm.$emit('close');
|
||||
});
|
||||
|
||||
it('does not call a mutation to update the dates', () => {
|
||||
expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates the due date picker to the same date', () => {
|
||||
expect(findDueDatePicker().props('value')).toEqual(startDate);
|
||||
});
|
||||
|
||||
it('opens the due date picker', () => {
|
||||
expect(datePickerOpenSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('due date', () => {
|
||||
describe('`Add due date` button', () => {
|
||||
describe.each`
|
||||
description | dueDate | exists
|
||||
${'when there is no due date'} | ${null} | ${true}
|
||||
${'when there is a due date'} | ${'2022-01-01'} | ${false}
|
||||
`('$description', ({ dueDate, exists }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate });
|
||||
});
|
||||
|
||||
it(exists ? 'renders' : 'does not render', () => {
|
||||
expect(findDueDateButton().exists()).toBe(exists);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it emits `click` event', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate: null });
|
||||
findDueDateButton().vm.$emit('click');
|
||||
});
|
||||
|
||||
it('renders due date picker', () => {
|
||||
expect(findDueDateInput().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hides itself', () => {
|
||||
expect(findDueDateButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('date picker', () => {
|
||||
describe('when it emits a `clear` event', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
|
||||
findDueDatePicker().vm.$emit('clear');
|
||||
});
|
||||
|
||||
it('hides the date picker', () => {
|
||||
expect(findDueDateInput().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the `Add due date` button', () => {
|
||||
expect(findDueDateButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls a mutation to update the dates', () => {
|
||||
expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: workItemId,
|
||||
startAndDueDateWidget: {
|
||||
dueDate: null,
|
||||
startDate: new Date('2022-01-01T00:00:00.000Z'),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it emits a `close` event', () => {
|
||||
const dueDate = new Date('2022-12-31T00:00:00.000Z');
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
|
||||
findDueDatePicker().vm.$emit('input', dueDate);
|
||||
findDueDatePicker().vm.$emit('close');
|
||||
});
|
||||
|
||||
it('calls a mutation to update the dates', () => {
|
||||
expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: workItemId,
|
||||
startAndDueDateWidget: {
|
||||
dueDate,
|
||||
startDate: new Date('2022-01-01T00:00:00.000Z'),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating date', () => {
|
||||
describe('when dates are changed', () => {
|
||||
let trackingSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
|
||||
findStartDatePicker().vm.$emit('input', new Date('2022-01-01T00:00:00.000Z'));
|
||||
findStartDatePicker().vm.$emit('close');
|
||||
});
|
||||
|
||||
it('mutation is called to update dates', () => {
|
||||
expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: workItemId,
|
||||
startAndDueDateWidget: {
|
||||
dueDate: new Date('2022-12-31T00:00:00.000Z'),
|
||||
startDate: new Date('2022-01-01T00:00:00.000Z'),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('start date input is disabled', () => {
|
||||
expect(findStartDatePicker().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('due date input is disabled', () => {
|
||||
expect(findDueDatePicker().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('tracks updating the dates', () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_dates', {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
label: 'item_dates',
|
||||
property: 'type_Task',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dates are unchanged', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
|
||||
|
||||
findStartDatePicker().vm.$emit('input', new Date('2022-12-31T00:00:00.000Z'));
|
||||
findStartDatePicker().vm.$emit('close');
|
||||
});
|
||||
|
||||
it('mutation is not called to update dates', () => {
|
||||
expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
description | mutationHandler
|
||||
${'when there is a GraphQL error'} | ${jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse)}
|
||||
${'when there is a network error'} | ${jest.fn().mockRejectedValue(new Error())}
|
||||
`('$description', ({ mutationHandler }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
canUpdate: true,
|
||||
dueDate: '2022-12-31',
|
||||
startDate: '2022-12-31',
|
||||
mutationHandler,
|
||||
});
|
||||
|
||||
findStartDatePicker().vm.$emit('input', new Date('2022-01-01T00:00:00.000Z'));
|
||||
findStartDatePicker().vm.$emit('close');
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('emits an error', () => {
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong while updating the task. Please try again.'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cannot update', () => {
|
||||
it('start and due date inputs are disabled', async () => {
|
||||
createComponent({ canUpdate: false, dueDate: '2022-01-01', startDate: '2022-01-01' });
|
||||
await nextTick();
|
||||
|
||||
expect(findStartDateInput().props('disabled')).toBe(true);
|
||||
expect(findDueDateInput().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
describe('when there is no start and due date', () => {
|
||||
it('shows None', () => {
|
||||
createComponent({ canUpdate: false, dueDate: null, startDate: null });
|
||||
|
||||
expect(findGlFormGroup().text()).toContain(WorkItemDueDate.i18n.none);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -138,6 +138,16 @@ export const updateWorkItemMutationResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const updateWorkItemMutationErrorResponse = {
|
||||
data: {
|
||||
workItemUpdate: {
|
||||
__typename: 'WorkItemUpdatePayload',
|
||||
errors: ['Error!'],
|
||||
workItem: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockParent = {
|
||||
parent: {
|
||||
id: 'gid://gitlab/Issue/1',
|
||||
|
@ -152,6 +162,7 @@ export const workItemResponseFactory = ({
|
|||
canDelete = false,
|
||||
allowsMultipleAssignees = true,
|
||||
assigneesWidgetPresent = true,
|
||||
datesWidgetPresent = true,
|
||||
weightWidgetPresent = true,
|
||||
confidential = false,
|
||||
canInviteMembers = false,
|
||||
|
@ -201,6 +212,14 @@ export const workItemResponseFactory = ({
|
|||
},
|
||||
}
|
||||
: { type: 'MOCK TYPE' },
|
||||
datesWidgetPresent
|
||||
? {
|
||||
__typename: 'WorkItemWidgetStartAndDueDate',
|
||||
type: 'START_AND_DUE_DATE',
|
||||
dueDate: '2022-12-31',
|
||||
startDate: '2022-01-01',
|
||||
}
|
||||
: { type: 'MOCK TYPE' },
|
||||
weightWidgetPresent
|
||||
? {
|
||||
__typename: 'WorkItemWidgetWeight',
|
||||
|
@ -387,6 +406,21 @@ export const deleteWorkItemFromTaskMutationErrorResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const workItemDatesSubscriptionResponse = {
|
||||
data: {
|
||||
issuableDatesUpdated: {
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
widgets: [
|
||||
{
|
||||
__typename: 'WorkItemWidgetStartAndDueDate',
|
||||
dueDate: '2022-12-31',
|
||||
startDate: '2022-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const workItemTitleSubscriptionResponse = {
|
||||
data: {
|
||||
issuableTitleUpdated: {
|
||||
|
|
|
@ -4,12 +4,14 @@ import VueApollo from 'vue-apollo';
|
|||
import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import {
|
||||
workItemDatesSubscriptionResponse,
|
||||
workItemResponseFactory,
|
||||
workItemTitleSubscriptionResponse,
|
||||
workItemWeightSubscriptionResponse,
|
||||
} from 'jest/work_items/mock_data';
|
||||
import App from '~/work_items/components/app.vue';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
|
||||
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
|
||||
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
|
||||
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
|
||||
|
@ -21,6 +23,7 @@ describe('Work items router', () => {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
|
||||
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
|
||||
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
|
||||
const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
|
||||
|
||||
|
@ -32,6 +35,7 @@ describe('Work items router', () => {
|
|||
|
||||
const handlers = [
|
||||
[workItemQuery, workItemQueryHandler],
|
||||
[workItemDatesSubscription, datesSubscriptionHandler],
|
||||
[workItemTitleSubscription, titleSubscriptionHandler],
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['CiGroupVariableConnection'] do
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %i[limit page_info edges nodes]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['CiProjectVariableConnection'] do
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %i[limit page_info edges nodes]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
|
@ -13,6 +13,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
|
|||
query {
|
||||
group(fullPath: "#{group.full_path}") {
|
||||
ciVariables {
|
||||
limit
|
||||
nodes {
|
||||
id
|
||||
key
|
||||
|
@ -46,6 +47,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
|
|||
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
expect(graphql_data.dig('group', 'ciVariables', 'limit')).to be(200)
|
||||
expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({
|
||||
'id' => variable.to_global_id.to_s,
|
||||
'key' => 'TEST_VAR',
|
||||
|
|
|
@ -13,6 +13,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
|
|||
query {
|
||||
project(fullPath: "#{project.full_path}") {
|
||||
ciVariables {
|
||||
limit
|
||||
nodes {
|
||||
id
|
||||
key
|
||||
|
@ -40,6 +41,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
|
|||
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
expect(graphql_data.dig('project', 'ciVariables', 'limit')).to be(200)
|
||||
expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({
|
||||
'id' => variable.to_global_id.to_s,
|
||||
'key' => 'TEST_VAR',
|
||||
|
|
|
@ -47,6 +47,54 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
context 'when pipeline has a job with environment' do
|
||||
let(:pipeline) { execute_service.payload }
|
||||
|
||||
before do
|
||||
stub_ci_pipeline_yaml_file(YAML.dump(config))
|
||||
end
|
||||
|
||||
context 'when environment name is valid' do
|
||||
let(:config) do
|
||||
{
|
||||
review_app: {
|
||||
script: 'deploy',
|
||||
environment: {
|
||||
name: 'review/${CI_COMMIT_REF_NAME}',
|
||||
url: 'http://${CI_COMMIT_REF_SLUG}-staging.example.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'has a job with environment' do
|
||||
expect(pipeline.builds.count).to eq(1)
|
||||
expect(pipeline.builds.first.persisted_environment.name).to eq('review/master')
|
||||
expect(pipeline.builds.first.deployment).to be_created
|
||||
end
|
||||
end
|
||||
|
||||
context 'when environment name is invalid' do
|
||||
let(:config) do
|
||||
{
|
||||
'job:deploy-to-test-site': {
|
||||
script: 'deploy',
|
||||
environment: {
|
||||
name: '${CI_JOB_NAME}',
|
||||
url: 'https://$APP_URL'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'has a job without environment' do
|
||||
expect(pipeline.builds.count).to eq(1)
|
||||
expect(pipeline.builds.first.persisted_environment).to be_nil
|
||||
expect(pipeline.builds.first.deployment).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'performance' do
|
||||
it_behaves_like 'pipelines are created without N+1 SQL queries' do
|
||||
let(:config1) do
|
||||
|
@ -463,7 +511,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
it 'pull it from Auto-DevOps' do
|
||||
pipeline = execute_service.payload
|
||||
expect(pipeline).to be_auto_devops_source
|
||||
expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality container_scanning eslint-sast secret_detection semgrep-sast test])
|
||||
expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality container_scanning secret_detection semgrep-sast test])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1268,54 +1316,6 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
end
|
||||
end
|
||||
|
||||
context 'when pipeline has a job with environment' do
|
||||
let(:pipeline) { execute_service.payload }
|
||||
|
||||
before do
|
||||
stub_ci_pipeline_yaml_file(YAML.dump(config))
|
||||
end
|
||||
|
||||
context 'when environment name is valid' do
|
||||
let(:config) do
|
||||
{
|
||||
review_app: {
|
||||
script: 'deploy',
|
||||
environment: {
|
||||
name: 'review/${CI_COMMIT_REF_NAME}',
|
||||
url: 'http://${CI_COMMIT_REF_SLUG}-staging.example.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'has a job with environment' do
|
||||
expect(pipeline.builds.count).to eq(1)
|
||||
expect(pipeline.builds.first.persisted_environment.name).to eq('review/master')
|
||||
expect(pipeline.builds.first.deployment).to be_created
|
||||
end
|
||||
end
|
||||
|
||||
context 'when environment name is invalid' do
|
||||
let(:config) do
|
||||
{
|
||||
'job:deploy-to-test-site': {
|
||||
script: 'deploy',
|
||||
environment: {
|
||||
name: '${CI_JOB_NAME}',
|
||||
url: 'https://$APP_URL'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'has a job without environment' do
|
||||
expect(pipeline.builds.count).to eq(1)
|
||||
expect(pipeline.builds.first.persisted_environment).to be_nil
|
||||
expect(pipeline.builds.first.deployment).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Pipeline for external pull requests' do
|
||||
let(:response) do
|
||||
execute_service(source: source,
|
||||
|
|
|
@ -75,9 +75,9 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
|
|||
end
|
||||
|
||||
context 'when attribute is not a counter attribute' do
|
||||
it 'delegates to ActiveRecord update!' do
|
||||
it 'raises ArgumentError' do
|
||||
expect { model.delayed_increment_counter(:unknown_attribute, 10) }
|
||||
.to raise_error(ActiveModel::MissingAttributeError)
|
||||
.to raise_error(ArgumentError, 'unknown_attribute is not a counter attribute')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue