summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-12 15:13:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-12 15:13:54 +0000
commit98638cd5e43611aac2193a5c2f80f72374040430 (patch)
tree6605f0f284efed1d05708b3799f093eb5e305a8f /app/assets
parent43d816ebc20da6ff959176248c70d8c4c7c9345a (diff)
downloadgitlab-ce-98638cd5e43611aac2193a5c2f80f72374040430.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/code_review/signals.js51
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue130
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue3
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_toolbar.vue8
-rw-r--r--app/assets/javascripts/diffs/components/app.vue3
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/store/actions.js9
-rw-r--r--app/assets/javascripts/environments/components/deploy_freeze_alert.vue79
-rw-r--r--app/assets/javascripts/environments/components/environments_detail_header.vue159
-rw-r--r--app/assets/javascripts/environments/graphql/queries/deploy_freezes.query.graphql12
-rw-r--r--app/assets/javascripts/environments/mount_show.js13
-rw-r--r--app/assets/javascripts/graphql_shared/queries/merge_request.query.graphql9
-rw-r--r--app/assets/javascripts/graphql_shared/subscriptions/merge_request_prepared.subscription.graphql8
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js3
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_details_button.js2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_award_emoji.vue144
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue32
-rw-r--r--app/assets/javascripts/work_items/components/work_item_todos.vue116
-rw-r--r--app/assets/javascripts/work_items/constants.js18
-rw-r--r--app/assets/javascripts/work_items/graphql/award_emoji.fragment.graphql6
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql22
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql20
-rw-r--r--app/assets/javascripts/work_items/utils.js47
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss22
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/login.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss4
30 files changed, 705 insertions, 236 deletions
diff --git a/app/assets/javascripts/code_review/signals.js b/app/assets/javascripts/code_review/signals.js
new file mode 100644
index 00000000000..101b7996bb5
--- /dev/null
+++ b/app/assets/javascripts/code_review/signals.js
@@ -0,0 +1,51 @@
+import createApolloClient from '../lib/graphql';
+
+import { getDerivedMergeRequestInformation } from '../diffs/utils/merge_request';
+import { EVT_MR_PREPARED } from '../diffs/constants';
+
+import getMr from '../graphql_shared/queries/merge_request.query.graphql';
+import mrPreparation from '../graphql_shared/subscriptions/merge_request_prepared.subscription.graphql';
+
+function required(name) {
+ throw new Error(`${name} is a required argument`);
+}
+
+async function observeMergeRequestFinishingPreparation({ apollo, signaler }) {
+ const { namespace, project, id: iid } = getDerivedMergeRequestInformation({
+ endpoint: document.location.pathname,
+ });
+ const projectPath = `${namespace}/${project}`;
+
+ if (projectPath && iid) {
+ const currentStatus = await apollo.query({
+ query: getMr,
+ variables: { projectPath, iid },
+ });
+ const { id: gqlMrId, preparedAt } = currentStatus.data.project.mergeRequest;
+ let preparationObservable;
+ let preparationSubscriber;
+
+ if (!preparedAt) {
+ preparationObservable = apollo.subscribe({
+ query: mrPreparation,
+ variables: {
+ issuableId: gqlMrId,
+ },
+ });
+
+ preparationSubscriber = preparationObservable.subscribe((preparationUpdate) => {
+ if (preparationUpdate.data.mergeRequestMergeStatusUpdated?.preparedAt) {
+ signaler.$emit(EVT_MR_PREPARED);
+ preparationSubscriber.unsubscribe();
+ }
+ });
+ }
+ }
+}
+
+export async function start({
+ signalBus = required('signalBus'),
+ apolloClient = createApolloClient(),
+} = {}) {
+ await observeMergeRequestFinishingPreparation({ signaler: signalBus, apollo: apolloClient });
+}
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue
deleted file mode 100644
index cef446c4cf8..00000000000
--- a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue
+++ /dev/null
@@ -1,130 +0,0 @@
-<script>
-import { GlButtonGroup } from '@gitlab/ui';
-import { BUBBLE_MENU_TRACKING_ACTION } from '../../constants';
-import trackUIControl from '../../services/track_ui_control';
-import Paragraph from '../../extensions/paragraph';
-import Heading from '../../extensions/heading';
-import Audio from '../../extensions/audio';
-import Video from '../../extensions/video';
-import Image from '../../extensions/image';
-import DrawioDiagram from '../../extensions/drawio_diagram';
-import ToolbarButton from '../toolbar_button.vue';
-import BubbleMenu from './bubble_menu.vue';
-
-export default {
- components: {
- BubbleMenu,
- GlButtonGroup,
- ToolbarButton,
- },
- inject: ['tiptapEditor'],
- methods: {
- trackToolbarControlExecution({ contentType, value }) {
- trackUIControl({ action: BUBBLE_MENU_TRACKING_ACTION, property: contentType, value });
- },
-
- shouldShow: ({ editor, from, to }) => {
- if (from === to) return false;
-
- const includes = [Paragraph.name, Heading.name];
- const excludes = [Image.name, Audio.name, Video.name, DrawioDiagram.name];
-
- return (
- includes.some((type) => editor.isActive(type)) &&
- !excludes.some((type) => editor.isActive(type))
- );
- },
- },
-};
-</script>
-<template>
- <bubble-menu
- data-testid="formatting-bubble-menu"
- class="gl-shadow gl-rounded-base gl-bg-white"
- :should-show="shouldShow"
- :plugin-key="'formatting'"
- >
- <gl-button-group>
- <toolbar-button
- data-testid="bold"
- content-type="bold"
- icon-name="bold"
- editor-command="toggleBold"
- category="tertiary"
- size="medium"
- :label="__('Bold text')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="italic"
- content-type="italic"
- icon-name="italic"
- editor-command="toggleItalic"
- category="tertiary"
- size="medium"
- :label="__('Italic text')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="strike"
- content-type="strike"
- icon-name="strikethrough"
- editor-command="toggleStrike"
- category="tertiary"
- size="medium"
- :label="__('Strikethrough')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="code"
- content-type="code"
- icon-name="code"
- editor-command="toggleCode"
- category="tertiary"
- size="medium"
- :label="__('Code')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="superscript"
- content-type="superscript"
- icon-name="superscript"
- editor-command="toggleSuperscript"
- category="tertiary"
- size="medium"
- :label="__('Superscript')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="subscript"
- content-type="subscript"
- icon-name="subscript"
- editor-command="toggleSubscript"
- category="tertiary"
- size="medium"
- :label="__('Subscript')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="highlight"
- content-type="highlight"
- icon-name="highlight"
- editor-command="toggleHighlight"
- category="tertiary"
- size="medium"
- :label="__('Highlight')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="link"
- content-type="link"
- icon-name="link"
- editor-command="editLink"
- category="tertiary"
- size="medium"
- :label="__('Insert link')"
- @execute="trackToolbarControlExecution"
- />
- </gl-button-group>
- </bubble-menu>
-</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 7fee798c65a..4c5bbca4110 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -8,7 +8,6 @@ import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
-import FormattingBubbleMenu from './bubble_menus/formatting_bubble_menu.vue';
import CodeBlockBubbleMenu from './bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from './bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from './bubble_menus/media_bubble_menu.vue';
@@ -24,7 +23,6 @@ export default {
ContentEditorProvider,
TiptapEditorContent,
FormattingToolbar,
- FormattingBubbleMenu,
CodeBlockBubbleMenu,
LinkBubbleMenu,
MediaBubbleMenu,
@@ -225,7 +223,6 @@ export default {
>
<formatting-toolbar ref="toolbar" @enableMarkdownEditor="$emit('enableMarkdownEditor')" />
<div class="gl-relative">
- <formatting-bubble-menu />
<code-block-bubble-menu />
<link-bubble-menu />
<media-bubble-menu />
diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
index 1ef38df0f78..fac259cf6a1 100644
--- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
@@ -54,6 +54,14 @@ export default {
@execute="trackToolbarControlExecution"
/>
<toolbar-button
+ data-testid="strike"
+ content-type="strike"
+ icon-name="strikethrough"
+ editor-command="toggleStrike"
+ :label="__('Strikethrough')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
data-testid="blockquote"
content-type="blockquote"
icon-name="quote"
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index bfabac3123b..02307150e2f 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -41,6 +41,7 @@ import {
TRACKING_WHITESPACE_HIDE,
TRACKING_SINGLE_FILE_MODE,
TRACKING_MULTIPLE_FILES_MODE,
+ EVT_MR_PREPARED,
} from '../constants';
import diffsEventHub from '../event_hub';
@@ -470,8 +471,10 @@ export default {
diffsEventHub.$on('diffFilesModified', this.setDiscussions);
notesEventHub.$on('fetchedNotesData', this.rereadNoteHash);
}
+ diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData);
},
unsubscribeFromEvents() {
+ diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData);
if (this.glFeatures.singleFileFileByFile) {
notesEventHub.$off('fetchedNotesData', this.rereadNoteHash);
diffsEventHub.$off('diffFilesModified', this.setDiscussions);
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index a459def6b4b..063e36fa7fb 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -79,6 +79,7 @@ export const RENAMED_DIFF_TRANSITIONS = {
};
// MR Diffs known events
+export const EVT_MR_PREPARED = 'mr:asyncPreparationFinished';
export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles';
export const EVT_PERF_MARK_FILE_TREE_START = 'mr:diffs:perf:fileTreeStart';
export const EVT_PERF_MARK_FILE_TREE_END = 'mr:diffs:perf:fileTreeEnd';
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 2a7b4da684b..0668551902a 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -49,6 +49,7 @@ import {
TRACKING_CLICK_SINGLE_FILE_SETTING,
TRACKING_SINGLE_FILE_MODE,
TRACKING_MULTIPLE_FILES_MODE,
+ EVT_MR_PREPARED,
} from '../constants';
import { DISCUSSION_SINGLE_DIFF_FAILED, LOAD_SINGLE_DIFF_FAILED } from '../i18n';
import eventHub from '../event_hub';
@@ -287,10 +288,14 @@ export const fetchDiffFilesMeta = ({ commit, state }) => {
})
.catch((error) => {
if (error.response.status === HTTP_STATUS_NOT_FOUND) {
- createAlert({
- message: __('Building your merge request. Wait a few moments, then refresh this page.'),
+ const alert = createAlert({
+ message: __(
+ 'Building your merge request… This page will update when the build is complete.',
+ ),
variant: VARIANT_WARNING,
});
+
+ eventHub.$once(EVT_MR_PREPARED, () => alert.dismiss());
} else {
throw error;
}
diff --git a/app/assets/javascripts/environments/components/deploy_freeze_alert.vue b/app/assets/javascripts/environments/components/deploy_freeze_alert.vue
new file mode 100644
index 00000000000..aaa7e71758c
--- /dev/null
+++ b/app/assets/javascripts/environments/components/deploy_freeze_alert.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { sortBy } from 'lodash';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+import deployFreezesQuery from '../graphql/queries/deploy_freezes.query.graphql';
+
+export default {
+ components: {
+ GlAlert,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['projectFullPath'],
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return { deployFreezes: [] };
+ },
+
+ apollo: {
+ deployFreezes: {
+ query: deployFreezesQuery,
+ update(data) {
+ const freezes = data?.project?.environment?.deployFreezes;
+ return sortBy(freezes, [(freeze) => freeze.startTime]);
+ },
+ variables() {
+ return {
+ projectFullPath: this.projectFullPath,
+ environmentName: this.name,
+ };
+ },
+ },
+ },
+ computed: {
+ shouldShowDeployFreezeAlert() {
+ return this.deployFreezes.length > 0;
+ },
+ nextDeployFreeze() {
+ return this.deployFreezes[0];
+ },
+ deployFreezeStartTime() {
+ return formatDate(this.nextDeployFreeze.startTime);
+ },
+ deployFreezeEndTime() {
+ return formatDate(this.nextDeployFreeze.endTime);
+ },
+ },
+ i18n: {
+ deployFreezeAlert: s__(
+ 'Environments|A freeze period is in effect from %{startTime} to %{endTime}. Deployments might fail during this time. For more information, see the %{docsLinkStart}deploy freeze documentation%{docsLinkEnd}.',
+ ),
+ },
+ deployFreezeDocsPath: helpPagePath('user/project/releases/index', {
+ anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze',
+ }),
+};
+</script>
+<template>
+ <gl-alert v-if="shouldShowDeployFreezeAlert" :dismissible="false" class="gl-mt-4">
+ <gl-sprintf :message="$options.i18n.deployFreezeAlert">
+ <template #startTime
+ ><span class="gl-font-weight-bold">{{ deployFreezeStartTime }}</span></template
+ >
+ <template #endTime
+ ><span class="gl-font-weight-bold">{{ deployFreezeEndTime }}</span></template
+ >
+ <template #docsLink="{ content }"
+ ><gl-link :href="$options.deployFreezeDocsPath">{{ content }}</gl-link></template
+ >
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
index 1e555347011..0507abf3eaf 100644
--- a/app/assets/javascripts/environments/components/environments_detail_header.vue
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -7,6 +7,7 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
+import DeployFreezeAlert from './deploy_freeze_alert.vue';
export default {
name: 'EnvironmentsDetailHeader',
@@ -15,6 +16,7 @@ export default {
GlButton,
GlSprintf,
TimeAgo,
+ DeployFreezeAlert,
DeleteEnvironmentModal,
StopEnvironmentModal,
},
@@ -96,81 +98,88 @@ export default {
};
</script>
<template>
- <header class="top-area gl-justify-content-between">
- <div class="gl-display-flex gl-flex-grow-1 gl-align-items-center">
- <h1 class="page-title gl-font-size-h-display">
- {{ environment.name }}
- </h1>
- <p v-if="shouldShowCancelAutoStopButton" class="gl-mb-0 gl-ml-3" data-testid="auto-stops-at">
- <gl-sprintf :message="$options.i18n.autoStopAtText">
- <template #autoStopAt>
- <time-ago :time="environment.autoStopAt" />
- </template>
- </gl-sprintf>
- </p>
- </div>
- <div class="nav-controls gl-my-1">
- <form method="POST" :action="cancelAutoStopPath" data-testid="cancel-auto-stop-form">
- <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
- <gl-button
+ <div>
+ <deploy-freeze-alert :name="environment.name" />
+ <header class="top-area gl-justify-content-between">
+ <div class="gl-display-flex gl-flex-grow-1 gl-align-items-center">
+ <h1 class="page-title gl-font-size-h-display">
+ {{ environment.name }}
+ </h1>
+ <p
v-if="shouldShowCancelAutoStopButton"
- v-gl-tooltip.hover
- data-testid="cancel-auto-stop-button"
- :title="$options.i18n.cancelAutoStopButtonTitle"
- type="submit"
- icon="thumbtack"
+ class="gl-mb-0 gl-ml-3"
+ data-testid="auto-stops-at"
+ >
+ <gl-sprintf :message="$options.i18n.autoStopAtText">
+ <template #autoStopAt>
+ <time-ago :time="environment.autoStopAt" />
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ <div class="nav-controls gl-my-1">
+ <form method="POST" :action="cancelAutoStopPath" data-testid="cancel-auto-stop-form">
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <gl-button
+ v-if="shouldShowCancelAutoStopButton"
+ v-gl-tooltip.hover
+ data-testid="cancel-auto-stop-button"
+ :title="$options.i18n.cancelAutoStopButtonTitle"
+ type="submit"
+ icon="thumbtack"
+ />
+ </form>
+ <gl-button
+ v-if="shouldShowTerminalButton"
+ data-testid="terminal-button"
+ :href="terminalPath"
+ icon="terminal"
/>
- </form>
- <gl-button
- v-if="shouldShowTerminalButton"
- data-testid="terminal-button"
- :href="terminalPath"
- icon="terminal"
- />
- <gl-button
- v-if="shouldShowExternalUrlButton"
- v-gl-tooltip.hover
- data-testid="external-url-button"
- :title="$options.i18n.externalButtonTitle"
- :href="environment.externalUrl"
- is-unsafe-link
- icon="external-link"
- target="_blank"
- >{{ $options.i18n.externalButtonText }}</gl-button
- >
- <gl-button
- v-if="shouldShowMetricsButton"
- v-gl-tooltip.hover
- data-testid="metrics-button"
- :href="metricsPath"
- :title="$options.i18n.metricsButtonTitle"
- icon="chart"
- class="gl-mr-2"
- >
- {{ $options.i18n.metricsButtonText }}
- </gl-button>
- <gl-button v-if="canUpdateEnvironment" data-testid="edit-button" :href="updatePath">
- {{ $options.i18n.editButtonText }}
- </gl-button>
- <gl-button
- v-if="shouldShowStopButton"
- v-gl-modal-directive="'stop-environment-modal'"
- data-testid="stop-button"
- icon="stop"
- variant="danger"
- >
- {{ $options.i18n.stopButtonText }}
- </gl-button>
- <gl-button
- v-if="canDestroyEnvironment"
- v-gl-modal-directive="'delete-environment-modal'"
- data-testid="destroy-button"
- variant="danger"
- >
- {{ $options.i18n.deleteButtonText }}
- </gl-button>
- </div>
- <delete-environment-modal v-if="canDestroyEnvironment" :environment="environment" />
- <stop-environment-modal v-if="shouldShowStopButton" :environment="environment" />
- </header>
+ <gl-button
+ v-if="shouldShowExternalUrlButton"
+ v-gl-tooltip.hover
+ data-testid="external-url-button"
+ :title="$options.i18n.externalButtonTitle"
+ :href="environment.externalUrl"
+ is-unsafe-link
+ icon="external-link"
+ target="_blank"
+ >{{ $options.i18n.externalButtonText }}</gl-button
+ >
+ <gl-button
+ v-if="shouldShowMetricsButton"
+ v-gl-tooltip.hover
+ data-testid="metrics-button"
+ :href="metricsPath"
+ :title="$options.i18n.metricsButtonTitle"
+ icon="chart"
+ class="gl-mr-2"
+ >
+ {{ $options.i18n.metricsButtonText }}
+ </gl-button>
+ <gl-button v-if="canUpdateEnvironment" data-testid="edit-button" :href="updatePath">
+ {{ $options.i18n.editButtonText }}
+ </gl-button>
+ <gl-button
+ v-if="shouldShowStopButton"
+ v-gl-modal-directive="'stop-environment-modal'"
+ data-testid="stop-button"
+ icon="stop"
+ variant="danger"
+ >
+ {{ $options.i18n.stopButtonText }}
+ </gl-button>
+ <gl-button
+ v-if="canDestroyEnvironment"
+ v-gl-modal-directive="'delete-environment-modal'"
+ data-testid="destroy-button"
+ variant="danger"
+ >
+ {{ $options.i18n.deleteButtonText }}
+ </gl-button>
+ </div>
+ <delete-environment-modal v-if="canDestroyEnvironment" :environment="environment" />
+ <stop-environment-modal v-if="shouldShowStopButton" :environment="environment" />
+ </header>
+ </div>
</template>
diff --git a/app/assets/javascripts/environments/graphql/queries/deploy_freezes.query.graphql b/app/assets/javascripts/environments/graphql/queries/deploy_freezes.query.graphql
new file mode 100644
index 00000000000..7d701b95bbf
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/deploy_freezes.query.graphql
@@ -0,0 +1,12 @@
+query getEnvironmentFreezes($projectFullPath: ID!, $environmentName: String) {
+ project(fullPath: $projectFullPath) {
+ id
+ environment(name: $environmentName) {
+ id
+ deployFreezes {
+ startTime
+ endTime
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index cc13a237aca..f73cb7fe1bc 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -3,9 +3,13 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
-import { apolloProvider } from './graphql/client';
+import { apolloProvider as createApolloProvider } from './graphql/client';
import environmentsMixin from './mixins/environments_mixin';
+Vue.use(VueApollo);
+
+const apolloProvider = createApolloProvider();
+
export const initHeader = () => {
const el = document.getElementById('environments-detail-view-header');
const container = document.getElementById('environments-detail-view');
@@ -13,7 +17,11 @@ export const initHeader = () => {
return new Vue({
el,
+ apolloProvider,
mixins: [environmentsMixin],
+ provide: {
+ projectFullPath: dataset.projectFullPath,
+ },
data() {
const environment = {
name: dataset.name,
@@ -60,7 +68,6 @@ export const initPage = async () => {
const dataElement = document.getElementById('environments-detail-view');
const dataSet = convertObjectPropsToCamelCase(JSON.parse(dataElement.dataset.details));
- Vue.use(VueApollo);
Vue.use(VueRouter);
const el = document.getElementById('environment_details_page');
@@ -90,7 +97,7 @@ export const initPage = async () => {
return new Vue({
el,
- apolloProvider: apolloProvider(),
+ apolloProvider,
router,
provide: {
projectPath: dataSet.projectFullPath,
diff --git a/app/assets/javascripts/graphql_shared/queries/merge_request.query.graphql b/app/assets/javascripts/graphql_shared/queries/merge_request.query.graphql
new file mode 100644
index 00000000000..a6ef5935162
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/merge_request.query.graphql
@@ -0,0 +1,9 @@
+query mergeRequestId($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ preparedAt
+ }
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/subscriptions/merge_request_prepared.subscription.graphql b/app/assets/javascripts/graphql_shared/subscriptions/merge_request_prepared.subscription.graphql
new file mode 100644
index 00000000000..ba658f56ebd
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/subscriptions/merge_request_prepared.subscription.graphql
@@ -0,0 +1,8 @@
+subscription mergeRequestPrepared($issuableId: IssuableID!) {
+ mergeRequestMergeStatusUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ id
+ preparedAt
+ }
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index fbd45f4bd7d..552e75da9b8 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -3,6 +3,8 @@ import VueApollo from 'vue-apollo';
import initMrNotes from 'ee_else_ce/mr_notes';
import StickyHeader from '~/merge_requests/components/sticky_header.vue';
import { initIssuableHeaderWarnings } from '~/issuable';
+import { start as startCodeReviewMessaging } from '~/code_review/signals';
+import diffsEventHub from '~/diffs/event_hub';
import store from '~/mr_notes/stores';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import { apolloProvider } from '~/graphql_shared/issuable_client';
@@ -15,6 +17,7 @@ Vue.use(VueApollo);
export function initMrPage() {
initMrNotes();
initShow();
+ startCodeReviewMessaging({ signalBus: diffsEventHub });
}
requestIdleCallback(() => {
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
index 13c9f0ff8ee..5bdafa15f72 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
@@ -3,7 +3,7 @@ query getFailedJobs($fullPath: ID!, $pipelineIid: ID!) {
id
pipeline(iid: $pipelineIid) {
id
- jobs(statuses: FAILED) {
+ jobs(statuses: FAILED, retried: false) {
nodes {
status
detailedStatus {
diff --git a/app/assets/javascripts/projects/commit_box/info/init_details_button.js b/app/assets/javascripts/projects/commit_box/info/init_details_button.js
index 667d6bd0250..520b20fcb86 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_details_button.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_details_button.js
@@ -6,7 +6,7 @@ export const initDetailsButton = () => {
}
expandButton.addEventListener('click', (event) => {
- const btn = event.target;
+ const btn = event.currentTarget;
const contentEl = btn.parentElement.querySelector('.js-details-content');
if (contentEl) {
diff --git a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
new file mode 100644
index 00000000000..91f87be1233
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
@@ -0,0 +1,144 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import AwardsList from '~/vue_shared/components/awards_list.vue';
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import {
+ EMOJI_ACTION_REMOVE,
+ EMOJI_ACTION_ADD,
+ WIDGET_TYPE_AWARD_EMOJI,
+ EMOJI_THUMBSDOWN,
+ EMOJI_THUMBSUP,
+} from '../constants';
+
+export default {
+ defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN],
+ isLoggedIn: isLoggedIn(),
+ components: {
+ AwardsList,
+ },
+ props: {
+ workItem: {
+ type: Object,
+ required: true,
+ },
+ awardEmoji: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ currentUserId() {
+ return window.gon.current_user_id;
+ },
+ /**
+ * Parse and convert award emoji list to a format that AwardsList can understand
+ */
+ awards() {
+ return this.awardEmoji.nodes.map((emoji, index) => ({
+ id: index + 1,
+ name: emoji.name,
+ user: {
+ id: getIdFromGraphQLId(emoji.user.id),
+ },
+ }));
+ },
+ },
+ methods: {
+ handleAward(name) {
+ // Decide action based on emoji is already present
+ const action =
+ this.awards.findIndex((emoji) => emoji.name === name) > -1
+ ? EMOJI_ACTION_REMOVE
+ : EMOJI_ACTION_ADD;
+ const inputVariables = {
+ id: this.workItem.id,
+ awardEmojiWidget: {
+ action,
+ name,
+ },
+ };
+
+ this.$apollo
+ .mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: inputVariables,
+ },
+ optimisticResponse: this.getOptimisticResponse({ name, action }),
+ })
+ .then(
+ ({
+ data: {
+ workItemUpdate: { errors },
+ },
+ }) => {
+ if (errors?.length) {
+ throw new Error(errors[0]);
+ }
+ },
+ )
+ .catch((error) => {
+ this.$emit('error', error.message);
+ Sentry.captureException(error);
+ });
+ },
+ /**
+ * Prepare workItemUpdate for optimistic response
+ */
+ getOptimisticResponse({ name, action }) {
+ let awardEmojiNodes = [
+ ...this.awardEmoji.nodes,
+ {
+ name,
+ __typename: 'AwardEmoji',
+ user: {
+ id: convertToGraphQLId(TYPENAME_USER, this.currentUserId),
+ __typename: 'UserCore',
+ },
+ },
+ ];
+ // Exclude the award emoji node in case of remove action
+ if (action === EMOJI_ACTION_REMOVE) {
+ awardEmojiNodes = [...this.awardEmoji.nodes.filter((emoji) => emoji.name !== name)];
+ }
+ return {
+ workItemUpdate: {
+ errors: [],
+ workItem: {
+ ...this.workItem,
+ widgets: [
+ {
+ type: WIDGET_TYPE_AWARD_EMOJI,
+ awardEmoji: {
+ nodes: awardEmojiNodes,
+ __typename: 'AwardEmojiConnection',
+ },
+ __typename: 'WorkItemWidgetAwardEmoji',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ __typename: 'WorkItemUpdatePayload',
+ },
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mt-3">
+ <awards-list
+ data-testid="work-item-award-list"
+ :awards="awards"
+ :can-award-emoji="$options.isLoggedIn"
+ :current-user-id="currentUserId"
+ :default-awards="$options.defaultAwards"
+ selected-class="selected"
+ @award="handleAward"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index e3ac49b90d5..0f1af44e8a1 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -18,6 +18,7 @@ import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url
import { isPositiveInteger } from '~/lib/utils/number_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
@@ -27,7 +28,9 @@ import {
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_LABELS,
WIDGET_TYPE_NOTIFICATIONS,
+ WIDGET_TYPE_CURRENT_USER_TODOS,
WIDGET_TYPE_DESCRIPTION,
+ WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_PROGRESS,
@@ -51,10 +54,12 @@ import { findHierarchyWidgetChildren } from '../utils';
import WorkItemTree from './work_item_links/work_item_tree.vue';
import WorkItemActions from './work_item_actions.vue';
+import WorkItemTodos from './work_item_todos.vue';
import WorkItemState from './work_item_state.vue';
import WorkItemTitle from './work_item_title.vue';
import WorkItemCreatedUpdated from './work_item_created_updated.vue';
import WorkItemDescription from './work_item_description.vue';
+import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
@@ -67,6 +72,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ isLoggedIn: isLoggedIn(),
components: {
GlAlert,
GlBadge,
@@ -77,8 +83,10 @@ export default {
GlEmptyState,
WorkItemAssignees,
WorkItemActions,
+ WorkItemTodos,
WorkItemCreatedUpdated,
WorkItemDescription,
+ WorkItemAwardEmoji,
WorkItemDueDate,
WorkItemLabels,
WorkItemTitle,
@@ -287,6 +295,15 @@ export default {
workItemNotificationsSubscribed() {
return Boolean(this.isWidgetPresent(WIDGET_TYPE_NOTIFICATIONS)?.subscribed);
},
+ workItemCurrentUserTodos() {
+ return this.isWidgetPresent(WIDGET_TYPE_CURRENT_USER_TODOS);
+ },
+ showWorkItemCurrentUserTodos() {
+ return this.$options.isLoggedIn && this.workItemCurrentUserTodos;
+ },
+ currentUserTodos() {
+ return this.workItemCurrentUserTodos?.currentUserTodos?.edges;
+ },
workItemAssignees() {
return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
@@ -302,6 +319,9 @@ export default {
workItemProgress() {
return this.isWidgetPresent(WIDGET_TYPE_PROGRESS);
},
+ workItemAwardEmoji() {
+ return this.isWidgetPresent(WIDGET_TYPE_AWARD_EMOJI);
+ },
workItemHierarchy() {
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
},
@@ -566,6 +586,12 @@ export default {
class="gl-mr-3 gl-cursor-help"
>{{ __('Confidential') }}</gl-badge
>
+ <work-item-todos
+ v-if="showWorkItemCurrentUserTodos"
+ :work-item="workItem"
+ :current-user-todos="currentUserTodos"
+ @error="updateError = $event"
+ />
<work-item-actions
v-if="canUpdate || canDelete"
:work-item-id="workItem.id"
@@ -685,6 +711,12 @@ export default {
class="gl-pt-5"
@error="updateError = $event"
/>
+ <work-item-award-emoji
+ v-if="workItemAwardEmoji"
+ :work-item="workItem"
+ :award-emoji="workItemAwardEmoji.awardEmoji"
+ @error="updateError = $event"
+ />
<work-item-tree
v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_todos.vue b/app/assets/javascripts/work_items/components/work_item_todos.vue
new file mode 100644
index 00000000000..4e787720a42
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_todos.vue
@@ -0,0 +1,116 @@
+<script>
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { updateGlobalTodoCount } from '~/sidebar/utils';
+import { getWorkItemTodoOptimisticResponse } from '../utils';
+import { ADD, MARK_AS_DONE, TODO_ADD_ICON, TODO_DONE_ICON } from '../constants';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+
+export default {
+ i18n: {
+ addATodo: s__('WorkItem|Add a to do'),
+ markAsDone: s__('WorkItem|Mark as done'),
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlIcon,
+ GlButton,
+ },
+ props: {
+ workItem: {
+ type: Object,
+ required: true,
+ },
+ currentUserTodos: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ buttonLabel:
+ this.currentUserTodos.length > 0
+ ? this.$options.i18n.markAsDone
+ : this.$options.i18n.addATodo,
+ };
+ },
+ computed: {
+ pendingTodo() {
+ return this.currentUserTodos.length > 0;
+ },
+ buttonIcon() {
+ return this.pendingTodo ? TODO_DONE_ICON : TODO_ADD_ICON;
+ },
+ },
+ methods: {
+ onToggle() {
+ this.isLoading = true;
+ this.buttonLabel = '';
+ const action = this.pendingTodo ? MARK_AS_DONE : ADD;
+ const inputVariables = {
+ id: this.workItem.id,
+ currentUserTodosWidget: {
+ action,
+ },
+ };
+ this.$apollo
+ .mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: inputVariables,
+ },
+ optimisticResponse: getWorkItemTodoOptimisticResponse({
+ workItem: this.workItem,
+ pendingTodo: this.pendingTodo,
+ }),
+ })
+ .then(
+ ({
+ data: {
+ workItemUpdate: { errors },
+ },
+ }) => {
+ if (errors?.length) {
+ throw new Error(errors[0]);
+ }
+ if (this.pendingTodo) {
+ updateGlobalTodoCount(1);
+ this.buttonLabel = this.$options.i18n.markAsDone;
+ } else {
+ updateGlobalTodoCount(-1);
+ this.buttonLabel = this.$options.i18n.addATodo;
+ }
+ },
+ )
+ .catch((error) => {
+ this.$emit('error', error.message);
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button
+ v-gl-tooltip.hover
+ data-testid="work-item-todos-action"
+ :loading="isLoading"
+ :title="buttonLabel"
+ category="tertiary"
+ :aria-label="buttonLabel"
+ @click="onToggle"
+ >
+ <gl-icon
+ data-testid="work-item-todos-icon"
+ :class="{ 'gl-fill-blue-500': pendingTodo }"
+ :name="buttonIcon"
+ />
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 6523734a382..6710c762c2e 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -14,7 +14,9 @@ export const TASK_TYPE_NAME = 'Task';
export const WIDGET_TYPE_ASSIGNEES = 'ASSIGNEES';
export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
+export const WIDGET_TYPE_AWARD_EMOJI = 'AWARD_EMOJI';
export const WIDGET_TYPE_NOTIFICATIONS = 'NOTIFICATIONS';
+export const WIDGET_TYPE_CURRENT_USER_TODOS = 'CURRENT_USER_TODOS';
export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
@@ -215,3 +217,19 @@ export const TEST_ID_NOTIFICATIONS_TOGGLE_ACTION = 'notifications-toggle-action'
export const TEST_ID_NOTIFICATIONS_TOGGLE_FORM = 'notifications-toggle-form';
export const TEST_ID_DELETE_ACTION = 'delete-action';
export const TEST_ID_PROMOTE_ACTION = 'promote-action';
+
+export const ADD = 'ADD';
+export const MARK_AS_DONE = 'MARK_AS_DONE';
+export const TODO_ADD_ICON = 'todo-add';
+export const TODO_DONE_ICON = 'todo-done';
+export const TODO_TYPENAME = 'Todo';
+export const TODO_EDGE_TYPENAME = 'TodoEdge';
+export const TODO_CONNECTION_TYPENAME = 'TodoConnection';
+export const CURRENT_USER_TODOS_TYPENAME = 'WorkItemWidgetCurrentUserTodos';
+export const WORK_ITEM_TYPENAME = 'WorkItem';
+export const WORK_ITEM_UPDATE_PAYLOAD_TYPENAME = 'WorkItemUpdatePayload';
+
+export const EMOJI_ACTION_ADD = 'ADD';
+export const EMOJI_ACTION_REMOVE = 'REMOVE';
+export const EMOJI_THUMBSUP = 'thumbsup';
+export const EMOJI_THUMBSDOWN = 'thumbsdown';
diff --git a/app/assets/javascripts/work_items/graphql/award_emoji.fragment.graphql b/app/assets/javascripts/work_items/graphql/award_emoji.fragment.graphql
new file mode 100644
index 00000000000..85b88990cd6
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/award_emoji.fragment.graphql
@@ -0,0 +1,6 @@
+fragment AwardEmojiFragment on AwardEmoji {
+ name
+ user {
+ id
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
index 44fda3ee894..42c057fb8fe 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
@@ -1,6 +1,7 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/work_items/graphql/milestone.fragment.graphql"
+#import "~/work_items/graphql/award_emoji.fragment.graphql"
fragment WorkItemMetadataWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
@@ -36,9 +37,28 @@ fragment WorkItemMetadataWidgets on WorkItemWidget {
}
}
}
-
... on WorkItemWidgetNotifications {
type
subscribed
}
+
+ ... on WorkItemWidgetCurrentUserTodos {
+ type
+ currentUserTodos(state: pending) {
+ edges {
+ node {
+ id
+ state
+ }
+ }
+ }
+ }
+ ... on WorkItemWidgetAwardEmoji {
+ type
+ awardEmoji {
+ nodes {
+ ...AwardEmojiFragment
+ }
+ }
+ }
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index 8039ef53f98..bf8dc9ce9b0 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -1,6 +1,7 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/work_items/graphql/milestone.fragment.graphql"
+#import "~/work_items/graphql/award_emoji.fragment.graphql"
#import "ee_else_ce/work_items/graphql/work_item_metadata_widgets.fragment.graphql"
fragment WorkItemWidgets on WorkItemWidget {
@@ -89,4 +90,23 @@ fragment WorkItemWidgets on WorkItemWidget {
type
subscribed
}
+ ... on WorkItemWidgetCurrentUserTodos {
+ type
+ currentUserTodos(state: pending) {
+ edges {
+ node {
+ id
+ state
+ }
+ }
+ }
+ }
+ ... on WorkItemWidgetAwardEmoji {
+ type
+ awardEmoji {
+ nodes {
+ ...AwardEmojiFragment
+ }
+ }
+ }
}
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index c5b937ef132..653819904af 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,4 +1,14 @@
-import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants';
+import { uniqueId } from 'lodash';
+import {
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_CURRENT_USER_TODOS,
+ CURRENT_USER_TODOS_TYPENAME,
+ TODO_CONNECTION_TYPENAME,
+ TODO_EDGE_TYPENAME,
+ TODO_TYPENAME,
+ WORK_ITEM_TYPENAME,
+ WORK_ITEM_UPDATE_PAYLOAD_TYPENAME,
+} from '~/work_items/constants';
import workItemQuery from './graphql/work_item.query.graphql';
import workItemByIidQuery from './graphql/work_item_by_iid.query.graphql';
@@ -28,3 +38,38 @@ export const markdownPreviewPath = (fullPath, iid) =>
`${
gon.relative_url_root || ''
}/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`;
+
+export const getWorkItemTodoOptimisticResponse = ({ workItem, pendingTodo }) => {
+ const todo = pendingTodo
+ ? [
+ {
+ node: {
+ id: -uniqueId(),
+ state: 'pending',
+ __typename: TODO_TYPENAME,
+ },
+ __typename: TODO_EDGE_TYPENAME,
+ },
+ ]
+ : [];
+ return {
+ workItemUpdate: {
+ errors: [],
+ workItem: {
+ ...workItem,
+ widgets: [
+ {
+ type: WIDGET_TYPE_CURRENT_USER_TODOS,
+ currentUserTodos: {
+ edges: todo,
+ __typename: TODO_CONNECTION_TYPENAME,
+ },
+ __typename: CURRENT_USER_TODOS_TYPENAME,
+ },
+ ],
+ __typename: WORK_ITEM_TYPENAME,
+ },
+ __typename: WORK_ITEM_UPDATE_PAYLOAD_TYPENAME,
+ },
+ };
+};
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 56ef6d13252..f828129cdf1 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -36,7 +36,7 @@
}
.right-sidebar-collapsed {
- --application-bar-right: #{$gutter-collapsed-width};
+ --application-bar-right: #{$right-sidebar-collapsed-width};
&.is-merge-request {
--application-bar-right: 0px;
@@ -44,7 +44,7 @@
}
.right-sidebar-expanded {
- --application-bar-right: #{$gutter-width};
+ --application-bar-right: #{$right-sidebar-width};
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index f2486640650..9fdf889f4e9 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -22,7 +22,7 @@
&:not(.is-merge-request) {
@include media-breakpoint-up(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
- padding-right: $gutter-collapsed-width;
+ padding-right: $right-sidebar-collapsed-width;
}
}
}
@@ -30,7 +30,7 @@
&.is-merge-request {
@include media-breakpoint-up(lg) {
.content-wrapper {
- padding-right: $gutter-collapsed-width;
+ padding-right: $right-sidebar-collapsed-width;
}
}
}
@@ -56,7 +56,7 @@
z-index: $zindex-dropdown-menu;
&.right-sidebar-merge-requests {
- width: $gutter-width;
+ width: $right-sidebar-width;
@include media-breakpoint-up(md) {
z-index: auto;
@@ -69,14 +69,14 @@
@include media-breakpoint-only(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
- padding-right: $gutter-collapsed-width;
+ padding-right: $right-sidebar-collapsed-width;
}
}
&:not(.is-merge-request) {
@include media-breakpoint-up(md) {
.content-wrapper {
- padding-right: $gutter-width;
+ padding-right: $right-sidebar-width;
}
}
}
@@ -102,7 +102,7 @@
@mixin maintain-sidebar-dimensions {
display: block;
- width: $gutter-width;
+ width: $right-sidebar-width;
}
.issues-bulk-update.right-sidebar {
@@ -113,7 +113,7 @@
&.right-sidebar-expanded {
@include maintain-sidebar-dimensions;
- width: $gutter-width;
+ width: $right-sidebar-width;
.issuable-sidebar-header {
// matches `.top-area .nav-controls` for issuable index pages
@@ -401,7 +401,7 @@
border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
- width: $gutter-inner-width;
+ width: $right-sidebar-inner-width;
// --
&:last-child {
@@ -474,7 +474,7 @@
&.right-sidebar-expanded {
&:not(.right-sidebar-merge-requests) {
- width: $gutter-width;
+ width: $right-sidebar-width;
}
.value {
@@ -554,13 +554,13 @@
}
}
- width: $gutter-collapsed-width;
+ width: $right-sidebar-collapsed-width;
padding: 0;
.block,
.sidebar-contained-width,
.issuable-sidebar-header {
- width: $gutter-collapsed-width - 2px;
+ width: $right-sidebar-collapsed-width - 2px;
padding: 0;
border-bottom: 0;
overflow: hidden;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 6cf231fbcef..dc6a5c5479c 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -2,9 +2,9 @@
* Layout
*/
$grid-size: 8px;
-$gutter-collapsed-width: 62px;
-$gutter-width: 290px;
-$gutter-inner-width: 250px;
+$right-sidebar-collapsed-width: 62px;
+$right-sidebar-width: 290px;
+$right-sidebar-inner-width: 250px;
$sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
$contextual-sidebar-width: 256px;
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 32a6af0dc36..5aca697ae26 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -15,7 +15,7 @@
width: 100%;
&.is-compact {
- width: calc(100% - #{$gutter-width});
+ width: calc(100% - #{$right-sidebar-width});
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/login.scss b/app/assets/stylesheets/page_bundles/login.scss
index 1ae7230772d..6175bba6ba7 100644
--- a/app/assets/stylesheets/page_bundles/login.scss
+++ b/app/assets/stylesheets/page_bundles/login.scss
@@ -292,3 +292,9 @@
}
}
}
+
+@include media-breakpoint-down(sm) {
+ .sm-bg-gray-10 {
+ @include gl-bg-gray-10;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 17ca7828a20..b282d83b74b 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -964,7 +964,7 @@ $tabs-holder-z-index: 250;
.merge-request-overview {
@include media-breakpoint-up(lg) {
display: grid;
- grid-template-columns: calc(97% - #{$gutter-width}) auto;
+ grid-template-columns: calc(97% - #{$right-sidebar-width}) auto;
grid-gap: 3%;
}
}
@@ -1131,7 +1131,7 @@ $tabs-holder-z-index: 250;
width: 100%;
height: $toggle-sidebar-height;
padding-left: $contextual-sidebar-width;
- padding-right: $gutter_collapsed_width;
+ padding-right: $right-sidebar-collapsed-width;
background: var(--white, $white);
border-top: 1px solid var(--border-color, $border-color);
transition: padding $gl-transition-duration-medium;