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
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
parent6819cb95c9c0aa63fce1d246026978df5cac9e44 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml3
-rw-r--r--.rubocop_manual_todo.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-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
-rw-r--r--changelogs/unreleased/Externalize-strings-in-show-html-haml.yml5
-rw-r--r--changelogs/unreleased/ap-access-tokens-ui-text-okr.yml5
-rw-r--r--changelogs/unreleased/issue-220040-fix-rails-savebang-wiki-model.yml5
-rw-r--r--config/metrics/counts_28d/20210216181002_projects_with_tracing_enabled.yml12
-rw-r--r--config/metrics/counts_all/20210216180929_projects_with_tracing_enabled.yml8
-rw-r--r--config/metrics/counts_all/20210216180951_projects_with_tracing_enabled.yml10
-rw-r--r--doc/api/graphql/reference/index.md16
-rw-r--r--doc/development/usage_ping/dictionary.md6
-rw-r--r--doc/university/training/index.md1
-rw-r--r--doc/university/training/topics/agile_git.md33
-rw-r--r--lib/gitlab/graphql/docs/helper.rb2
-rw-r--r--locale/gitlab.pot128
-rw-r--r--spec/features/action_cable_logging_spec.rb6
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb53
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb2
-rw-r--r--spec/frontend/actioncable_link_spec.js110
-rw-r--r--spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js137
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js58
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js108
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js3
-rw-r--r--spec/frontend/sidebar/mock_data.js6
-rw-r--r--spec/frontend/sidebar/sidebar_assignees_spec.js1
-rw-r--r--spec/models/wiki_page/meta_spec.rb4
46 files changed, 925 insertions, 419 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 229067167af..d086bdf7361 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -575,6 +575,9 @@ Rails/SaveBang:
- 'ee/spec/**/*.rb'
- 'qa/spec/**/*.rb'
- 'qa/qa/specs/**/*.rb'
+ Exclude:
+ - spec/models/wiki_page/**/*
+ - spec/models/wiki_page_spec.rb
Cop/PutProjectRoutesUnderScope:
Include:
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index c6fe01b3ac6..35676e023d8 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -245,8 +245,6 @@ Rails/SaveBang:
- 'spec/models/user_preference_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/models/user_status_spec.rb'
- - 'spec/models/wiki_page/meta_spec.rb'
- - 'spec/models/wiki_page_spec.rb'
Rails/TimeZone:
Enabled: true
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 62eee2ff3ca..d81d04162c1 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-b19cafa222fd7a999167d3f9f8562c2d74b62bfd
+5658d720f02d2c84b51feaae484ea68aeeb59773
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?
diff --git a/changelogs/unreleased/Externalize-strings-in-show-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-show-html-haml.yml
new file mode 100644
index 00000000000..f4926316a54
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-show-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings in /users/show.html.haml
+merge_request: 58126
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/ap-access-tokens-ui-text-okr.yml b/changelogs/unreleased/ap-access-tokens-ui-text-okr.yml
new file mode 100644
index 00000000000..b9926faa4c1
--- /dev/null
+++ b/changelogs/unreleased/ap-access-tokens-ui-text-okr.yml
@@ -0,0 +1,5 @@
+---
+title: Revise project access tokens UI text
+merge_request: 59878
+author:
+type: other
diff --git a/changelogs/unreleased/issue-220040-fix-rails-savebang-wiki-model.yml b/changelogs/unreleased/issue-220040-fix-rails-savebang-wiki-model.yml
new file mode 100644
index 00000000000..14becc0eac8
--- /dev/null
+++ b/changelogs/unreleased/issue-220040-fix-rails-savebang-wiki-model.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Rails/SaveBang Rubocop offenses for wiki_page models
+merge_request: 57899
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/config/metrics/counts_28d/20210216181002_projects_with_tracing_enabled.yml b/config/metrics/counts_28d/20210216181002_projects_with_tracing_enabled.yml
index 6a559c1460f..b934fa26cd4 100644
--- a/config/metrics/counts_28d/20210216181002_projects_with_tracing_enabled.yml
+++ b/config/metrics/counts_28d/20210216181002_projects_with_tracing_enabled.yml
@@ -4,13 +4,15 @@ description: Projects with tracing enabled
product_section: ops
product_stage:
product_group: group::monitor
-product_category:
+product_category: tracing
value_type: number
status: data_available
time_frame: 28d
-data_source:
+data_source: database
distribution:
-- ce
+ - ce
+ - ee
tier:
-- free
-skip_validation: true
+ - free
+ - premium
+ - ultimate
diff --git a/config/metrics/counts_all/20210216180929_projects_with_tracing_enabled.yml b/config/metrics/counts_all/20210216180929_projects_with_tracing_enabled.yml
index 6ce96c5750d..a3a5f2d8ffb 100644
--- a/config/metrics/counts_all/20210216180929_projects_with_tracing_enabled.yml
+++ b/config/metrics/counts_all/20210216180929_projects_with_tracing_enabled.yml
@@ -10,7 +10,9 @@ status: data_available
time_frame: all
data_source: database
distribution:
-- ce
+ - ce
+ - ee
tier:
-- free
-skip_validation: true
+ - free
+ - premium
+ - ultimate
diff --git a/config/metrics/counts_all/20210216180951_projects_with_tracing_enabled.yml b/config/metrics/counts_all/20210216180951_projects_with_tracing_enabled.yml
index fc362feda69..26c5073cae1 100644
--- a/config/metrics/counts_all/20210216180951_projects_with_tracing_enabled.yml
+++ b/config/metrics/counts_all/20210216180951_projects_with_tracing_enabled.yml
@@ -8,9 +8,11 @@ product_category: tracing
value_type: number
status: data_available
time_frame: all
-data_source:
+data_source: database
distribution:
-- ce
+ - ce
+ - ee
tier:
-- free
-skip_validation: true
+ - free
+ - premium
+ - ultimate
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index fcf9e4b91cf..beff7def776 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11900,22 +11900,6 @@ Represents the Geo sync and verification state of a snippet repository.
| <a id="submoduletype"></a>`type` | [`EntryType!`](#entrytype) | Type of tree entry. |
| <a id="submoduleweburl"></a>`webUrl` | [`String`](#string) | Web URL for the sub-module. |
-### `Subscription`
-
-#### Fields with arguments
-
-##### `Subscription.issuableAssigneesUpdated`
-
-Triggered when the assignees of an issuable are updated.
-
-Returns [`Issuable`](#issuable).
-
-###### Arguments
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| <a id="subscriptionissuableassigneesupdatedissuableid"></a>`issuableId` | [`IssuableID!`](#issuableid) | ID of the issuable. |
-
### `TaskCompletionStatus`
Completion status of tasks.
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index 99a29a9f2c8..dc2d5d65872 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -5082,7 +5082,7 @@ Group: `group::monitor`
Status: `data_available`
-Tiers: `free`
+Tiers: `free`, `premium`, `ultimate`
### `counts.projects_youtrack_active`
@@ -15680,7 +15680,7 @@ Group: `group::monitor`
Status: `data_available`
-Tiers: `free`
+Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage.package.projects_with_packages`
@@ -17600,7 +17600,7 @@ Group: `group::monitor`
Status: `data_available`
-Tiers: `free`
+Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage_monthly.package.projects_with_packages`
diff --git a/doc/university/training/index.md b/doc/university/training/index.md
index f69bd51b341..7aabd6b2757 100644
--- a/doc/university/training/index.md
+++ b/doc/university/training/index.md
@@ -17,7 +17,6 @@ All training material is open to public contribution.
This section contains the following topics:
-- [Agile and Git](topics/agile_git.md).
- [Bisect](topics/bisect.md).
- [Cherry pick](topics/cherry_picking.md).
- [Code review and collaboration with Merge Requests](topics/merge_requests.md).
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
index cb82d3cec64..f912f92fad2 100644
--- a/doc/university/training/topics/agile_git.md
+++ b/doc/university/training/topics/agile_git.md
@@ -1,33 +1,8 @@
---
-stage: none
-group: unassigned
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
-comments: false
+redirect_to: '../../../user/project/issue_board.md'
---
-# Agile and Git
+Information about using Agile concepts in GitLab can be found in [another location](../../../user/project/issue_board.md).
-## Agile
-
-Lean software development methods focused on collaboration and interaction
-with fast and smaller deployment cycles.
-
-## Where Git comes in
-
-Git is an excellent tool for an Agile team considering that it allows
-decentralized and simultaneous development.
-
-### Branching And Workflows
-
-Branching in an Agile environment usually happens around user stories with one
-or more developers working on it.
-
-If more than one developer then another branch for each developer is also used
-with their initials, and US ID.
-
-After its tested merge into master and remove the branch.
-
-## What about GitLab
-
-Tools like GitLab enhance collaboration by adding dialog around code mainly
-through issues and merge requests.
+<!-- This redirect file can be deleted after <2021-07-23>. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb
index ce5fb575b54..5274b2ee3ba 100644
--- a/lib/gitlab/graphql/docs/helper.rb
+++ b/lib/gitlab/graphql/docs/helper.rb
@@ -347,7 +347,7 @@ module Gitlab
mutations = schema.mutation&.fields&.keys&.to_set || []
graphql_object_types
- .reject { |object_type| object_type[:name]["__"] } # We ignore introspection types.
+ .reject { |object_type| object_type[:name]["__"] || object_type[:name] == 'Subscription' } # We ignore introspection and subscription types.
.map do |type|
name = type[:name]
type.merge(
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 79dd8f1ba43..6fea3589adc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1370,6 +1370,9 @@ msgstr ""
msgid "A basic template for developing Linux programs using Kotlin Native"
msgstr ""
+msgid "A blocked user cannot:"
+msgstr ""
+
msgid "A complete DevOps platform"
msgstr ""
@@ -1667,6 +1670,9 @@ msgstr ""
msgid "Acceptable for use in this project"
msgstr ""
+msgid "Access Git repositories"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
@@ -1793,6 +1799,9 @@ msgstr ""
msgid "Account and limit"
msgstr ""
+msgid "Account:"
+msgstr ""
+
msgid "Account: %{account}"
msgstr ""
@@ -4268,6 +4277,9 @@ msgstr ""
msgid "Are you sure you want to reindex?"
msgstr ""
+msgid "Are you sure you want to remove %{email}?"
+msgstr ""
+
msgid "Are you sure you want to remove %{group_name}?"
msgstr ""
@@ -5696,6 +5708,9 @@ msgstr ""
msgid "Can be manually deployed to"
msgstr ""
+msgid "Can create groups:"
+msgstr ""
+
msgid "Can't apply as the source branch was deleted."
msgstr ""
@@ -8374,6 +8389,9 @@ msgstr ""
msgid "Confirm new password"
msgstr ""
+msgid "Confirm user"
+msgstr ""
+
msgid "Confirm your account"
msgstr ""
@@ -8386,6 +8404,12 @@ msgstr ""
msgid "Confirmation required"
msgstr ""
+msgid "Confirmed at:"
+msgstr ""
+
+msgid "Confirmed:"
+msgstr ""
+
msgid "Confluence"
msgstr ""
@@ -9619,6 +9643,12 @@ msgstr ""
msgid "Current password"
msgstr ""
+msgid "Current sign-in IP:"
+msgstr ""
+
+msgid "Current sign-in at:"
+msgstr ""
+
msgid "Current vulnerabilities count"
msgstr ""
@@ -10375,6 +10405,9 @@ msgstr ""
msgid "Days to merge"
msgstr ""
+msgid "Deactivate this user"
+msgstr ""
+
msgid "Dear Administrator,"
msgstr ""
@@ -10615,6 +10648,9 @@ msgstr ""
msgid "Deleting a project places it into a read-only state until %{date}, at which point the project will be permanently deleted. Are you ABSOLUTELY sure?"
msgstr ""
+msgid "Deleting a user has the following effects:"
+msgstr ""
+
msgid "Deleting the project will delete its repository and all related resources including issues, merge requests, etc."
msgstr ""
@@ -11361,6 +11397,9 @@ msgstr ""
msgid "Disable"
msgstr ""
+msgid "Disable Two-factor Authentication"
+msgstr ""
+
msgid "Disable for this project"
msgstr ""
@@ -11879,6 +11918,9 @@ msgstr ""
msgid "Email updates (optional)"
msgstr ""
+msgid "Email:"
+msgstr ""
+
msgid "Email: %{email}"
msgstr ""
@@ -13169,6 +13211,9 @@ msgstr ""
msgid "External URL"
msgstr ""
+msgid "External User:"
+msgstr ""
+
msgid "External authentication"
msgstr ""
@@ -14218,6 +14263,9 @@ msgstr ""
msgid "Generate new token"
msgstr ""
+msgid "Generate project access tokens scoped to this project for your applications that need access to the GitLab API."
+msgstr ""
+
msgid "Generate site and private keys at"
msgstr ""
@@ -18625,6 +18673,9 @@ msgstr ""
msgid "LDAP synchronizations"
msgstr ""
+msgid "LDAP uid:"
+msgstr ""
+
msgid "LFS"
msgstr ""
@@ -18780,6 +18831,12 @@ msgstr ""
msgid "Last sign-in"
msgstr ""
+msgid "Last sign-in IP:"
+msgstr ""
+
+msgid "Last sign-in at:"
+msgstr ""
+
msgid "Last successful sync"
msgstr ""
@@ -19439,6 +19496,9 @@ msgstr ""
msgid "Locks the discussion."
msgstr ""
+msgid "Log in"
+msgstr ""
+
msgid "Login with smartcard"
msgstr ""
@@ -19922,6 +19982,9 @@ msgstr ""
msgid "Member since %{date}"
msgstr ""
+msgid "Member since:"
+msgstr ""
+
msgid "MemberInviteEmail|%{member_name} invited you to join GitLab"
msgstr ""
@@ -23390,6 +23453,9 @@ msgstr ""
msgid "Personal projects"
msgstr ""
+msgid "Personal projects limit:"
+msgstr ""
+
msgid "Phabricator Server Import"
msgstr ""
@@ -24407,6 +24473,9 @@ msgstr ""
msgid "Profile image guideline"
msgstr ""
+msgid "Profile page:"
+msgstr ""
+
msgid "ProfileSession|on"
msgstr ""
@@ -24827,7 +24896,7 @@ msgstr ""
msgid "Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group."
msgstr ""
-msgid "Project access token creation is disabled in this group. You can still use and manage existing tokens."
+msgid "Project access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "Project already deleted"
@@ -26291,6 +26360,9 @@ msgstr ""
msgid "Re-verification interval"
msgstr ""
+msgid "Reactivate this user"
+msgstr ""
+
msgid "Read documentation"
msgstr ""
@@ -26700,6 +26772,9 @@ msgstr ""
msgid "Remove runner"
msgstr ""
+msgid "Remove secondary email"
+msgstr ""
+
msgid "Remove secondary node"
msgstr ""
@@ -28169,6 +28244,9 @@ msgstr ""
msgid "Secondary"
msgstr ""
+msgid "Secondary email:"
+msgstr ""
+
msgid "Seconds"
msgstr ""
@@ -29478,6 +29556,9 @@ msgstr ""
msgid "Sign up was successful! Please confirm your email to sign in."
msgstr ""
+msgid "Sign-in count:"
+msgstr ""
+
msgid "Sign-in page"
msgstr ""
@@ -32289,6 +32370,9 @@ msgstr ""
msgid "This URL is already used for another link; duplicate URLs are not allowed"
msgstr ""
+msgid "This account has been locked"
+msgstr ""
+
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr ""
@@ -32631,6 +32715,9 @@ msgstr ""
msgid "This only applies to repository indexing operations."
msgstr ""
+msgid "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."
+msgstr ""
+
msgid "This option is only available on GitLab.com"
msgstr ""
@@ -32724,6 +32811,12 @@ msgstr ""
msgid "This user does not have a pending request"
msgstr ""
+msgid "This user has an unconfirmed email address %{email}. You may force a confirmation."
+msgstr ""
+
+msgid "This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account."
+msgstr ""
+
msgid "This user has no active %{type}."
msgstr ""
@@ -32739,6 +32832,12 @@ msgstr ""
msgid "This user has the %{access} role in the %{name} project."
msgstr ""
+msgid "This user is blocked"
+msgstr ""
+
+msgid "This user is currently an owner in these groups:"
+msgstr ""
+
msgid "This user is the author of this %{noteable}."
msgstr ""
@@ -33711,6 +33810,9 @@ msgstr ""
msgid "Two-factor Authentication Recovery codes"
msgstr ""
+msgid "Two-factor Authentication:"
+msgstr ""
+
msgid "Two-factor authentication"
msgstr ""
@@ -33972,6 +34074,9 @@ msgstr ""
msgid "Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
msgstr ""
+msgid "Unlock user"
+msgstr ""
+
msgid "Unlocked"
msgstr ""
@@ -34824,6 +34929,9 @@ msgstr ""
msgid "Username or email"
msgstr ""
+msgid "Username:"
+msgstr ""
+
msgid "Username: %{username}"
msgstr ""
@@ -36195,6 +36303,9 @@ msgstr ""
msgid "You can also upload existing files from your computer using the instructions below."
msgstr ""
+msgid "You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}"
+msgstr ""
+
msgid "You can always edit this later"
msgstr ""
@@ -36240,9 +36351,6 @@ msgstr ""
msgid "You can find more information about GitLab subscriptions in %{subscriptions_doc_link}."
msgstr ""
-msgid "You can generate an access token scoped to this project for each application to use the GitLab API."
-msgstr ""
-
msgid "You can get started by cloning the repository or start adding files to it with one of the following options."
msgstr ""
@@ -36378,6 +36486,9 @@ msgstr ""
msgid "You do not have permissions to run the import."
msgstr ""
+msgid "You don't have access to delete this user."
+msgstr ""
+
msgid "You don't have any U2F devices registered yet."
msgstr ""
@@ -36528,6 +36639,9 @@ msgstr ""
msgid "You must solve the CAPTCHA in order to submit"
msgstr ""
+msgid "You must transfer ownership or delete these groups before you can delete this user."
+msgstr ""
+
msgid "You must upload a file with the same file name when dropping onto an existing design."
msgstr ""
@@ -36771,6 +36885,9 @@ msgstr ""
msgid "Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO."
msgstr ""
+msgid "Your action succeeded."
+msgstr ""
+
msgid "Your applications (%{size})"
msgstr ""
@@ -38404,6 +38521,9 @@ msgstr ""
msgid "suggestPipeline|We’re adding a GitLab CI configuration file to add a pipeline to the project. You could create it manually, but we recommend that you start with a GitLab template that works out of the box."
msgstr ""
+msgid "system ghost user"
+msgstr ""
+
msgid "tag name"
msgstr ""
diff --git a/spec/features/action_cable_logging_spec.rb b/spec/features/action_cable_logging_spec.rb
index ce7c0e03aad..2e6ce93f7f7 100644
--- a/spec/features/action_cable_logging_spec.rb
+++ b/spec/features/action_cable_logging_spec.rb
@@ -22,11 +22,7 @@ RSpec.describe 'ActionCable logging', :js do
subscription_data = a_hash_including(
remote_ip: '127.0.0.1',
user_id: user.id,
- username: user.username,
- params: a_hash_including(
- project_path: project.full_path,
- iid: issue.iid.to_s
- )
+ username: user.username
)
expect(ActiveSupport::Notifications).to receive(:instrument).with('subscribe.action_cable', subscription_data)
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 7c564d76f70..289088a3c87 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -119,6 +119,59 @@ RSpec.describe 'File blob', :js do
end
end
+ context 'when ref switch' do
+ def switch_ref_to(ref_name)
+ first('.qa-branches-select').click
+
+ page.within '.project-refs-form' do
+ click_link ref_name
+ end
+ end
+
+ it 'displays single highlighted line number of different ref' do
+ visit_blob('files/js/application.js', anchor: 'L1')
+
+ switch_ref_to('feature')
+
+ page.within '.blob-content' do
+ expect(find_by_id('LC1')[:class]).to include("hll")
+ end
+ end
+
+ it 'displays multiple highlighted line numbers of different ref' do
+ visit_blob('files/js/application.js', anchor: 'L1-3')
+
+ switch_ref_to('feature')
+
+ page.within '.blob-content' do
+ expect(find_by_id('LC1')[:class]).to include("hll")
+ expect(find_by_id('LC2')[:class]).to include("hll")
+ expect(find_by_id('LC3')[:class]).to include("hll")
+ end
+ end
+
+ it 'displays no highlighted number of different ref' do
+ Files::UpdateService.new(
+ project,
+ project.owner,
+ commit_message: 'Update',
+ start_branch: 'feature',
+ branch_name: 'feature',
+ file_path: 'files/js/application.js',
+ file_content: 'new content'
+ ).execute
+
+ project.commit('feature').diffs.diff_files.first
+
+ visit_blob('files/js/application.js', anchor: 'L3')
+ switch_ref_to('feature')
+
+ page.within '.blob-content' do
+ expect(page).not_to have_css('.hll')
+ end
+ end
+ end
+
context 'visiting with a line number anchor' do
before do
visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1')
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
index 8083c851bb7..76d5d7308d1 100644
--- a/spec/features/projects/settings/access_tokens_spec.rb
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
visit project_settings_access_tokens_path(personal_project)
expect(page).to have_selector('#new_project_access_token')
- expect(page).to have_text('You can generate an access token scoped to this project for each application to use the GitLab API.')
+ expect(page).to have_text('Generate project access tokens scoped to this project for your applications that need access to the GitLab API.')
end
end
diff --git a/spec/frontend/actioncable_link_spec.js b/spec/frontend/actioncable_link_spec.js
new file mode 100644
index 00000000000..f3e3556f7bb
--- /dev/null
+++ b/spec/frontend/actioncable_link_spec.js
@@ -0,0 +1,110 @@
+import { print } from 'graphql';
+import gql from 'graphql-tag';
+import cable from '~/actioncable_consumer';
+import ActionCableLink from '~/actioncable_link';
+
+// Mock uuids module for determinism
+jest.mock('~/diffs/utils/uuids', () => ({
+ uuids: () => ['testuuid'],
+}));
+
+const TEST_OPERATION = {
+ query: gql`
+ query foo {
+ project {
+ id
+ }
+ }
+ `,
+ operationName: 'foo',
+ variables: [],
+};
+
+/**
+ * Create an observer that passes calls to the given spy.
+ *
+ * This helps us assert which calls were made in what order.
+ */
+const createSpyObserver = (spy) => ({
+ next: (...args) => spy('next', ...args),
+ error: (...args) => spy('error', ...args),
+ complete: (...args) => spy('complete', ...args),
+});
+
+const notify = (...notifications) => {
+ notifications.forEach((data) => cable.subscriptions.notifyAll('received', data));
+};
+
+const getSubscriptionCount = () => cable.subscriptions.subscriptions.length;
+
+describe('~/actioncable_link', () => {
+ let cableLink;
+
+ beforeEach(() => {
+ jest.spyOn(cable.subscriptions, 'create');
+
+ cableLink = new ActionCableLink();
+ });
+
+ describe('request', () => {
+ let subscription;
+ let spy;
+
+ beforeEach(() => {
+ spy = jest.fn();
+ subscription = cableLink.request(TEST_OPERATION).subscribe(createSpyObserver(spy));
+ });
+
+ afterEach(() => {
+ subscription.unsubscribe();
+ });
+
+ it('creates a subscription', () => {
+ expect(getSubscriptionCount()).toBe(1);
+ expect(cable.subscriptions.create).toHaveBeenCalledWith(
+ {
+ channel: 'GraphqlChannel',
+ nonce: 'testuuid',
+ ...TEST_OPERATION,
+ query: print(TEST_OPERATION.query),
+ },
+ { received: expect.any(Function) },
+ );
+ });
+
+ it('when "unsubscribe", unsubscribes underlying cable subscription', () => {
+ subscription.unsubscribe();
+
+ expect(getSubscriptionCount()).toBe(0);
+ });
+
+ it('when receives data, triggers observer until no ".more"', () => {
+ notify(
+ { result: 'test result', more: true },
+ { result: 'test result 2', more: true },
+ { result: 'test result 3' },
+ { result: 'test result 4' },
+ );
+
+ expect(spy.mock.calls).toEqual([
+ ['next', 'test result'],
+ ['next', 'test result 2'],
+ ['next', 'test result 3'],
+ ['complete'],
+ ]);
+ });
+
+ it('when receives errors, triggers observer', () => {
+ notify(
+ { result: 'test result', more: true },
+ { result: 'test result 2', errors: ['boom!'], more: true },
+ { result: 'test result 3' },
+ );
+
+ expect(spy.mock.calls).toEqual([
+ ['next', 'test result'],
+ ['error', ['boom!']],
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
new file mode 100644
index 00000000000..93ebbc648fe
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
@@ -0,0 +1,137 @@
+import { GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
+import { CODE_SNIPPET_SOURCES } from '~/pipeline_editor/components/code_snippet_alert/constants';
+import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
+import {
+ COMMIT_FAILURE,
+ COMMIT_SUCCESS,
+ DEFAULT_FAILURE,
+ DEFAULT_SUCCESS,
+ LOAD_FAILURE_UNKNOWN,
+} from '~/pipeline_editor/constants';
+
+describe('Pipeline Editor messages', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(PipelineEditorMessages, {
+ propsData: props,
+ });
+ };
+
+ const findCodeSnippetAlert = () => wrapper.findComponent(CodeSnippetAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ describe('success alert', () => {
+ it('shows a message for successful commit type', () => {
+ createComponent({ successType: COMMIT_SUCCESS, showSuccess: true });
+
+ expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]);
+ });
+
+ it('does not show alert when there is a successType but visibility is off', () => {
+ createComponent({ successType: COMMIT_SUCCESS, showSuccess: false });
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('shows a success alert with default copy if `showSuccess` is true and the `successType` is not valid,', () => {
+ createComponent({ successType: 'random', showSuccess: true });
+
+ expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[DEFAULT_SUCCESS]);
+ });
+
+ it('emit `hide-success` event when clicking on the dismiss button', async () => {
+ const expectedEvent = 'hide-success';
+
+ createComponent({ successType: COMMIT_SUCCESS, showSuccess: true });
+ expect(wrapper.emitted(expectedEvent)).not.toBeDefined();
+
+ await findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted(expectedEvent)).toBeDefined();
+ });
+ });
+
+ describe('failure alert', () => {
+ it.each`
+ failureType | message | expectedFailureType
+ ${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE}
+ ${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN}
+ ${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE}
+ `('shows a message for $message', ({ failureType, expectedFailureType }) => {
+ createComponent({ failureType, showFailure: true });
+
+ expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[expectedFailureType]);
+ });
+
+ it('show failure reasons when there are some', () => {
+ const failureReasons = ['There was a problem', 'ouppps'];
+ createComponent({ failureType: COMMIT_FAILURE, failureReasons, showFailure: true });
+
+ expect(wrapper.html()).toContain(failureReasons[0]);
+ expect(wrapper.html()).toContain(failureReasons[1]);
+ });
+
+ it('does not show a message for error with a disabled visibility', () => {
+ createComponent({ failureType: 'random', showFailure: false });
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('emit `hide-failure` event when clicking on the dismiss button', async () => {
+ const expectedEvent = 'hide-failure';
+
+ createComponent({ failureType: COMMIT_FAILURE, showFailure: true });
+ expect(wrapper.emitted(expectedEvent)).not.toBeDefined();
+
+ await findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted(expectedEvent)).toBeDefined();
+ });
+ });
+
+ describe('code snippet alert', () => {
+ const setCodeSnippetUrlParam = (value) => {
+ global.jsdom.reconfigure({
+ url: `${TEST_HOST}/?code_snippet_copied_from=${value}`,
+ });
+ };
+
+ it('does not show by default', () => {
+ createComponent();
+
+ expect(findCodeSnippetAlert().exists()).toBe(false);
+ });
+
+ it.each(CODE_SNIPPET_SOURCES)('shows if URL param is %s, and cleans up URL', (source) => {
+ jest.spyOn(window.history, 'replaceState');
+ setCodeSnippetUrlParam(source);
+ createComponent();
+
+ expect(findCodeSnippetAlert().exists()).toBe(true);
+ expect(window.history.replaceState).toHaveBeenCalledWith({}, document.title, `${TEST_HOST}/`);
+ });
+
+ it('does not show if URL param is invalid', () => {
+ setCodeSnippetUrlParam('foo_bar');
+ createComponent();
+
+ expect(findCodeSnippetAlert().exists()).toBe(false);
+ });
+
+ it('disappears on dismiss', async () => {
+ setCodeSnippetUrlParam('api_fuzzing');
+ createComponent();
+ const alert = findCodeSnippetAlert();
+
+ expect(alert.exists()).toBe(true);
+
+ await alert.vm.$emit('dismiss');
+
+ expect(alert.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index adb8c8836bc..b3cc1a1479e 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -2,17 +2,15 @@ import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
-import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
-import { CODE_SNIPPET_SOURCES } from '~/pipeline_editor/components/code_snippet_alert/constants';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
-import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
+import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
+import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
@@ -56,6 +54,7 @@ describe('Pipeline editor app component', () => {
CommitForm,
PipelineEditorHome,
PipelineEditorTabs,
+ PipelineEditorMessages,
EditorLite: MockEditorLite,
PipelineEditorEmptyState,
},
@@ -113,7 +112,6 @@ describe('Pipeline editor app component', () => {
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
- const findCodeSnippetAlert = () => wrapper.findComponent(CodeSnippetAlert);
beforeEach(() => {
mockBlobContentData = jest.fn();
@@ -133,48 +131,6 @@ describe('Pipeline editor app component', () => {
});
});
- describe('code snippet alert', () => {
- const setCodeSnippetUrlParam = (value) => {
- global.jsdom.reconfigure({
- url: `${TEST_HOST}/?code_snippet_copied_from=${value}`,
- });
- };
-
- it('does not show by default', () => {
- createComponent();
-
- expect(findCodeSnippetAlert().exists()).toBe(false);
- });
-
- it.each(CODE_SNIPPET_SOURCES)('shows if URL param is %s, and cleans up URL', (source) => {
- jest.spyOn(window.history, 'replaceState');
- setCodeSnippetUrlParam(source);
- createComponent();
-
- expect(findCodeSnippetAlert().exists()).toBe(true);
- expect(window.history.replaceState).toHaveBeenCalledWith({}, document.title, `${TEST_HOST}/`);
- });
-
- it('does not show if URL param is invalid', () => {
- setCodeSnippetUrlParam('foo_bar');
- createComponent();
-
- expect(findCodeSnippetAlert().exists()).toBe(false);
- });
-
- it('disappears on dismiss', async () => {
- setCodeSnippetUrlParam('api_fuzzing');
- createComponent();
- const alert = findCodeSnippetAlert();
-
- expect(alert.exists()).toBe(true);
-
- await alert.vm.$emit('dismiss');
-
- expect(alert.exists()).toBe(false);
- });
- });
-
describe('when queries are called', () => {
beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockCiYml);
@@ -235,11 +191,14 @@ describe('Pipeline editor app component', () => {
describe('because of a fetching error', () => {
it('shows a unkown error message', async () => {
+ const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
+
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(false);
- expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
+
+ expect(findAlert().text()).toBe(loadUnknownFailureText);
expect(findEditorHome().exists()).toBe(true);
});
});
@@ -273,6 +232,7 @@ describe('Pipeline editor app component', () => {
describe('when the user commits', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
+ const updateSuccessMessage = 'Your changes have been successfully committed.';
describe('and the commit mutation succeeds', () => {
beforeEach(() => {
@@ -283,7 +243,7 @@ describe('Pipeline editor app component', () => {
});
it('shows a confirmation message', () => {
- expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]);
+ expect(findAlert().text()).toBe(updateSuccessMessage);
});
it('scrolls to the top of the page to bring attention to the confirmation message', () => {
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
index f0a6fa40d67..8b579d1f8f9 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -1,41 +1,44 @@
-import ActionCable from '@rails/actioncable';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
-import { assigneesQueries } from '~/sidebar/constants';
+import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import SidebarMediator from '~/sidebar/sidebar_mediator';
-import Mock from './mock_data';
+import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
+import Mock, { issuableQueryResponse, subscriptionNullResponse } from './mock_data';
-jest.mock('@rails/actioncable', () => {
- const mockConsumer = {
- subscriptions: { create: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }) },
- };
- return {
- createConsumer: jest.fn().mockReturnValue(mockConsumer),
- };
-});
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Assignees Realtime', () => {
let wrapper;
let mediator;
+ let fakeApollo;
+
+ const issuableQueryHandler = jest.fn().mockResolvedValue(issuableQueryResponse);
+ const subscriptionInitialHandler = jest.fn().mockResolvedValue(subscriptionNullResponse);
- const createComponent = (issuableType = 'issue') => {
+ const createComponent = ({
+ issuableType = 'issue',
+ issuableId = 1,
+ subscriptionHandler = subscriptionInitialHandler,
+ } = {}) => {
+ fakeApollo = createMockApollo([
+ [getIssueParticipantsQuery, issuableQueryHandler],
+ [issuableAssigneesSubscription, subscriptionHandler],
+ ]);
wrapper = shallowMount(AssigneesRealtime, {
propsData: {
- issuableIid: '1',
- mediator,
- projectPath: 'path/to/project',
issuableType,
- },
- mocks: {
- $apollo: {
- query: assigneesQueries[issuableType].query,
- queries: {
- workspace: {
- refetch: jest.fn(),
- },
- },
+ issuableId,
+ queryVariables: {
+ issuableIid: '1',
+ projectPath: 'path/to/project',
},
+ mediator,
},
+ apolloProvider: fakeApollo,
+ localVue,
});
};
@@ -45,59 +48,24 @@ describe('Assignees Realtime', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
+ fakeApollo = null;
SidebarMediator.singleton = null;
});
- describe('when handleFetchResult is called from smart query', () => {
- it('sets assignees to the store', () => {
- const data = {
- workspace: {
- issuable: {
- assignees: {
- nodes: [{ id: 'gid://gitlab/Environments/123', avatarUrl: 'url' }],
- },
- },
- },
- };
- const expected = [{ id: 123, avatar_url: 'url', avatarUrl: 'url' }];
- createComponent();
+ it('calls the query with correct variables', () => {
+ createComponent();
- wrapper.vm.handleFetchResult({ data });
-
- expect(mediator.store.assignees).toEqual(expected);
+ expect(issuableQueryHandler).toHaveBeenCalledWith({
+ issuableIid: '1',
+ projectPath: 'path/to/project',
});
});
- describe('when mounted', () => {
- it('calls create subscription', () => {
- const cable = ActionCable.createConsumer();
-
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(cable.subscriptions.create).toHaveBeenCalledTimes(1);
- expect(cable.subscriptions.create).toHaveBeenCalledWith(
- {
- channel: 'IssuesChannel',
- iid: wrapper.props('issuableIid'),
- project_path: wrapper.props('projectPath'),
- },
- { received: wrapper.vm.received },
- );
- });
- });
- });
-
- describe('when subscription is recieved', () => {
- it('refetches the GraphQL project query', () => {
- createComponent();
-
- wrapper.vm.received({ event: 'updated' });
+ it('calls the subscription with correct variable for issue', () => {
+ createComponent();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.$apollo.queries.workspace.refetch).toHaveBeenCalledTimes(1);
- });
+ expect(subscriptionInitialHandler).toHaveBeenCalledWith({
+ issuableId: 'gid://gitlab/Issue/1',
});
});
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index 824f6d49c65..543bc1c128a 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -487,6 +487,9 @@ describe('Sidebar assignees widget', () => {
it('when realtime feature flag is enabled', async () => {
createComponent({
+ props: {
+ issuableId: 1,
+ },
provide: {
glFeatures: {
realTimeIssueSidebar: true,
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 2a4858a6320..3bb41548941 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -401,4 +401,10 @@ export const updateIssueAssigneesMutationResponse = {
},
};
+export const subscriptionNullResponse = {
+ data: {
+ issuableAssigneesUpdated: null,
+ },
+};
+
export default mockData;
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js
index e737b57e33d..dc121dcb897 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/sidebar_assignees_spec.js
@@ -17,6 +17,7 @@ describe('sidebar assignees', () => {
wrapper = shallowMount(SidebarAssignees, {
propsData: {
issuableIid: '1',
+ issuableId: 1,
mediator,
field: '',
projectPath: 'projectPath',
diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb
index 42ec98c3491..37a282657d9 100644
--- a/spec/models/wiki_page/meta_spec.rb
+++ b/spec/models/wiki_page/meta_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe WikiPage::Meta do
subject { described_class.find(meta.id) }
let_it_be(:meta) do
- described_class.create(title: generate(:wiki_page_title), project: project)
+ described_class.create!(title: generate(:wiki_page_title), project: project)
end
context 'there are no slugs' do
@@ -183,7 +183,7 @@ RSpec.describe WikiPage::Meta do
# an old slug that = canonical_slug
different_slug = generate(:sluggified_title)
create(:wiki_page_meta, project: project, canonical_slug: different_slug)
- .slugs.create(slug: wiki_page.slug)
+ .slugs.create!(slug: wiki_page.slug)
end
shared_examples 'metadata examples' do