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>2021-04-26 12:09:53 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-26 12:09:53 +0300
commit0ccabeb3f62c5fbc81f52cc16fa654404bb87874 (patch)
tree27c81cfa9d498fa0b604acaa9c4f5400743f83fd /app
parent6819cb95c9c0aa63fce1d246026978df5cac9e44 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/actioncable_link.js40
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js13
-rw-r--r--app/assets/javascripts/lib/graphql.js19
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue155
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js1
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue125
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue79
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue9
-rw-r--r--app/assets/javascripts/sidebar/constants.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js6
-rw-r--r--app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql16
-rw-r--r--app/helpers/issuables_helper.rb1
-rw-r--r--app/helpers/services_helper.rb4
-rw-r--r--app/models/service.rb20
-rw-r--r--app/services/admin/propagate_integration_service.rb2
-rw-r--r--app/views/admin/users/show.html.haml97
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml11
-rw-r--r--app/workers/propagate_integration_group_worker.rb2
-rw-r--r--app/workers/propagate_integration_project_worker.rb2
20 files changed, 395 insertions, 223 deletions
diff --git a/app/assets/javascripts/actioncable_link.js b/app/assets/javascripts/actioncable_link.js
new file mode 100644
index 00000000000..4c642db2f3c
--- /dev/null
+++ b/app/assets/javascripts/actioncable_link.js
@@ -0,0 +1,40 @@
+import { ApolloLink, Observable } from 'apollo-link';
+import { print } from 'graphql';
+import cable from '~/actioncable_consumer';
+import { uuids } from '~/diffs/utils/uuids';
+
+export default class ActionCableLink extends ApolloLink {
+ // eslint-disable-next-line class-methods-use-this
+ request(operation) {
+ return new Observable((observer) => {
+ const subscription = cable.subscriptions.create(
+ {
+ channel: 'GraphqlChannel',
+ query: operation.query ? print(operation.query) : null,
+ variables: operation.variables,
+ operationName: operation.operationName,
+ nonce: uuids()[0],
+ },
+ {
+ received(data) {
+ if (data.errors) {
+ observer.error(data.errors);
+ } else if (data.result) {
+ observer.next(data.result);
+ }
+
+ if (!data.more) {
+ observer.complete();
+ }
+ },
+ },
+ );
+
+ return {
+ unsubscribe() {
+ subscription.unsubscribe();
+ },
+ };
+ });
+ }
+}
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 22f88b1caa7..ee8384f734d 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -238,10 +238,13 @@ class GfmAutoComplete {
const MEMBER_COMMAND = {
ASSIGN: '/assign',
UNASSIGN: '/unassign',
+ ASSIGN_REVIEWER: '/assign_reviewer',
+ UNASSIGN_REVIEWER: '/unassign_reviewer',
REASSIGN: '/reassign',
CC: '/cc',
};
let assignees = [];
+ let reviewers = [];
let command = '';
// Team Members
@@ -286,9 +289,11 @@ class GfmAutoComplete {
return null;
});
- // Cache assignees list for easier filtering later
+ // Cache assignees & reviewers list for easier filtering later
assignees =
SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || [];
+ reviewers =
+ SidebarMediator.singleton?.store?.reviewers?.map(createMemberSearchString) || [];
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
return match && match.length ? match[1] : null;
@@ -309,6 +314,12 @@ class GfmAutoComplete {
} else if (command === MEMBER_COMMAND.UNASSIGN) {
// Only include members which are assigned to Issuable currently
return data.filter((member) => assignees.includes(member.search));
+ } else if (command === MEMBER_COMMAND.ASSIGN_REVIEWER) {
+ // Only include members which are not assigned as a reviewer to Issuable currently
+ return data.filter((member) => !reviewers.includes(member.search));
+ } else if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) {
+ // Only include members which are not assigned as a reviewer to Issuable currently
+ return data.filter((member) => reviewers.includes(member.search));
}
return data;
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 1630f0d689c..cec689a44ca 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -4,6 +4,7 @@ import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { createHttpLink } from 'apollo-link-http';
import { createUploadLink } from 'apollo-upload-client';
+import ActionCableLink from '~/actioncable_link';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
@@ -83,15 +84,27 @@ export default (resolvers = {}, config = {}) => {
});
});
- return new ApolloClient({
- typeDefs,
- link: ApolloLink.from([
+ const hasSubscriptionOperation = ({ query: { definitions } }) => {
+ return definitions.some(
+ ({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription',
+ );
+ };
+
+ const appLink = ApolloLink.split(
+ hasSubscriptionOperation,
+ new ActionCableLink(),
+ ApolloLink.from([
requestCounterLink,
performanceBarLink,
new StartupJSLink(),
apolloCaptchaLink,
uploadsLink,
]),
+ );
+
+ return new ApolloClient({
+ typeDefs,
+ link: appLink,
cache: new InMemoryCache({
...cacheConfig,
freezeResults: assumeImmutableResults,
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
new file mode 100644
index 00000000000..091b202e10b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
@@ -0,0 +1,155 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
+import { __, s__ } from '~/locale';
+import {
+ COMMIT_FAILURE,
+ COMMIT_SUCCESS,
+ DEFAULT_FAILURE,
+ DEFAULT_SUCCESS,
+ LOAD_FAILURE_UNKNOWN,
+} from '../../constants';
+import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue';
+import {
+ CODE_SNIPPET_SOURCE_URL_PARAM,
+ CODE_SNIPPET_SOURCES,
+} from '../code_snippet_alert/constants';
+
+export default {
+ components: {
+ GlAlert,
+ CodeSnippetAlert,
+ },
+ errorTexts: {
+ [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
+ [DEFAULT_FAILURE]: __('Something went wrong on our end.'),
+ [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
+ },
+ successTexts: {
+ [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
+ [DEFAULT_SUCCESS]: __('Your action succeeded.'),
+ },
+ props: {
+ failureType: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ failureReasons: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ showFailure: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showSuccess: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ successType: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ codeSnippetCopiedFrom: '',
+ };
+ },
+ computed: {
+ failure() {
+ switch (this.failureType) {
+ case LOAD_FAILURE_UNKNOWN:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
+ variant: 'danger',
+ };
+ case COMMIT_FAILURE:
+ return {
+ text: this.$options.errorTexts[COMMIT_FAILURE],
+ variant: 'danger',
+ };
+ default:
+ return {
+ text: this.$options.errorTexts[DEFAULT_FAILURE],
+ variant: 'danger',
+ };
+ }
+ },
+ success() {
+ switch (this.successType) {
+ case COMMIT_SUCCESS:
+ return {
+ text: this.$options.successTexts[COMMIT_SUCCESS],
+ variant: 'info',
+ };
+ default:
+ return {
+ text: this.$options.successTexts[DEFAULT_SUCCESS],
+ variant: 'info',
+ };
+ }
+ },
+ },
+ created() {
+ this.parseCodeSnippetSourceParam();
+ },
+ methods: {
+ dismissCodeSnippetAlert() {
+ this.codeSnippetCopiedFrom = '';
+ },
+ dismissFailure() {
+ this.$emit('hide-failure');
+ },
+ dismissSuccess() {
+ this.$emit('hide-success');
+ },
+ parseCodeSnippetSourceParam() {
+ const [codeSnippetCopiedFrom] = getParameterValues(CODE_SNIPPET_SOURCE_URL_PARAM);
+ if (codeSnippetCopiedFrom && CODE_SNIPPET_SOURCES.includes(codeSnippetCopiedFrom)) {
+ this.codeSnippetCopiedFrom = codeSnippetCopiedFrom;
+ window.history.replaceState(
+ {},
+ document.title,
+ removeParams([CODE_SNIPPET_SOURCE_URL_PARAM]),
+ );
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <code-snippet-alert
+ v-if="codeSnippetCopiedFrom"
+ :source="codeSnippetCopiedFrom"
+ class="gl-mb-5"
+ @dismiss="dismissCodeSnippetAlert"
+ />
+ <gl-alert
+ v-if="showSuccess"
+ :variant="success.variant"
+ class="gl-mb-5"
+ @dismiss="dismissSuccess"
+ >
+ {{ success.text }}
+ </gl-alert>
+ <gl-alert
+ v-if="showFailure"
+ :variant="failure.variant"
+ class="gl-mb-5"
+ @dismiss="dismissFailure"
+ >
+ {{ failure.text }}
+ <ul v-if="failureReasons.length" class="gl-mb-0">
+ <li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
+ </ul>
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index 8d0ec6c3e2d..56862f17858 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -14,6 +14,7 @@ export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
+export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export const CREATE_TAB = 'CREATE_TAB';
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 5f9662e90e6..bf36e00c662 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,22 +1,15 @@
<script>
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import httpStatusCodes from '~/lib/utils/http_status';
-import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
-import CodeSnippetAlert from './components/code_snippet_alert/code_snippet_alert.vue';
-import {
- CODE_SNIPPET_SOURCE_URL_PARAM,
- CODE_SNIPPET_SOURCES,
-} from './components/code_snippet_alert/constants';
+
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
+import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue';
import {
- COMMIT_FAILURE,
- COMMIT_SUCCESS,
- DEFAULT_FAILURE,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
@@ -32,11 +25,10 @@ import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
components: {
ConfirmUnsavedChangesDialog,
- GlAlert,
GlLoadingIcon,
PipelineEditorEmptyState,
PipelineEditorHome,
- CodeSnippetAlert,
+ PipelineEditorMessages,
},
inject: {
ciConfigPath: {
@@ -51,15 +43,14 @@ export default {
ciConfigData: {},
failureType: null,
failureReasons: [],
- showStartScreen: false,
initialCiFileContent: '',
isNewCiConfigFile: false,
lastCommittedContent: '',
currentCiFileContent: '',
- showFailureAlert: false,
- showSuccessAlert: false,
successType: null,
- codeSnippetCopiedFrom: '',
+ showStartScreen: false,
+ showSuccess: false,
+ showFailure: false,
};
},
@@ -152,50 +143,12 @@ export default {
isEmpty() {
return this.currentCiFileContent === '';
},
- failure() {
- switch (this.failureType) {
- case LOAD_FAILURE_UNKNOWN:
- return {
- text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
- variant: 'danger',
- };
- case COMMIT_FAILURE:
- return {
- text: this.$options.errorTexts[COMMIT_FAILURE],
- variant: 'danger',
- };
- default:
- return {
- text: this.$options.errorTexts[DEFAULT_FAILURE],
- variant: 'danger',
- };
- }
- },
- success() {
- switch (this.successType) {
- case COMMIT_SUCCESS:
- return {
- text: this.$options.successTexts[COMMIT_SUCCESS],
- variant: 'info',
- };
- default:
- return null;
- }
- },
},
i18n: {
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
- errorTexts: {
- [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
- [DEFAULT_FAILURE]: __('Something went wrong on our end.'),
- [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
- },
- successTexts: {
- [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
- },
watch: {
isEmpty(flag) {
if (flag) {
@@ -203,9 +156,6 @@ export default {
}
},
},
- created() {
- this.parseCodeSnippetSourceParam();
- },
methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
@@ -223,12 +173,11 @@ export default {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
}
},
-
- dismissFailure() {
- this.showFailureAlert = false;
+ hideFailure() {
+ this.showFailure = false;
},
- dismissSuccess() {
- this.showSuccessAlert = false;
+ hideSuccess() {
+ this.showSuccess = false;
},
async refetchContent() {
this.$apollo.queries.initialCiFileContent.skip = false;
@@ -238,13 +187,13 @@ export default {
this.setAppStatus(EDITOR_APP_STATUS_ERROR);
window.scrollTo({ top: 0, behavior: 'smooth' });
- this.showFailureAlert = true;
+ this.showFailure = true;
this.failureType = type;
this.failureReasons = reasons;
},
reportSuccess(type) {
window.scrollTo({ top: 0, behavior: 'smooth' });
- this.showSuccessAlert = true;
+ this.showSuccess = true;
this.successType = type;
},
resetContent() {
@@ -277,20 +226,6 @@ export default {
// if the user has made changes to the file that are unsaved.
this.lastCommittedContent = this.currentCiFileContent;
},
- parseCodeSnippetSourceParam() {
- const [codeSnippetCopiedFrom] = getParameterValues(CODE_SNIPPET_SOURCE_URL_PARAM);
- if (codeSnippetCopiedFrom && CODE_SNIPPET_SOURCES.includes(codeSnippetCopiedFrom)) {
- this.codeSnippetCopiedFrom = codeSnippetCopiedFrom;
- window.history.replaceState(
- {},
- document.title,
- removeParams([CODE_SNIPPET_SOURCE_URL_PARAM]),
- );
- }
- },
- dismissCodeSnippetAlert() {
- this.codeSnippetCopiedFrom = '';
- },
},
};
</script>
@@ -303,31 +238,15 @@ export default {
@createEmptyConfigFile="setNewEmptyCiConfigFile"
/>
<div v-else>
- <code-snippet-alert
- v-if="codeSnippetCopiedFrom"
- :source="codeSnippetCopiedFrom"
- class="gl-mb-5"
- @dismiss="dismissCodeSnippetAlert"
+ <pipeline-editor-messages
+ :failure-type="failureType"
+ :failure-reasons="failureReasons"
+ :show-failure="showFailure"
+ :show-success="showSuccess"
+ :success-type="successType"
+ @hide-success="hideSuccess"
+ @hide-failure="hideFailure"
/>
- <gl-alert
- v-if="showSuccessAlert"
- :variant="success.variant"
- class="gl-mb-5"
- @dismiss="dismissSuccess"
- >
- {{ success.text }}
- </gl-alert>
- <gl-alert
- v-if="showFailureAlert"
- :variant="failure.variant"
- class="gl-mb-5"
- @dismiss="dismissFailure"
- >
- {{ failure.text }}
- <ul v-if="failureReasons.length" class="gl-mb-0">
- <li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
- </ul>
- </gl-alert>
<pipeline-editor-home
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index f98798582c1..e7ef731eed8 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,6 +1,7 @@
<script>
-import actionCable from '~/actioncable_consumer';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import produce from 'immer';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { IssuableType } from '~/issue_show/constants';
import { assigneesQueries } from '~/sidebar/constants';
export default {
@@ -12,60 +13,62 @@ export default {
required: false,
default: null,
},
- issuableIid: {
+ issuableType: {
type: String,
required: true,
},
- projectPath: {
- type: String,
+ issuableId: {
+ type: Number,
required: true,
},
- issuableType: {
- type: String,
+ queryVariables: {
+ type: Object,
required: true,
},
},
+ computed: {
+ issuableClass() {
+ return Object.keys(IssuableType).find((key) => IssuableType[key] === this.issuableType);
+ },
+ },
apollo: {
- workspace: {
+ issuable: {
query() {
return assigneesQueries[this.issuableType].query;
},
variables() {
- return {
- iid: this.issuableIid,
- fullPath: this.projectPath,
- };
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.workspace?.issuable;
},
- result(data) {
- if (this.mediator) {
- this.handleFetchResult(data);
- }
+ subscribeToMore: {
+ document() {
+ return assigneesQueries[this.issuableType].subscription;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId(this.issuableClass, this.issuableId),
+ };
+ },
+ updateQuery(prev, { subscriptionData }) {
+ if (prev && subscriptionData?.data?.issuableAssigneesUpdated) {
+ const data = produce(prev, (draftData) => {
+ draftData.workspace.issuable.assignees.nodes =
+ subscriptionData.data.issuableAssigneesUpdated.assignees.nodes;
+ });
+ if (this.mediator) {
+ this.handleFetchResult(data);
+ }
+ return data;
+ }
+ return prev;
+ },
},
},
},
- mounted() {
- this.initActionCablePolling();
- },
- beforeDestroy() {
- this.$options.subscription.unsubscribe();
- },
methods: {
- received(data) {
- if (data.event === 'updated') {
- this.$apollo.queries.workspace.refetch();
- }
- },
- initActionCablePolling() {
- this.$options.subscription = actionCable.subscriptions.create(
- {
- channel: 'IssuesChannel',
- project_path: this.projectPath,
- iid: this.issuableIid,
- },
- { received: this.received },
- );
- },
- handleFetchResult({ data }) {
+ handleFetchResult(data) {
const { nodes } = data.workspace.issuable.assignees;
const assignees = nodes.map((n) => ({
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index e15ea595190..ca95599742a 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -44,6 +44,10 @@ export default {
type: String,
required: true,
},
+ issuableId: {
+ type: Number,
+ required: true,
+ },
assigneeAvailabilityStatus: {
type: Object,
required: false,
@@ -61,6 +65,12 @@ export default {
// Note: Realtime is only available on issues right now, future support for MR wil be built later.
return this.glFeatures.realTimeIssueSidebar && this.issuableType === 'issue';
},
+ queryVariables() {
+ return {
+ iid: this.issuableIid,
+ fullPath: this.projectPath,
+ };
+ },
relativeUrlRoot() {
return gon.relative_url_root ?? '';
},
@@ -121,9 +131,9 @@ export default {
<div>
<assignees-realtime
v-if="shouldEnableRealtime"
- :issuable-iid="issuableIid"
- :project-path="projectPath"
:issuable-type="issuableType"
+ :issuable-id="issuableId"
+ :query-variables="queryVariables"
:mediator="mediator"
/>
<assignee-title
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index 78cac989850..2fc25151d1c 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -73,6 +73,11 @@ export default {
return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
},
},
+ issuableId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
multipleAssignees: {
type: Boolean,
required: false,
@@ -340,9 +345,9 @@ export default {
<div data-testid="assignees-widget">
<sidebar-assignees-realtime
v-if="shouldEnableRealtime"
- :project-path="fullPath"
- :issuable-iid="iid"
:issuable-type="issuableType"
+ :issuable-id="issuableId"
+ :query-variables="queryVariables"
/>
<sidebar-editable-item
ref="toggle"
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 80e07d556bf..58e4b0348ca 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,5 +1,6 @@
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.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';
@@ -17,6 +18,7 @@ export const ASSIGNEES_DEBOUNCE_DELAY = 250;
export const assigneesQueries = {
[IssuableType.Issue]: {
query: getIssueParticipants,
+ subscription: issuableAssigneesSubscription,
mutation: updateAssigneesMutation,
},
[IssuableType.MergeRequest]: {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 1304e84814b..52a1efa04e4 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -53,7 +53,7 @@ function mountAssigneesComponentDeprecated(mediator) {
if (!el) return;
- const { iid, fullPath } = getSidebarOptions();
+ const { id, iid, fullPath } = getSidebarOptions();
const assigneeAvailabilityStatus = getSidebarAssigneeAvailabilityData();
// eslint-disable-next-line no-new
new Vue({
@@ -74,6 +74,7 @@ function mountAssigneesComponentDeprecated(mediator) {
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
: IssuableType.MergeRequest,
+ issuableId: id,
assigneeAvailabilityStatus,
},
}),
@@ -85,7 +86,7 @@ function mountAssigneesComponent() {
if (!el) return;
- const { iid, fullPath, editable, projectMembersPath } = getSidebarOptions();
+ const { id, iid, fullPath, editable, projectMembersPath } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
@@ -108,6 +109,7 @@ function mountAssigneesComponent() {
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
: IssuableType.MergeRequest,
+ issuableId: id,
multipleAssignees: !el.dataset.maxAssignees,
},
scopedSlots: {
diff --git a/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql b/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql
new file mode 100644
index 00000000000..47ce094418c
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql
@@ -0,0 +1,16 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+subscription issuableAssigneesUpdated($issuableId: IssuableID!) {
+ issuableAssigneesUpdated(issuableId: $issuableId) {
+ ... on Issue {
+ assignees {
+ nodes {
+ ...User
+ status {
+ availability
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index c6fde38579b..e5ea2920eaa 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -386,6 +386,7 @@ module IssuablesHelper
rootPath: root_path,
fullPath: issuable[:project_full_path],
iid: issuable[:iid],
+ id: issuable[:id],
severity: issuable[:severity],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours,
createNoteEmail: issuable[:create_note_email],
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index ffa09cb12fb..0711a6b7e97 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -153,9 +153,9 @@ module ServicesHelper
private
def integration_level(integration)
- if integration.instance
+ if integration.instance_level?
'instance'
- elsif integration.group_id
+ elsif integration.group_level?
'group'
else
'project'
diff --git a/app/models/service.rb b/app/models/service.rb
index 7782b016b52..51ac4555c1f 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -51,14 +51,14 @@ class Service < ApplicationRecord
belongs_to :group, inverse_of: :services
has_one :service_hook
- validates :project_id, presence: true, unless: -> { template? || instance? || group_id }
- validates :group_id, presence: true, unless: -> { template? || instance? || project_id }
- validates :project_id, :group_id, absence: true, if: -> { template? || instance? }
+ validates :project_id, presence: true, unless: -> { template? || instance_level? || group_level? }
+ validates :group_id, presence: true, unless: -> { template? || instance_level? || project_level? }
+ validates :project_id, :group_id, absence: true, if: -> { template? || instance_level? }
validates :type, presence: true
validates :type, uniqueness: { scope: :template }, if: :template?
- validates :type, uniqueness: { scope: :instance }, if: :instance?
- validates :type, uniqueness: { scope: :project_id }, if: :project_id?
- validates :type, uniqueness: { scope: :group_id }, if: :group_id?
+ validates :type, uniqueness: { scope: :instance }, if: :instance_level?
+ validates :type, uniqueness: { scope: :project_id }, if: :project_level?
+ validates :type, uniqueness: { scope: :group_id }, if: :group_level?
validate :validate_is_instance_or_template
validate :validate_belongs_to_project_or_group
@@ -240,7 +240,7 @@ class Service < ApplicationRecord
service.instance = false
service.project_id = project_id
service.group_id = group_id
- service.inherit_from_id = integration.id if integration.instance? || integration.group
+ service.inherit_from_id = integration.id if integration.instance_level? || integration.group_level?
service
end
@@ -409,7 +409,7 @@ class Service < ApplicationRecord
# Disable test for instance-level and group-level services.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def can_test?
- !instance? && !group_id
+ !(instance_level? || group_level?)
end
def project_level?
@@ -460,11 +460,11 @@ class Service < ApplicationRecord
private
def validate_is_instance_or_template
- errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance?
+ errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance_level?
end
def validate_belongs_to_project_or_group
- errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_id && group_id
+ errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_level? && group_level?
end
def validate_recipients?
diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb
index 253c3a84fef..2ce1756ef1a 100644
--- a/app/services/admin/propagate_integration_service.rb
+++ b/app/services/admin/propagate_integration_service.rb
@@ -5,7 +5,7 @@ module Admin
include PropagateService
def propagate
- if integration.instance?
+ if integration.instance_level?
update_inherited_integrations
create_integration_for_groups_without_integration
create_integration_for_projects_without_integration
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index c7ec3ab66d7..db5f58f8bd0 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -12,7 +12,7 @@
%li
= image_tag avatar_icon_for_user(@user, 60), class: "avatar s60"
%li
- %span.light Profile page:
+ %span.light= _('Profile page:')
%strong
= link_to user_path(@user) do
= @user.username
@@ -20,25 +20,25 @@
.card
.card-header
- Account:
+ = _('Account:')
%ul.content-list
%li
- %span.light Name:
+ %span.light= _('Name:')
%strong= @user.name
%li
- %span.light Username:
+ %span.light= _('Username:')
%strong
= @user.username
%li
- %span.light Email:
+ %span.light= _('Email:')
%strong
= render partial: 'shared/email_with_badge', locals: { email: mail_to(@user.email), verified: @user.confirmed? }
- @user.emails.each do |email|
%li
- %span.light Secondary email:
+ %span.light= _('Secondary email:')
%strong
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
- = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
+ = link_to remove_email_admin_user_path(@user, email), data: { confirm: _("Are you sure you want to remove %{email}?") % { email: email.email } }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: _('Remove secondary email'), id: "remove_email_#{email.id}" do
= sprite_icon('close', size: 16, css_class: 'gl-icon')
%li
%span.light ID:
@@ -50,65 +50,65 @@
= @user.namespace_id
%li.two-factor-status
- %span.light Two-factor Authentication:
+ %span.light= _('Two-factor Authentication:')
%strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- if @user.two_factor_enabled?
- Enabled
- = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: 'Disable Two-factor Authentication'
+ = _('Enabled')
+ = link_to _('Disable'), disable_two_factor_admin_user_path(@user), data: { confirm: _('Are you sure?') }, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: _('Disable Two-factor Authentication')
- else
- Disabled
+ = _('Disabled')
= render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
%li
- %span.light External User:
+ %span.light= _('External User:')
%strong
- = @user.external? ? "Yes" : "No"
+ = @user.external? ? _('Yes') : _('No')
%li
- %span.light Can create groups:
+ %span.light= _('Can create groups:')
%strong
- = @user.can_create_group ? "Yes" : "No"
+ = @user.can_create_group ? _('Yes') : _('No')
%li
- %span.light Personal projects limit:
+ %span.light= _('Personal projects limit:')
%strong
= @user.projects_limit
%li
- %span.light Member since:
+ %span.light= _('Member since:')
%strong
= @user.created_at.to_s(:medium)
- if @user.confirmed_at
%li
- %span.light Confirmed at:
+ %span.light= _('Confirmed at:')
%strong
= @user.confirmed_at.to_s(:medium)
- else
%li
- %span.light Confirmed:
+ %span.ligh= _('Confirmed:')
%strong.cred
- No
+ = _('No')
%li
- %span.light Current sign-in IP:
+ %span.light= _('Current sign-in IP:')
%strong
= @user.current_sign_in_ip || _('never')
%li
- %span.light Current sign-in at:
+ %span.light= _('Current sign-in at:')
%strong
= @user.current_sign_in_at&.to_s(:medium) || _('never')
%li
- %span.light Last sign-in IP:
+ %span.light= _('Last sign-in IP:')
%strong
= @user.last_sign_in_ip || _('never')
%li
- %span.light Last sign-in at:
+ %span.light= _('Last sign-in at:')
%strong
= @user.last_sign_in_at&.to_s(:medium) || _('never')
%li
- %span.light Sign-in count:
+ %span.light= _('Sign-in count:')
%strong
= @user.sign_in_count
@@ -121,13 +121,13 @@
- if @user.ldap_user?
%li
- %span.light LDAP uid:
+ %span.light= _('LDAP uid:')
%strong
= @user.ldap_identity.extern_uid
- if @user.created_by
%li
- %span.light Created by:
+ %span.light= _('Created by:')
%strong
= link_to @user.created_by.name, [:admin, @user.created_by]
@@ -140,13 +140,13 @@
- if can_force_email_confirmation?(@user)
.gl-card.border-info.gl-mb-5
.gl-card-header.bg-info.text-white
- Confirm user
+ = _('Confirm user')
.gl-card-body
- if @user.unconfirmed_email.present?
- email = " (#{@user.unconfirmed_email})"
- %p This user has an unconfirmed email address#{email}. You may force a confirmation.
+ %p= _('This user has an unconfirmed email address %{email}. You may force a confirmation.') % { email: email }
%br
- = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' }
+ = link_to _('Confirm user'), confirm_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: _('Are you sure?'), qa_selector: 'confirm_user_button' }
= render 'admin/users/user_detail_note'
@@ -154,7 +154,7 @@
- if @user.deactivated?
.gl-card.border-info.gl-mb-5
.gl-card-header.bg-info.text-white
- Reactivate this user
+ = _('Reactivate this user')
.gl-card-body
= render partial: 'admin/users/user_activation_effects'
%br
@@ -163,7 +163,7 @@
- elsif @user.can_be_deactivated?
.gl-card.border-warning.gl-mb-5
.gl-card-header.bg-warning.text-white
- Deactivate this user
+ = _('Deactivate this user')
.gl-card-body
= user_deactivation_effects
%br
@@ -176,12 +176,12 @@
- else
.gl-card.border-info.gl-mb-5
.gl-card-header.gl-bg-blue-500.gl-text-white
- This user is blocked
+ = _('This user is blocked')
.gl-card-body
- %p A blocked user cannot:
+ %p= _('A blocked user cannot:')
%ul
- %li Log in
- %li Access Git repositories
+ %li= _('Log in')
+ %li= _('Access Git repositories')
%br
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: user_unblock_data(@user) }
= s_('AdminUsers|Unblock user')
@@ -191,18 +191,18 @@
- if @user.access_locked?
.card.border-info.gl-mb-5
.card-header.bg-info.text-white
- This account has been locked
+ = _('This account has been locked')
.card-body
- %p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.
+ %p= _('This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.')
%br
- = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
+ = link_to _('Unlock user'), unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: _('Are you sure?') }
- if !@user.blocked_pending_approval?
.gl-card.border-danger.gl-mb-5
.gl-card-header.bg-danger.text-white
= s_('AdminUsers|Delete user')
.gl-card-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
- %p Deleting a user has the following effects:
+ %p= _('Deleting a user has the following effects:')
= render 'users/deletion_guidance', user: @user
%br
%button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete',
@@ -213,13 +213,13 @@
- else
- if @user.solo_owned_groups.present?
%p
- This user is currently an owner in these groups:
+ = _('This user is currently an owner in these groups:')
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
- You must transfer ownership or delete these groups before you can delete this user.
+ = _('You must transfer ownership or delete these groups before you can delete this user.')
- else
%p
- You don't have access to delete this user.
+ = _("You don't have access to delete this user.")
.gl-card.border-danger
.gl-card-header.bg-danger.text-white
@@ -227,13 +227,8 @@
.gl-card-body
- if can?(current_user, :destroy_user, @user)
%p
- This option deletes the user and any contributions that
- would usually be moved to the
- = succeed "." do
- = link_to "system ghost user", help_page_path("user/profile/account/delete_account")
- As well as the user's personal projects, groups owned solely by
- the user, and projects in them, will also be removed. Commits
- to other projects are unaffected.
+ - link_to_ghost_user = link_to(_("system ghost user"), help_page_path("user/profile/account/delete_account"))
+ = _("This option deletes the user and any contributions that would usually be moved to the %{link_to_ghost_user}. As well as the user's personal projects, groups owned solely by the user, and projects in them, will also be removed. Commits to other projects are unaffected.").html_safe % { link_to_ghost_user: link_to_ghost_user }
%br
%button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(@user, hard_delete: true),
@@ -242,6 +237,6 @@
= s_('AdminUsers|Delete user and contributions')
- else
%p
- You don't have access to delete this user.
+ = _("You don't have access to delete this user.")
= render partial: 'admin/users/modals'
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index 01f3e441eef..1bf252b6282 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -9,13 +9,13 @@
%h4.gl-mt-0
= page_title
%p
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/project_access_tokens') }
- if current_user.can?(:create_resource_access_tokens, @project)
- = _('You can generate an access token scoped to this project for each application to use the GitLab API.')
- -# Commented out until https://gitlab.com/gitlab-org/gitlab/-/issues/219551 is fixed
- -# %p
- -# = _('You can also use project access tokens to authenticate against Git over HTTP.')
+ = _('Generate project access tokens scoped to this project for your applications that need access to the GitLab API.')
+ %p
+ = _('You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- else
- = _('Project access token creation is disabled in this group. You can still use and manage existing tokens.')
+ = _('Project access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
%p
- root_group = @project.group.root_ancestor
- if current_user.can?(:admin_group, root_group)
@@ -23,7 +23,6 @@
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_settings_link }
= _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
-
.col-lg-8
- if @new_project_access_token
= render 'shared/access_tokens/created_container',
diff --git a/app/workers/propagate_integration_group_worker.rb b/app/workers/propagate_integration_group_worker.rb
index 01155753877..f763406b380 100644
--- a/app/workers/propagate_integration_group_worker.rb
+++ b/app/workers/propagate_integration_group_worker.rb
@@ -11,7 +11,7 @@ class PropagateIntegrationGroupWorker
integration = Service.find_by_id(integration_id)
return unless integration
- batch = if integration.instance?
+ batch = if integration.instance_level?
Group.where(id: min_id..max_id).without_integration(integration)
else
integration.group.descendants.where(id: min_id..max_id).without_integration(integration)
diff --git a/app/workers/propagate_integration_project_worker.rb b/app/workers/propagate_integration_project_worker.rb
index 188d81e5fc1..50c68c85f03 100644
--- a/app/workers/propagate_integration_project_worker.rb
+++ b/app/workers/propagate_integration_project_worker.rb
@@ -12,7 +12,7 @@ class PropagateIntegrationProjectWorker
return unless integration
batch = Project.where(id: min_id..max_id).without_integration(integration)
- batch = batch.in_namespace(integration.group.self_and_descendants) if integration.group_id
+ batch = batch.in_namespace(integration.group.self_and_descendants) if integration.group_level?
return if batch.empty?