diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-28 12:10:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-28 12:10:13 +0300 |
commit | d6024427e8036c93ccf04759a3725167ec6c02f4 (patch) | |
tree | f722805d09e3cf62c76f384bbaae9dccfa6f755e /app | |
parent | c7836133e0d9287e147e9e19099e058a37e87a9a (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
23 files changed, 636 insertions, 232 deletions
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 4fed7f555f6..c2ef6414716 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -45,7 +45,6 @@ function initStatusTriggers() { defaultEmoji, currentMessage, currentAvailability, - canSetUserAvailability, currentClearStatusAfter, } = setStatusModalWrapperEl.dataset; @@ -54,7 +53,6 @@ function initStatusTriggers() { defaultEmoji, currentMessage, currentAvailability, - canSetUserAvailability, currentClearStatusAfter, }; }, @@ -64,7 +62,6 @@ function initStatusTriggers() { defaultEmoji, currentMessage, currentAvailability, - canSetUserAvailability, currentClearStatusAfter, } = this; @@ -74,7 +71,6 @@ function initStatusTriggers() { defaultEmoji, currentMessage, currentAvailability, - canSetUserAvailability, currentClearStatusAfter, }, }); diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index bff90254c04..c754af5c7de 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -67,11 +67,6 @@ export default { required: false, default: '', }, - canSetUserAvailability: { - type: Boolean, - required: false, - default: false, - }, currentClearStatusAfter: { type: String, required: false, @@ -292,7 +287,7 @@ export default { </button> </span> </div> - <div v-if="canSetUserAvailability" class="form-group"> + <div class="form-group"> <div class="gl-display-flex"> <gl-form-checkbox v-model="availability" diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue new file mode 100644 index 00000000000..f2f3671c753 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -0,0 +1,293 @@ +<script> +import { GlIcon, GlDatepicker, GlTooltipDirective, GlLink, GlPopover } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { IssuableType } from '~/issue_show/constants'; +import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility'; +import { __, sprintf } from '~/locale'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { dateFields, dateTypes, dueDateQueries, startDateQueries } from '~/sidebar/constants'; +import SidebarFormattedDate from './sidebar_formatted_date.vue'; +import SidebarInheritDate from './sidebar_inherit_date.vue'; + +const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', { + bubbles: true, +}); + +export default { + tracking: { + event: 'click_edit_button', + label: 'right_sidebar', + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + GlIcon, + GlDatepicker, + GlLink, + GlPopover, + SidebarEditableItem, + SidebarFormattedDate, + SidebarInheritDate, + }, + inject: ['canUpdate'], + props: { + iid: { + type: String, + required: true, + }, + fullPath: { + type: String, + required: true, + }, + dateType: { + type: String, + required: false, + default: dateTypes.due, + }, + issuableType: { + required: true, + type: String, + }, + canInherit: { + required: false, + type: Boolean, + default: false, + }, + }, + data() { + return { + issuable: {}, + loading: false, + tracking: { + ...this.$options.tracking, + property: this.dateType === dateTypes.start ? 'startDate' : 'dueDate', + }, + }; + }, + apollo: { + issuable: { + query() { + return this.dateQueries[this.issuableType].query; + }, + variables() { + return { + fullPath: this.fullPath, + iid: String(this.iid), + }; + }, + update(data) { + return data.workspace?.issuable || {}; + }, + result({ data }) { + this.$emit(`${this.dateType}Updated`, data.workspace?.issuable?.[this.dateType]); + }, + error() { + createFlash({ + message: sprintf( + __('Something went wrong while setting %{issuableType} %{dateType} date.'), + { + issuableType: this.issuableType, + dateType: this.dateType === dateTypes.start ? 'start' : 'due', + }, + ), + }); + }, + }, + }, + computed: { + dateQueries() { + return this.dateType === dateTypes.start ? startDateQueries : dueDateQueries; + }, + dateLabel() { + return this.dateType === dateTypes.start + ? this.$options.i18n.startDate + : this.$options.i18n.dueDate; + }, + removeDateLabel() { + return this.dateType === dateTypes.start + ? this.$options.i18n.removeStartDate + : this.$options.i18n.removeDueDate; + }, + dateValue() { + return this.issuable?.[this.dateType] || null; + }, + isLoading() { + return this.$apollo.queries.issuable.loading || this.loading; + }, + hasDate() { + return this.dateValue !== null; + }, + parsedDate() { + if (!this.hasDate) { + return null; + } + + return parsePikadayDate(this.dateValue); + }, + formattedDate() { + if (!this.hasDate) { + return this.$options.i18n.noDate; + } + + return dateInWords(this.parsedDate, true); + }, + workspacePath() { + return this.issuableType === IssuableType.Issue + ? { + projectPath: this.fullPath, + } + : { + groupPath: this.fullPath, + }; + }, + dataTestId() { + return this.dateType === dateTypes.start ? 'start-date' : 'due-date'; + }, + }, + methods: { + closeForm() { + this.$refs.editable.collapse(); + this.$el.dispatchEvent(hideDropdownEvent); + this.$emit('closeForm'); + }, + openDatePicker() { + this.$refs.datePicker.calendar.show(); + }, + setFixedDate(isFixed) { + const date = this.issuable[dateFields[this.dateType].dateFixed]; + this.setDate(date, isFixed); + }, + setDate(date, isFixed = true) { + const formattedDate = date ? formatDate(date, 'yyyy-mm-dd') : null; + this.loading = true; + this.$refs.editable.collapse(); + this.$apollo + .mutate({ + mutation: this.dateQueries[this.issuableType].mutation, + variables: { + input: { + ...this.workspacePath, + iid: this.iid, + ...(this.canInherit + ? { + [dateFields[this.dateType].dateFixed]: isFixed ? formattedDate : undefined, + [dateFields[this.dateType].isDateFixed]: isFixed, + } + : { + [this.dateType]: formattedDate, + }), + }, + }, + }) + .then( + ({ + data: { + issuableSetDate: { errors }, + }, + }) => { + if (errors.length) { + createFlash({ + message: errors[0], + }); + } else { + this.$emit('closeForm'); + } + }, + ) + .catch(() => { + createFlash({ + message: sprintf( + __('Something went wrong while setting %{issuableType} %{dateType} date.'), + { + issuableType: this.issuableType, + dateType: this.dateType === dateTypes.start ? 'start' : 'due', + }, + ), + }); + }) + .finally(() => { + this.loading = false; + }); + }, + }, + i18n: { + dueDate: __('Due date'), + startDate: __('Start date'), + noDate: __('None'), + removeDueDate: __('remove due date'), + removeStartDate: __('remove start date'), + dateHelpValidMessage: __( + 'These dates affect how your epics appear in the roadmap. Set a fixed date or one inherited from the milestones assigned to issues in this epic.', + ), + help: __('Help'), + learnMore: __('Learn more'), + }, + dateHelpUrl: '/help/user/group/epics/index.md#start-date-and-due-date', +}; +</script> + +<template> + <sidebar-editable-item + ref="editable" + :title="dateLabel" + :tracking="tracking" + :loading="isLoading" + class="block" + :data-testid="dataTestId" + @open="openDatePicker" + > + <template #title-extra> + <gl-icon + ref="epicDatePopover" + name="question-o" + class="gl-ml-3 gl-cursor-pointer gl-text-blue-600" + tabindex="0" + :aria-label="$options.i18n.help" + /> + <gl-popover + :target="() => $refs.epicDatePopover.$el" + triggers="focus" + placement="left" + boundary="viewport" + > + <p>{{ $options.i18n.dateHelpValidMessage }}</p> + <gl-link :href="$options.dateHelpUrl" target="_blank">{{ + $options.i18n.learnMore + }}</gl-link> + </gl-popover> + </template> + <template #collapsed> + <div v-gl-tooltip :title="dateLabel" class="sidebar-collapsed-icon"> + <gl-icon :size="16" name="calendar" /> + <span class="collapse-truncated-title">{{ formattedDate }}</span> + </div> + <sidebar-inherit-date + v-if="canInherit" + :issuable="issuable" + :is-loading="isLoading" + :date-type="dateType" + @reset-date="setDate(null)" + @set-date="setFixedDate" + /> + <sidebar-formatted-date + v-else + :has-date="hasDate" + :formatted-date="formattedDate" + :reset-text="removeDateLabel" + :is-loading="isLoading" + @reset-date="setDate(null)" + /> + </template> + <template #default> + <gl-datepicker + ref="datePicker" + class="gl-relative" + :value="parsedDate" + show-clear-button + @input="setDate" + @clear="setDate(null)" + /> + </template> + </sidebar-editable-item> +</template> diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue b/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue new file mode 100644 index 00000000000..87cf1c29fb0 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue @@ -0,0 +1,56 @@ +<script> +import { GlButton } from '@gitlab/ui'; + +export default { + components: { + GlButton, + }, + inject: ['canUpdate'], + props: { + formattedDate: { + required: true, + type: String, + }, + hasDate: { + required: true, + type: Boolean, + }, + resetText: { + required: true, + type: String, + }, + isLoading: { + required: true, + type: Boolean, + }, + canDelete: { + required: false, + type: Boolean, + default: true, + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center hide-collapsed"> + <span + :class="hasDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'" + data-testid="sidebar-date-value" + > + {{ formattedDate }} + </span> + <div v-if="hasDate && canUpdate && canDelete" class="gl-display-flex"> + <span class="gl-px-2">-</span> + <gl-button + variant="link" + class="gl-text-gray-500!" + data-testid="reset-button" + :disabled="isLoading" + @click="$emit('reset-date', $event)" + > + {{ resetText }} + </gl-button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue b/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue new file mode 100644 index 00000000000..b6bfacb2e47 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue @@ -0,0 +1,110 @@ +<script> +import { GlFormRadio } from '@gitlab/ui'; +import { dateInWords, parsePikadayDate } from '~/lib/utils/datetime_utility'; +import { __ } from '~/locale'; +import { dateFields } from '../../constants'; +import SidebarFormattedDate from './sidebar_formatted_date.vue'; + +export default { + components: { + GlFormRadio, + SidebarFormattedDate, + }, + inject: ['canUpdate'], + props: { + issuable: { + required: true, + type: Object, + }, + isLoading: { + required: true, + type: Boolean, + }, + dateType: { + type: String, + required: true, + }, + }, + computed: { + dateIsFixed: { + get() { + return this.issuable?.[dateFields[this.dateType].isDateFixed] || false; + }, + set(fixed) { + this.$emit('set-date', fixed); + }, + }, + hasFixedDate() { + return this.issuable[dateFields[this.dateType].dateFixed] !== null; + }, + formattedFixedDate() { + const dateFixed = this.issuable[dateFields[this.dateType].dateFixed]; + if (!dateFixed) { + return this.$options.i18n.noDate; + } + + return dateInWords(parsePikadayDate(dateFixed), true); + }, + formattedInheritedDate() { + const dateFromMilestones = this.issuable[dateFields[this.dateType].dateFromMilestones]; + if (!dateFromMilestones) { + return this.$options.i18n.noDate; + } + + return dateInWords(parsePikadayDate(dateFromMilestones), true); + }, + }, + i18n: { + fixed: __('Fixed:'), + inherited: __('Inherited:'), + remove: __('remove'), + noDate: __('None'), + }, +}; +</script> + +<template> + <div class="hide-collapsed gl-mt-3"> + <div class="gl-display-flex gl-align-items-baseline" data-testid="sidebar-fixed-date"> + <gl-form-radio + v-model="dateIsFixed" + :value="true" + :disabled="!canUpdate || isLoading" + class="gl-pr-2" + > + <span :class="dateIsFixed ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"> + {{ $options.i18n.fixed }} + </span> + </gl-form-radio> + <sidebar-formatted-date + :has-date="dateIsFixed" + :formatted-date="formattedFixedDate" + :reset-text="$options.i18n.remove" + :is-loading="isLoading" + :can-delete="dateIsFixed && hasFixedDate" + class="gl-line-height-normal" + @reset-date="$emit('reset-date', $event)" + /> + </div> + <div class="gl-display-flex gl-align-items-baseline" data-testid="sidebar-inherited-date"> + <gl-form-radio + v-model="dateIsFixed" + :value="false" + :disabled="!canUpdate || isLoading" + class="gl-pr-2" + > + <span :class="!dateIsFixed ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"> + {{ $options.i18n.inherited }} + </span> + </gl-form-radio> + <sidebar-formatted-date + :has-date="!dateIsFixed" + :formatted-date="formattedInheritedDate" + :reset-text="$options.i18n.remove" + :is-loading="isLoading" + :can-delete="false" + class="gl-line-height-normal" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue b/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue deleted file mode 100644 index 141c2b3aae9..00000000000 --- a/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue +++ /dev/null @@ -1,203 +0,0 @@ -<script> -import { GlButton, GlIcon, GlDatepicker, GlTooltipDirective } from '@gitlab/ui'; -import createFlash from '~/flash'; -import { IssuableType } from '~/issue_show/constants'; -import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility'; -import { __, sprintf } from '~/locale'; -import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { dueDateQueries } from '~/sidebar/constants'; - -const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', { - bubbles: true, -}); - -export default { - tracking: { - event: 'click_edit_button', - label: 'right_sidebar', - property: 'dueDate', - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - components: { - GlButton, - GlIcon, - GlDatepicker, - SidebarEditableItem, - }, - inject: ['fullPath', 'iid', 'canUpdate'], - props: { - issuableType: { - required: true, - type: String, - }, - }, - data() { - return { - dueDate: null, - loading: false, - }; - }, - apollo: { - dueDate: { - query() { - return dueDateQueries[this.issuableType].query; - }, - variables() { - return { - fullPath: this.fullPath, - iid: String(this.iid), - }; - }, - update(data) { - return data.workspace?.issuable?.dueDate || null; - }, - result({ data }) { - this.$emit('dueDateUpdated', data.workspace?.issuable?.dueDate); - }, - error() { - createFlash({ - message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), { - issuableType: this.issuableType, - }), - }); - }, - }, - }, - computed: { - isLoading() { - return this.$apollo.queries.dueDate.loading || this.loading; - }, - hasDueDate() { - return this.dueDate !== null; - }, - parsedDueDate() { - if (!this.hasDueDate) { - return null; - } - - return parsePikadayDate(this.dueDate); - }, - formattedDueDate() { - if (!this.hasDueDate) { - return this.$options.i18n.noDueDate; - } - - return dateInWords(this.parsedDueDate, true); - }, - workspacePath() { - return this.issuableType === IssuableType.Issue - ? { - projectPath: this.fullPath, - } - : { - groupPath: this.fullPath, - }; - }, - }, - methods: { - closeForm() { - this.$refs.editable.collapse(); - this.$el.dispatchEvent(hideDropdownEvent); - this.$emit('closeForm'); - }, - openDatePicker() { - this.$refs.datePicker.calendar.show(); - }, - setDueDate(date) { - this.loading = true; - this.$refs.editable.collapse(); - this.$apollo - .mutate({ - mutation: dueDateQueries[this.issuableType].mutation, - variables: { - input: { - ...this.workspacePath, - iid: this.iid, - dueDate: date ? formatDate(date, 'yyyy-mm-dd') : null, - }, - }, - }) - .then( - ({ - data: { - issuableSetDueDate: { errors }, - }, - }) => { - if (errors.length) { - createFlash({ - message: errors[0], - }); - } else { - this.$emit('closeForm'); - } - }, - ) - .catch(() => { - createFlash({ - message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), { - issuableType: this.issuableType, - }), - }); - }) - .finally(() => { - this.loading = false; - }); - }, - }, - i18n: { - dueDate: __('Due date'), - noDueDate: __('None'), - removeDueDate: __('remove due date'), - }, -}; -</script> - -<template> - <sidebar-editable-item - ref="editable" - :title="$options.i18n.dueDate" - :tracking="$options.tracking" - :loading="isLoading" - class="block" - data-testid="due-date" - @open="openDatePicker" - > - <template #collapsed> - <div v-gl-tooltip :title="$options.i18n.dueDate" class="sidebar-collapsed-icon"> - <gl-icon :size="16" name="calendar" /> - <span class="collapse-truncated-title">{{ formattedDueDate }}</span> - </div> - <div class="gl-display-flex gl-align-items-center hide-collapsed"> - <span - :class="hasDueDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'" - data-testid="sidebar-duedate-value" - > - {{ formattedDueDate }} - </span> - <div v-if="hasDueDate && canUpdate" class="gl-display-flex"> - <span class="gl-px-2">-</span> - <gl-button - variant="link" - class="gl-text-gray-500!" - data-testid="reset-button" - :disabled="isLoading" - @click="setDueDate(null)" - > - {{ $options.i18n.removeDueDate }} - </gl-button> - </div> - </div> - </template> - <template #default> - <gl-datepicker - ref="datePicker" - :value="parsedDueDate" - show-clear-button - @input="setDueDate" - @clear="setDueDate(null)" - /> - </template> - </sidebar-editable-item> -</template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue index caf1c92c28a..809dd1b2dba 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -103,6 +103,7 @@ export default { <div> <div class="gl-display-flex gl-align-items-center" @click.self="collapse"> <span class="hide-collapsed" data-testid="title" @click="collapse">{{ title }}</span> + <slot name="title-extra"></slot> <gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" /> <gl-loading-icon v-if="loading && isClassicSidebar" diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 58e4b0348ca..82cad094d6b 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -1,11 +1,15 @@ import { IssuableType } from '~/issue_show/constants'; import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; +import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql'; +import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql'; import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql'; import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; -import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql'; +import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql'; +import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql'; +import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql'; import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql'; import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql'; import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; @@ -34,7 +38,7 @@ export const confidentialityQueries = { }, [IssuableType.Epic]: { query: epicConfidentialQuery, - mutation: updateEpicMutation, + mutation: updateEpicConfidentialMutation, }, }; @@ -47,9 +51,38 @@ export const referenceQueries = { }, }; +export const dateTypes = { + start: 'startDate', + due: 'dueDate', +}; + +export const dateFields = { + [dateTypes.start]: { + isDateFixed: 'startDateIsFixed', + dateFixed: 'startDateFixed', + dateFromMilestones: 'startDateFromMilestones', + }, + [dateTypes.due]: { + isDateFixed: 'dueDateIsFixed', + dateFixed: 'dueDateFixed', + dateFromMilestones: 'dueDateFromMilestones', + }, +}; + export const dueDateQueries = { [IssuableType.Issue]: { query: issueDueDateQuery, mutation: updateIssueDueDateMutation, }, + [IssuableType.Epic]: { + query: epicDueDateQuery, + mutation: updateEpicDueDateMutation, + }, +}; + +export const startDateQueries = { + [IssuableType.Epic]: { + query: epicStartDateQuery, + mutation: updateEpicStartDateMutation, + }, }; diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 52a1efa04e4..9115c3562d3 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -13,7 +13,7 @@ import { __ } from '~/locale'; import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; -import SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue'; +import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import { apolloProvider } from '~/sidebar/graphql'; import Translate from '../vue_shared/translate'; @@ -225,14 +225,14 @@ function mountDueDateComponent() { SidebarDueDateWidget, }, provide: { - iid: String(iid), - fullPath, canUpdate: editable, }, render: (createElement) => createElement('sidebar-due-date-widget', { props: { + iid: String(iid), + fullPath, issuableType: IssuableType.Issue, }, }), diff --git a/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql b/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql new file mode 100644 index 00000000000..f60f44abebd --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql @@ -0,0 +1,13 @@ +query epicDueDate($fullPath: ID!, $iid: ID) { + workspace: group(fullPath: $fullPath) { + __typename + issuable: epic(iid: $iid) { + __typename + id + dueDate + dueDateIsFixed + dueDateFixed + dueDateFromMilestones + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql b/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql new file mode 100644 index 00000000000..c6c24fd3d95 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql @@ -0,0 +1,13 @@ +query epicStartDate($fullPath: ID!, $iid: ID) { + workspace: group(fullPath: $fullPath) { + __typename + issuable: epic(iid: $iid) { + __typename + id + startDate + startDateIsFixed + startDateFixed + startDateFromMilestones + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/update_epic_due_date.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_due_date.mutation.graphql new file mode 100644 index 00000000000..9b0a8b4a8f7 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/update_epic_due_date.mutation.graphql @@ -0,0 +1,11 @@ +mutation updateEpicDueDate($input: UpdateEpicInput!) { + issuableSetDate: updateEpic(input: $input) { + issuable: epic { + id + dueDateIsFixed + dueDateFixed + dueDateFromMilestones + } + errors + } +} diff --git a/app/assets/javascripts/sidebar/queries/update_epic_start_date.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_start_date.mutation.graphql new file mode 100644 index 00000000000..9b4bb9159c3 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/update_epic_start_date.mutation.graphql @@ -0,0 +1,11 @@ +mutation updateEpicStartDate($input: UpdateEpicInput!) { + issuableSetDate: updateEpic(input: $input) { + issuable: epic { + id + startDateIsFixed + startDateFixed + startDateFromMilestones + } + errors + } +} diff --git a/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql index cf7eccd61c7..4765b0b08cc 100644 --- a/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql +++ b/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql @@ -1,5 +1,5 @@ mutation updateIssueDueDate($input: UpdateIssueInput!) { - issuableSetDueDate: updateIssue(input: $input) { + issuableSetDate: updateIssue(input: $input) { issuable: issue { id dueDate diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js index cdfecceb78a..d2e69bc06cf 100644 --- a/app/assets/javascripts/tracking.js +++ b/app/assets/javascripts/tracking.js @@ -153,6 +153,21 @@ export default class Tracking { return loadEvents; } + static enableFormTracking(config, contexts = []) { + if (!this.enabled()) return; + + if (!config?.forms?.whitelist?.length && !config?.fields?.whitelist?.length) { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('Unable to enable form event tracking without whitelist rules.'); + } + + contexts.unshift(STANDARD_CONTEXT); + const enabler = () => window.snowplow('enableFormTracking', config, contexts); + + if (document.readyState !== 'loading') enabler(); + else document.addEventListener('DOMContentLoaded', enabler); + } + static mixin(opts = {}) { return { computed: { diff --git a/app/graphql/types/release_assets_type.rb b/app/graphql/types/release_assets_type.rb index 79c132358e0..d847d9842d5 100644 --- a/app/graphql/types/release_assets_type.rb +++ b/app/graphql/types/release_assets_type.rb @@ -13,7 +13,7 @@ module Types field :count, GraphQL::INT_TYPE, null: true, method: :assets_count, description: 'Number of assets of the release.' - field :links, Types::ReleaseAssetLinkType.connection_type, null: true, + field :links, Types::ReleaseAssetLinkType.connection_type, null: true, method: :sorted_links, description: 'Asset links of the release.' field :sources, Types::ReleaseSourceType.connection_type, null: true, description: 'Sources of the release.' diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 6997c8cffda..2729951d685 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -162,7 +162,6 @@ module PageLayoutHelper default_properties = { current_emoji: '', current_message: '', - can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml), default_emoji: UserStatus::DEFAULT_EMOJI } diff --git a/app/models/project.rb b/app/models/project.rb index 5033eb43979..2cddcc43c4b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -129,6 +129,7 @@ class Project < ApplicationRecord after_create :check_repository_absence! acts_as_ordered_taggable + alias_method :topics, :tag_list attr_accessor :old_path_with_namespace attr_accessor :template_name diff --git a/app/models/release.rb b/app/models/release.rb index 5ca8f537baa..5037a1558ca 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -13,6 +13,7 @@ class Release < ApplicationRecord belongs_to :author, class_name: 'User' has_many :links, class_name: 'Releases::Link' + has_many :sorted_links, -> { sorted }, class_name: 'Releases::Link' has_many :milestone_releases has_many :milestones, through: :milestone_releases @@ -27,7 +28,10 @@ class Release < ApplicationRecord validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] } scope :sorted, -> { order(released_at: :desc) } - scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) } + scope :preloaded, -> { + includes(:author, :evidences, :milestones, :links, :sorted_links, + project: [:project_feature, :route, { namespace: :route }]) + } scope :with_project_and_namespace, -> { includes(project: :namespace) } scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) } scope :without_evidence, -> { left_joins(:evidences).where(::Releases::Evidence.arel_table[:id].eq(nil)) } @@ -58,8 +62,8 @@ class Release < ApplicationRecord end def assets_count(except: []) - links_count = links.count - sources_count = except.include?(:sources) ? 0 : sources.count + links_count = links.size + sources_count = except.include?(:sources) ? 0 : sources.size links_count + sources_count end diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index aad1c816cf1..681c5cfca74 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -390,16 +390,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def topics_to_show - project.tag_list.take(MAX_TOPICS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord + project.topics.take(MAX_TOPICS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord end def topics_not_shown - project.tag_list - topics_to_show + project.topics - topics_to_show end def count_of_extra_topics_not_shown - if project.tag_list.count > MAX_TOPICS_TO_SHOW - project.tag_list.count - MAX_TOPICS_TO_SHOW + if project.topics.count > MAX_TOPICS_TO_SHOW + project.topics.count - MAX_TOPICS_TO_SHOW else 0 end diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 15544fb9c45..c3ec2f7bab3 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -70,10 +70,9 @@ prepend: emoji_button, append: reset_message_button, placeholder: s_("Profiles|What's your status?") - - if Feature.enabled?(:set_user_availability_status, @user, default_enabled: :yaml) - .checkbox-icon-inline-wrapper - = status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"] - .gl-text-gray-600.gl-ml-5= s_('Profiles|"Busy" will be shown next to your name') + .checkbox-icon-inline-wrapper + = status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"] + .gl-text-gray-600.gl-ml-5= s_('Profiles|"Busy" will be shown next to your name') - if Feature.enabled?(:user_time_settings) .col-lg-12 %hr diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index b7c39e829fd..f69d07b1dd9 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1115,6 +1115,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: package_repositories:packages_debian_process_changes + :feature_category: :package_registry + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: package_repositories:packages_go_sync_packages :feature_category: :package_registry :has_external_dependencies: diff --git a/app/workers/packages/debian/process_changes_worker.rb b/app/workers/packages/debian/process_changes_worker.rb new file mode 100644 index 00000000000..c1731112099 --- /dev/null +++ b/app/workers/packages/debian/process_changes_worker.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Packages + module Debian + class ProcessChangesWorker + include ApplicationWorker + include Gitlab::Utils::StrongMemoize + + deduplicate :until_executed + idempotent! + + queue_namespace :package_repositories + feature_category :package_registry + + def perform(package_file_id, user_id) + @package_file_id = package_file_id + @user_id = user_id + + return unless package_file && user + + ::Packages::Debian::ProcessChangesService.new(package_file, user).execute + rescue ArgumentError, + Packages::Debian::ExtractChangesMetadataService::ExtractionError, + Packages::Debian::ExtractDebMetadataService::CommandFailedError, + Packages::Debian::ExtractMetadataService::ExtractionError, + Packages::Debian::ParseDebian822Service::InvalidDebian822Error, + ActiveRecord::RecordNotFound => e + Gitlab::ErrorTracking.log_exception(e, package_file_id: @package_file_id, user_id: @user_id) + package_file.destroy! + end + + private + + attr_reader :package_file_id, :user_id + + def package_file + strong_memoize(:package_file) do + ::Packages::PackageFile.find_by_id(package_file_id) + end + end + + def user + strong_memoize(:user) do + ::User.find_by_id(user_id) + end + end + end + end +end |