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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-11 15:09:05 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-11 15:09:05 +0300
commit28e90894e1e6f17320f5b1d2fff6fe736bf65dff (patch)
tree21d63bf124b6064eb1650acc3e2aabe6dbc99f58 /app
parenta48957b317edf23b1bcfc6df0c098a824eae86f4 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue28
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js8
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue68
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue108
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue1
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue3
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue39
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql4
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql2
-rw-r--r--app/assets/stylesheets/page_bundles/issues_list.scss5
-rw-r--r--app/channels/awareness_channel.rb85
-rw-r--r--app/graphql/types/permission_types/work_item.rb2
-rw-r--r--app/helpers/auth_helper.rb1
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/models/awareness_session.rb245
-rw-r--r--app/models/concerns/awareness.rb41
-rw-r--r--app/models/user.rb1
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml2
23 files changed, 222 insertions, 433 deletions
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
index 6e7c87b8515..69021dde0e9 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
@@ -70,7 +70,7 @@ export default {
captureException({ error, component: this.$options.name });
},
pollInterval() {
- if (this.runner?.status === STATUS_ONLINE) {
+ if (this.isRunnerOnline) {
// stop polling
return 0;
}
@@ -97,9 +97,6 @@ export default {
}
return s__('Runners|Register runner');
},
- status() {
- return this.runner?.status;
- },
tokenMessage() {
if (this.token) {
return s__(
@@ -122,15 +119,34 @@ export default {
runCommand() {
return runCommand({ platform: this.platform });
},
+ isRunnerOnline() {
+ return this.runner?.status === STATUS_ONLINE;
+ },
+ },
+ created() {
+ window.addEventListener('beforeunload', this.onBeforeunload);
+ },
+ destroyed() {
+ window.removeEventListener('beforeunload', this.onBeforeunload);
},
methods: {
toggleDrawer() {
this.$emit('toggleDrawer');
},
+ onBeforeunload(event) {
+ if (this.isRunnerOnline) {
+ return undefined;
+ }
+
+ const str = s__('Runners|You may lose access to the runner token if you leave this page.');
+ event.preventDefault();
+ // eslint-disable-next-line no-param-reassign
+ event.returnValue = str; // Chrome requires returnValue to be set
+ return str;
+ },
},
EXECUTORS_HELP_URL,
SERVICE_COMMANDS_HELP_URL,
- STATUS_ONLINE,
I18N_REGISTRATION_SUCCESS,
};
</script>
@@ -225,7 +241,7 @@ export default {
</gl-sprintf>
</p>
</section>
- <section v-if="status == $options.STATUS_ONLINE">
+ <section v-if="isRunnerOnline">
<h2 class="gl-font-size-h2">🎉 {{ $options.I18N_REGISTRATION_SUCCESS }}</h2>
<p class="gl-pl-6">
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index f482fabc5f6..3e310f941ec 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -81,6 +81,14 @@ export const config = {
});
},
},
+ userPermissions: {
+ read(permission = {}) {
+ return {
+ ...permission,
+ setWorkItemMetadata: false,
+ };
+ },
+ },
},
},
MemberInterfaceConnection: {
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index 34b7d95322e..d1c0e757a91 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -92,6 +92,6 @@ export default {
{{ $options.i18n.emptyHint }}
</div>
</gl-collapse>
- <hr class="gl-my-2" />
+ <hr class="gl-my-2 gl-mx-4" />
</section>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 2bb355736fd..3fdc5124111 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -134,7 +134,7 @@ export default {
<ul class="gl-p-0 gl-m-0">
<nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static />
</ul>
- <hr class="gl-my-2" />
+ <hr class="gl-my-2 gl-mx-4" />
</section>
<pinned-section
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index 082c261977b..650fa798db6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -130,7 +130,7 @@ export default {
<span v-if="approvalLeftMessage">{{ message }}</span>
<span v-else class="gl-font-weight-bold">{{ message }}</span>
<user-avatar-list
- class="gl-display-inline-block gl-vertical-align-middle gl-pt-1"
+ class="gl-display-inline-flex gl-vertical-align-middle"
:img-size="24"
:items="approvers"
/>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 71cf85c75a7..6552a874c3a 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -90,7 +90,7 @@ export default {
</script>
<template>
- <span ref="userAvatar" class="gl-display-inline-flex">
+ <span ref="userAvatar">
<gl-avatar
:class="{
lazy: lazy,
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index 8a05960869c..713c08e20e9 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -1,8 +1,8 @@
<script>
-import { GlAvatar, GlButton } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
import { ASC } from '~/notes/constants';
+import { __ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getWorkItemQuery } from '../../utils';
@@ -17,8 +17,6 @@ export default {
avatarUrl: window.gon.current_user_avatar_url,
},
components: {
- GlAvatar,
- GlButton,
WorkItemNoteSignedOut,
WorkItemCommentLocked,
WorkItemCommentForm,
@@ -75,11 +73,16 @@ export default {
required: false,
default: () => ({}),
},
+ isNewDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
workItem: {},
- isEditing: false,
+ isEditing: this.isNewDiscussion,
isSubmitting: false,
isSubmittingWithKeydown: false,
};
@@ -118,23 +121,9 @@ export default {
property: `type_${this.workItemType}`,
};
},
- isLockedOutOrSignedOut() {
- return !this.signedIn || !this.canUpdate;
- },
- lockedOutUserWarningInReplies() {
- return this.addPadding && this.isLockedOutOrSignedOut;
- },
- timelineEntryClass() {
- return {
- 'timeline-entry gl-mb-3 note note-wrapper note-comment': true,
- 'gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-p-5! gl-mx-n3 gl-mb-n2!': this
- .lockedOutUserWarningInReplies,
- };
- },
timelineEntryInnerClass() {
return {
- 'timeline-entry-inner': true,
- 'gl-pb-3': this.addPadding,
+ 'timeline-entry-inner': this.isNewDiscussion,
};
},
timelineContentClass() {
@@ -155,6 +144,18 @@ export default {
canUpdate() {
return this.workItem?.userPermissions?.updateWorkItem;
},
+ workItemState() {
+ return this.workItem?.state;
+ },
+ commentButtonText() {
+ return this.isNewDiscussion ? __('Comment') : __('Reply');
+ },
+ timelineEntryClass() {
+ return this.isNewDiscussion
+ ? 'timeline-entry note-form'
+ : // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'note note-wrapper note-comment discussion-reply-holder gl-border-t-0! clearfix';
+ },
},
watch: {
autofocus: {
@@ -226,9 +227,13 @@ export default {
}
},
cancelEditing() {
- this.isEditing = false;
+ this.isEditing = this.isNewDiscussion;
this.$emit('cancelEditing');
},
+ showReplyForm() {
+ this.isEditing = true;
+ this.$emit('startReplying');
+ },
},
};
</script>
@@ -242,9 +247,6 @@ export default {
:is-project-archived="isProjectArchived"
/>
<div v-else :class="timelineEntryInnerClass">
- <div class="timeline-avatar gl-float-left">
- <gl-avatar :src="$options.constantOptions.avatarUrl" :size="32" class="gl-mr-3" />
- </div>
<div :class="timelineContentClass">
<div :class="parentClass">
<work-item-comment-form
@@ -253,17 +255,27 @@ export default {
:aria-label="__('Add a reply')"
:is-submitting="isSubmitting"
:autosave-key="autosaveKey"
+ :is-new-discussion="isNewDiscussion"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ :work-item-state="workItemState"
+ :work-item-id="workItemId"
+ :autofocus="autofocus"
+ :comment-button-text="commentButtonText"
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
/>
- <gl-button
+ <textarea
v-else
- class="gl-flex-grow-1 gl-justify-content-start! gl-text-secondary!"
- @click="isEditing = true"
- >{{ __('Add a reply') }}</gl-button
- >
+ ref="textarea"
+ rows="1"
+ class="reply-placeholder-text-field gl-font-regular!"
+ data-testid="note-reply-textarea"
+ :placeholder="__('Reply')"
+ :aria-label="__('Reply to comment')"
+ @focus="showReplyForm"
+ @click="showReplyForm"
+ ></textarea>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index 8390ae5b2e1..f9f24366725 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -1,10 +1,22 @@
<script>
import { GlButton } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__, __ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
+import Tracking from '~/tracking';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ STATE_OPEN,
+ STATE_EVENT_REOPEN,
+ STATE_EVENT_CLOSE,
+ TRACKING_CATEGORY_SHOW,
+ i18n,
+} from '~/work_items/constants';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
export default {
constantOptions: {
@@ -14,8 +26,13 @@ export default {
GlButton,
MarkdownEditor,
},
+ mixins: [Tracking.mixin()],
inject: ['fullPath'],
props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
workItemType: {
type: String,
required: true,
@@ -52,13 +69,36 @@ export default {
required: false,
default: () => ({}),
},
+ isNewDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ workItemState: {
+ type: String,
+ required: false,
+ default: STATE_OPEN,
+ },
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
commentText: getDraft(this.autosaveKey) || this.initialValue || '',
+ updateInProgress: false,
};
},
computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'work_item_task_status',
+ property: `type_${this.workItemType}`,
+ };
+ },
formFieldProps() {
return {
'aria-label': this.ariaLabel,
@@ -67,6 +107,17 @@ export default {
name: 'work-item-add-or-edit-comment',
};
},
+ isWorkItemOpen() {
+ return this.workItemState === STATE_OPEN;
+ },
+ toggleWorkItemStateText() {
+ return this.isWorkItemOpen
+ ? sprintf(__('Close %{workItemType}'), { workItemType: this.workItemType.toLowerCase() })
+ : sprintf(__('Reopen %{workItemType}'), { workItemType: this.workItemType.toLowerCase() });
+ },
+ cancelButtonText() {
+ return this.isNewDiscussion ? this.toggleWorkItemStateText : __('Cancel');
+ },
},
methods: {
setCommentText(newText) {
@@ -99,13 +150,55 @@ export default {
this.$emit('cancelEditing');
clearDraft(this.autosaveKey);
},
+ async toggleWorkItemState() {
+ const input = {
+ id: this.workItemId,
+ stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
+ };
+
+ this.updateInProgress = true;
+
+ try {
+ this.track('updated_state');
+
+ const { mutation, variables } = getUpdateWorkItemMutation({
+ workItemParentId: this.workItemParentId,
+ input,
+ });
+
+ const { data } = await this.$apollo.mutate({
+ mutation,
+ variables,
+ });
+
+ const errors = data.workItemUpdate?.errors;
+
+ if (errors?.length) {
+ this.$emit('error', i18n.updateError);
+ }
+ } catch (error) {
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ }
+
+ this.updateInProgress = false;
+ },
+ cancelButtonAction() {
+ if (this.isNewDiscussion) {
+ this.toggleWorkItemState();
+ } else {
+ this.cancelEditing();
+ }
+ },
},
};
</script>
<template>
- <div class="timeline-discussion-body">
- <div class="note-body">
+ <div class="timeline-discussion-body gl-overflow-visible!">
+ <div class="note-body gl-p-0! gl-overflow-visible!">
<form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
<markdown-editor
:value="commentText"
@@ -113,11 +206,12 @@ export default {
:markdown-docs-path="$options.constantOptions.markdownDocsPath"
:autocomplete-data-sources="autocompleteDataSources"
:form-field-props="formFieldProps"
+ :add-spacing-classes="false"
data-testid="work-item-add-comment"
class="gl-mb-3"
- autofocus
use-bottom-toolbar
supports-quick-actions
+ :autofocus="autofocus"
@input="setCommentText"
@keydown.meta.enter="$emit('submitForm', commentText)"
@keydown.ctrl.enter="$emit('submitForm', commentText)"
@@ -127,6 +221,7 @@ export default {
category="primary"
variant="confirm"
data-testid="confirm-button"
+ :disabled="!commentText.length"
:loading="isSubmitting"
@click="$emit('submitForm', commentText)"
>{{ commentButtonText }}
@@ -135,8 +230,9 @@ export default {
data-testid="cancel-button"
category="primary"
class="gl-ml-3"
- @click="cancelEditing"
- >{{ __('Cancel') }}
+ :loading="updateInProgress"
+ @click="cancelButtonAction"
+ >{{ cancelButtonText }}
</gl-button>
</form>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index 6cf15ba50ec..21fc8f99366 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -248,6 +248,7 @@ export default {
:add-padding="true"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ @startReplying="showReplyForm"
@cancelEditing="hideReplyForm"
@replied="onReplied"
@replying="onReplying"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index 8b25d305398..5ccc5526ce8 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -310,6 +310,9 @@ export default {
:comment-button-text="__('Save comment')"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ :work-item-id="workItemId"
+ :autofocus="isEditing"
+ class="gl-pl-3 gl-mt-3"
@cancelEditing="isEditing = false"
@submitForm="updateNote"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index a1f1eda8bc5..00cdc224320 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -1,6 +1,7 @@
<script>
import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import { uniqueId } from 'lodash';
import { __ } from '~/locale';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants';
@@ -96,6 +97,7 @@ export default {
sortOrder: ASC,
noteToDelete: null,
discussionFilter: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ addNoteKey: uniqueId(`work-item-add-note-${this.workItemId}`),
};
},
computed: {
@@ -134,6 +136,7 @@ export default {
fetchByIid: this.fetchByIid,
workItemType: this.workItemType,
sortOrder: this.sortOrder,
+ isNewDiscussion: true,
markdownPreviewPath: this.markdownPreviewPath,
autocompleteDataSources: this.autocompleteDataSources,
};
@@ -278,6 +281,9 @@ export default {
filterDiscussions(filterValue) {
this.discussionFilter = filterValue;
},
+ updateKey() {
+ this.addNoteKey = uniqueId(`work-item-add-note-${this.workItemId}`);
+ },
async fetchMoreNotes() {
this.isLoadingMore = true;
// copied from discussions batch logic - every fetchMore call has a higher
@@ -361,12 +367,17 @@ export default {
</div>
<div v-else class="issuable-discussion gl-mb-5 gl-clearfix!">
<template v-if="!initialLoading">
- <ul class="notes main-notes-list timeline gl-clearfix!">
- <work-item-add-note
- v-if="formAtTop && !commentsDisabled"
- v-bind="workItemCommentFormProps"
- @error="$emit('error', $event)"
- />
+ <div v-if="formAtTop && !commentsDisabled" class="js-comment-form">
+ <ul class="notes notes-form timeline">
+ <work-item-add-note
+ v-bind="workItemCommentFormProps"
+ :key="addNoteKey"
+ @cancelEditing="updateKey"
+ @error="$emit('error', $event)"
+ />
+ </ul>
+ </div>
+ <ul class="notes main-notes-list timeline">
<template v-for="discussion in notesArray">
<system-note
v-if="isSystemNote(discussion)"
@@ -393,17 +404,21 @@ export default {
</template>
</template>
- <work-item-add-note
- v-if="!formAtTop && !commentsDisabled"
- v-bind="workItemCommentFormProps"
- @error="$emit('error', $event)"
- />
-
<work-item-history-only-filter-note
v-if="commentsDisabled"
@changeFilter="filterDiscussions"
/>
</ul>
+ <div v-if="!formAtTop && !commentsDisabled" class="js-comment-form">
+ <ul class="notes notes-form timeline">
+ <work-item-add-note
+ v-bind="workItemCommentFormProps"
+ :key="addNoteKey"
+ @cancelEditing="updateKey"
+ @error="$emit('error', $event)"
+ />
+ </ul>
+ </div>
</template>
<template v-if="showLoadingMoreSkeleton">
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index fda71fabe22..40fb0fbc91d 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -15,6 +15,10 @@ extend type WorkItem {
mockWidgets: [LocalWorkItemWidget]
}
+extend type WorkItemPermissions {
+ setWorkItemMetadata: Boolean
+}
+
input LocalUserInput {
id: ID!
name: String
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 3651cad48f6..86640a6d994 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -27,7 +27,7 @@ fragment WorkItem on WorkItem {
userPermissions {
deleteWorkItem
updateWorkItem
- setWorkItemMetadata
+ setWorkItemMetadata @client
}
widgets {
...WorkItemWidgets
diff --git a/app/assets/stylesheets/page_bundles/issues_list.scss b/app/assets/stylesheets/page_bundles/issues_list.scss
index f39dee12126..41515a98e0a 100644
--- a/app/assets/stylesheets/page_bundles/issues_list.scss
+++ b/app/assets/stylesheets/page_bundles/issues_list.scss
@@ -23,6 +23,11 @@
margin-bottom: 2px;
}
+ .issue-labels,
+ .author-link {
+ display: inline-block;
+ }
+
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
diff --git a/app/channels/awareness_channel.rb b/app/channels/awareness_channel.rb
deleted file mode 100644
index cf7ba0e5aaf..00000000000
--- a/app/channels/awareness_channel.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-class AwarenessChannel < ApplicationCable::Channel # rubocop:disable Gitlab/NamespacedClass
- REFRESH_INTERVAL = ENV.fetch("GITLAB_AWARENESS_REFRESH_INTERVAL_SEC", 60)
- private_constant :REFRESH_INTERVAL
-
- # Produces a refresh interval value, based of the
- # GITLAB_AWARENESS_REFRESH_INTERVAL_SEC environment variable or the given
- # default. Makes sure, that the interval after a jitter is applied, is never
- # less than half the predefined interval.
- def self.refresh_interval(range: -10..10)
- min = REFRESH_INTERVAL / 2.to_f
- [min.to_i, REFRESH_INTERVAL.to_i + rand(range)].max.seconds
- end
- private_class_method :refresh_interval
-
- # keep clients updated about session membership
- periodically every: refresh_interval do
- transmit payload
- end
-
- def subscribed
- reject unless valid_subscription?
- return if subscription_rejected?
-
- stream_for session, coder: ActiveSupport::JSON
-
- session.join(current_user)
- AwarenessChannel.broadcast_to(session, payload)
- end
-
- def unsubscribed
- return if subscription_rejected?
-
- session.leave(current_user)
- AwarenessChannel.broadcast_to(session, payload)
- end
-
- # Allows a client to let the server know they are still around. This is not
- # like a heartbeat mechanism. This can be triggered by any action that results
- # in a meaningful "presence" update. Like scrolling the screen (debounce),
- # window becoming active, user starting to type in a text field, etc.
- def touch
- session.touch!(current_user)
-
- transmit payload
- end
-
- private
-
- def valid_subscription?
- current_user.present? && path.present?
- end
-
- def payload
- { collaborators: collaborators }
- end
-
- def collaborators
- session.online_users_with_last_activity.map do |user, last_activity|
- collaborator(user, last_activity)
- end
- end
-
- def collaborator(user, last_activity)
- {
- id: user.id,
- name: user.name,
- username: user.username,
- avatar_url: user.avatar_url(size: 36),
- last_activity: last_activity,
- last_activity_humanized: ActionController::Base.helpers.distance_of_time_in_words(
- Time.zone.now, last_activity
- )
- }
- end
-
- def session
- @session ||= AwarenessSession.for(path)
- end
-
- def path
- params[:path]
- end
-end
diff --git a/app/graphql/types/permission_types/work_item.rb b/app/graphql/types/permission_types/work_item.rb
index 25d6b3e924d..f35f42001e0 100644
--- a/app/graphql/types/permission_types/work_item.rb
+++ b/app/graphql/types/permission_types/work_item.rb
@@ -6,7 +6,7 @@ module Types
graphql_name 'WorkItemPermissions'
description 'Check permissions for the current user on a work item'
- abilities :read_work_item, :update_work_item, :delete_work_item, :admin_work_item, :set_work_item_metadata
+ abilities :read_work_item, :update_work_item, :delete_work_item, :admin_work_item
end
end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index e2e89c9abca..00cf8e395bb 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -47,6 +47,7 @@ module AuthHelper
def qa_class_for_provider(provider)
{
+ github: 'qa-github-login-button',
saml: 'qa-saml-login-button'
}[provider.to_sym]
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 2442856d7fe..f2fa82aebdb 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -132,7 +132,7 @@ module PreferencesHelper
Gitlab::CurrentSettings.gitpod_url.presence || 'https://gitpod.io/'
end
- # Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too
+ # Ensure that anyone adding new options updates `localized_dashboard_choices` too
def validate_dashboard_choices!(user_dashboards)
if user_dashboards.size != localized_dashboard_choices.size
raise "`User` defines #{user_dashboards.size} dashboard choices," \
diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb
deleted file mode 100644
index 0b652984630..00000000000
--- a/app/models/awareness_session.rb
+++ /dev/null
@@ -1,245 +0,0 @@
-# frozen_string_literal: true
-
-# A Redis backed session store for real-time collaboration. A session is defined
-# by its documents and the users that join this session. An online user can have
-# two states within the session: "active" and "away".
-#
-# By design, session must eventually be cleaned up. If this doesn't happen
-# explicitly, all keys used within the session model must have an expiry
-# timestamp set.
-class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
- # An awareness session expires automatically after 1 hour of no activity
- SESSION_LIFETIME = 1.hour
- private_constant :SESSION_LIFETIME
-
- # Expire user awareness keys after some time of inactivity
- USER_LIFETIME = 1.hour
- private_constant :USER_LIFETIME
-
- PRESENCE_LIFETIME = 10.minutes
- private_constant :PRESENCE_LIFETIME
-
- KEY_NAMESPACE = "gitlab:awareness"
- private_constant :KEY_NAMESPACE
-
- class << self
- def for(value = nil)
- # Creates a unique value for situations where we have no unique value to
- # create a session with. This could be when creating a new issue, a new
- # merge request, etc.
- value = SecureRandom.uuid unless value.present?
-
- # We use SHA-256 based session identifiers (similar to abbreviated git
- # hashes). There is always a chance for Hash collisions (birthday
- # problem), we therefore have to pick a good tradeoff between the amount
- # of data stored and the probability of a collision.
- #
- # The approximate probability for a collision can be calculated:
- #
- # p ~= n^2 / 2m
- # ~= (2^18)^2 / (2 * 16^15)
- # ~= 2^36 / 2^61
- #
- # n is the number of awareness sessions and m the number of possibilities
- # for each item. For a hex number, this is 16^c, where c is the number of
- # characters. With 260k (~2^18) sessions, the probability for a collision
- # is ~2^-25.
- #
- # The number of 15 is selected carefully. The integer representation fits
- # nicely into a signed 64 bit integer and eventually allows Redis to
- # optimize its memory usage. 16 chars would exceed the space for
- # this datatype.
- id = Digest::SHA256.hexdigest(value.to_s)[0, 15]
-
- AwarenessSession.new(id)
- end
- end
-
- def initialize(id)
- @id = id
- end
-
- def join(user)
- user_key = user_sessions_key(user.id)
-
- with_redis do |redis|
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- pipeline.sadd?(user_key, id_i)
- pipeline.expire(user_key, USER_LIFETIME.to_i)
-
- pipeline.zadd(users_key, timestamp.to_f, user.id)
-
- # We also mark for expiry when a session key is created (first user joins),
- # because some users might never actively leave a session and the key could
- # therefore become stale, w/o us noticing.
- reset_session_expiry(pipeline)
- end
- end
- end
-
- nil
- end
-
- def leave(user)
- user_key = user_sessions_key(user.id)
-
- with_redis do |redis|
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- pipeline.srem?(user_key, id_i)
- pipeline.zrem(users_key, user.id)
- end
- end
-
- # cleanup orphan sessions and users
- #
- # this needs to be a second pipeline due to the delete operations being
- # dependent on the result of the cardinality checks
- user_sessions_count, session_users_count =
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- pipeline.scard(user_key)
- pipeline.zcard(users_key)
- end
- end
-
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- pipeline.del(user_key) unless user_sessions_count > 0
-
- unless session_users_count > 0
- pipeline.del(users_key)
- @id = nil
- end
- end
- end
- end
-
- nil
- end
-
- def present?(user, threshold: PRESENCE_LIFETIME)
- with_redis do |redis|
- user_timestamp = redis.zscore(users_key, user.id)
- break false unless user_timestamp.present?
-
- timestamp - user_timestamp < threshold
- end
- end
-
- def away?(user, threshold: PRESENCE_LIFETIME)
- !present?(user, threshold: threshold)
- end
-
- # Updates the last_activity timestamp for a user in this session
- def touch!(user)
- with_redis do |redis|
- redis.pipelined do |pipeline|
- pipeline.zadd(users_key, timestamp.to_f, user.id)
-
- # extend the session lifetime due to user activity
- reset_session_expiry(pipeline)
- end
- end
-
- nil
- end
-
- def size
- with_redis do |redis|
- redis.zcard(users_key)
- end
- end
-
- def to_param
- id&.to_s
- end
-
- def to_s
- "awareness_session=#{id}"
- end
-
- def online_users_with_last_activity(threshold: PRESENCE_LIFETIME)
- users_with_last_activity.filter do |_user, last_activity|
- user_online?(last_activity, threshold: threshold)
- end
- end
-
- def users
- User.where(id: user_ids)
- end
-
- def users_with_last_activity
- # where in (x, y, [...z]) is a set and does not maintain any order, we need
- # to make sure to establish a stable order for both, the pairs returned from
- # redis and the ActiveRecord query. Using IDs in ascending order.
- user_ids, last_activities = user_ids_with_last_activity
- .sort_by(&:first)
- .transpose
-
- return [] if user_ids.blank?
-
- users = User.where(id: user_ids).order(id: :asc)
- users.zip(last_activities)
- end
-
- private
-
- attr_reader :id
-
- def user_online?(last_activity, threshold:)
- last_activity.to_i + threshold.to_i > Time.zone.now.to_i
- end
-
- # converts session id from hex to integer representation
- def id_i
- Integer(id, 16) if id.present?
- end
-
- def users_key
- "#{KEY_NAMESPACE}:session:#{id}:users"
- end
-
- def user_sessions_key(user_id)
- "#{KEY_NAMESPACE}:user:#{user_id}:sessions"
- end
-
- def with_redis
- Gitlab::Redis::SharedState.with do |redis|
- yield redis if block_given?
- end
- end
-
- def timestamp
- Time.now.to_i
- end
-
- def user_ids
- with_redis do |redis|
- redis.zrange(users_key, 0, -1)
- end
- end
-
- # Returns an array of tuples, where the first element in the tuple represents
- # the user ID and the second part the last_activity timestamp.
- def user_ids_with_last_activity
- pairs = with_redis do |redis|
- redis.zrange(users_key, 0, -1, with_scores: true)
- end
-
- # map data type of score (float) to Time
- pairs.map do |user_id, score|
- [user_id, Time.zone.at(score.to_i)]
- end
- end
-
- # We want sessions to cleanup automatically after a certain period of
- # inactivity. This sets the expiry timestamp for this session to
- # [SESSION_LIFETIME].
- def reset_session_expiry(redis)
- redis.expire(users_key, SESSION_LIFETIME)
-
- nil
- end
-end
diff --git a/app/models/concerns/awareness.rb b/app/models/concerns/awareness.rb
deleted file mode 100644
index da87d87e838..00000000000
--- a/app/models/concerns/awareness.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module Awareness
- extend ActiveSupport::Concern
-
- KEY_NAMESPACE = "gitlab:awareness"
- private_constant :KEY_NAMESPACE
-
- def join(session)
- session.join(self)
-
- nil
- end
-
- def leave(session)
- session.leave(self)
-
- nil
- end
-
- def session_ids
- with_redis do |redis|
- redis
- .smembers(user_sessions_key)
- # converts session ids from (internal) integer to hex presentation
- .map { |key| key.to_i.to_s(16) }
- end
- end
-
- private
-
- def user_sessions_key
- "#{KEY_NAMESPACE}:user:#{id}:sessions"
- end
-
- def with_redis
- Gitlab::Redis::SharedState.with do |redis|
- yield redis if block_given?
- end
- end
-end
diff --git a/app/models/user.rb b/app/models/user.rb
index 86e8aace514..71ea185b6f1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -9,7 +9,6 @@ class User < ApplicationRecord
include Gitlab::SQL::Pattern
include AfterCommitQueue
include Avatarable
- include Awareness
include Referable
include Sortable
include CaseSensitivity
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 95d9caa686d..7156a0e5931 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -8,7 +8,7 @@
.title-container.hide-when-top-nav-responsive-open.gl-transition-medium.gl-display-flex.gl-align-items-stretch.gl-pt-0.gl-mr-3
.title
%span.gl-sr-only GitLab
- = link_to root_path, title: _('Dashboard'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation_top') do
+ = link_to root_path, title: _('Homepage'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation_top') do
= brand_header_logo
.gl-display-flex.gl-align-items-center
- if Gitlab.com_and_canary?
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 23dd824c268..c16469bbf79 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -78,7 +78,7 @@
= f.select :layout, layout_choices, {}, class: 'gl-form-select custom-select'
.form-text.text-muted
= s_('Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout.').html_safe % { percentage: '100%' }
- .js-listbox-input{ data: { label: s_('Preferences|Dashboard'), description: s_('Preferences|Choose what content you want to see by default on your dashboard.'), name: 'user[dashboard]', items: dashboard_choices.to_json, value: current_user.dashboard } }
+ .js-listbox-input{ data: { label: s_('Preferences|Homepage'), description: s_('Preferences|Choose what content you want to see by default on your homepage.'), name: 'user[dashboard]', items: dashboard_choices.to_json, value: current_user.dashboard } }
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific