Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6819cb95c9
commit
0ccabeb3f6
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
b19cafa222fd7a999167d3f9f8562c2d74b62bfd
|
||||
5658d720f02d2c84b51feaae484ea68aeeb59773
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
issuableId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
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;
|
||||
},
|
||||
result(data) {
|
||||
if (this.mediator) {
|
||||
this.handleFetchResult(data);
|
||||
}
|
||||
update(data) {
|
||||
return data.workspace?.issuable;
|
||||
},
|
||||
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) => ({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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],
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize strings in /users/show.html.haml
|
||||
merge_request: 58126
|
||||
author: nuwe1
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Revise project access tokens UI text
|
||||
merge_request: 59878
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Rails/SaveBang Rubocop offenses for wiki_page models
|
||||
merge_request: 57899
|
||||
author: Huzaifa Iftikhar @huzaifaiftikhar
|
||||
type: fixed
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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!']],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 createComponent = (issuableType = 'issue') => {
|
||||
const issuableQueryHandler = jest.fn().mockResolvedValue(issuableQueryResponse);
|
||||
const subscriptionInitialHandler = jest.fn().mockResolvedValue(subscriptionNullResponse);
|
||||
|
||||
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();
|
||||
it('calls the subscription with correct variable for issue', () => {
|
||||
createComponent();
|
||||
|
||||
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' });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.$apollo.queries.workspace.refetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(subscriptionInitialHandler).toHaveBeenCalledWith({
|
||||
issuableId: 'gid://gitlab/Issue/1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -487,6 +487,9 @@ describe('Sidebar assignees widget', () => {
|
|||
|
||||
it('when realtime feature flag is enabled', async () => {
|
||||
createComponent({
|
||||
props: {
|
||||
issuableId: 1,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
realTimeIssueSidebar: true,
|
||||
|
|
|
@ -401,4 +401,10 @@ export const updateIssueAssigneesMutationResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const subscriptionNullResponse = {
|
||||
data: {
|
||||
issuableAssigneesUpdated: null,
|
||||
},
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
|
|
|
@ -17,6 +17,7 @@ describe('sidebar assignees', () => {
|
|||
wrapper = shallowMount(SidebarAssignees, {
|
||||
propsData: {
|
||||
issuableIid: '1',
|
||||
issuableId: 1,
|
||||
mediator,
|
||||
field: '',
|
||||
projectPath: 'projectPath',
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue