Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5ebc4d92cd
commit
97f0ae7454
27 changed files with 449 additions and 114 deletions
|
@ -188,6 +188,15 @@ const Api = {
|
|||
return axios.get(url, { params });
|
||||
},
|
||||
|
||||
createProjectMergeRequest(projectPath, options) {
|
||||
const url = Api.buildUrl(Api.projectMergeRequestsPath).replace(
|
||||
':id',
|
||||
encodeURIComponent(projectPath),
|
||||
);
|
||||
|
||||
return axios.post(url, options);
|
||||
},
|
||||
|
||||
// Return Merge Request for project
|
||||
projectMergeRequest(projectPath, mergeRequestId, params = {}) {
|
||||
const url = Api.buildUrl(Api.projectMergeRequestPath)
|
||||
|
|
|
@ -3,10 +3,8 @@ import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
|
|||
import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
|
||||
import BlobHeader from '~/blob/components/blob_header.vue';
|
||||
import BlobContent from '~/blob/components/blob_content.vue';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
|
||||
|
||||
import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql';
|
||||
import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
|
||||
|
||||
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
|
||||
|
@ -16,25 +14,9 @@ export default {
|
|||
BlobEmbeddable,
|
||||
BlobHeader,
|
||||
BlobContent,
|
||||
GlLoadingIcon,
|
||||
CloneDropdownButton,
|
||||
},
|
||||
apollo: {
|
||||
blob: {
|
||||
query: GetSnippetBlobQuery,
|
||||
variables() {
|
||||
return {
|
||||
ids: this.snippet.id,
|
||||
};
|
||||
},
|
||||
update: data => data.snippets.edges[0].node.blob,
|
||||
result(res) {
|
||||
const viewer = res.data.snippets.edges[0].node.blob.richViewer
|
||||
? RICH_BLOB_VIEWER
|
||||
: SIMPLE_BLOB_VIEWER;
|
||||
this.switchViewer(viewer, true);
|
||||
},
|
||||
},
|
||||
blobContent: {
|
||||
query: GetBlobContent,
|
||||
variables() {
|
||||
|
@ -55,18 +37,18 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
blob: {},
|
||||
blob: this.snippet.blob,
|
||||
blobContent: '',
|
||||
activeViewerType: window.location.hash ? SIMPLE_BLOB_VIEWER : '',
|
||||
activeViewerType:
|
||||
this.snippet.blob?.richViewer && !window.location.hash
|
||||
? RICH_BLOB_VIEWER
|
||||
: SIMPLE_BLOB_VIEWER,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
embeddable() {
|
||||
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
|
||||
},
|
||||
isBlobLoading() {
|
||||
return this.$apollo.queries.blob.loading;
|
||||
},
|
||||
isContentLoading() {
|
||||
return this.$apollo.queries.blobContent.loading;
|
||||
},
|
||||
|
@ -79,8 +61,8 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
switchViewer(newViewer, respectHash = false) {
|
||||
this.activeViewerType = respectHash && window.location.hash ? SIMPLE_BLOB_VIEWER : newViewer;
|
||||
switchViewer(newViewer) {
|
||||
this.activeViewerType = newViewer;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -88,13 +70,7 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" />
|
||||
<gl-loading-icon
|
||||
v-if="isBlobLoading"
|
||||
:label="__('Loading blob')"
|
||||
size="lg"
|
||||
class="prepend-top-20 append-bottom-20"
|
||||
/>
|
||||
<article v-else class="file-holder snippet-file-content">
|
||||
<article class="file-holder snippet-file-content">
|
||||
<blob-header :blob="blob" :active-viewer-type="viewer.type" @viewer-changed="switchViewer">
|
||||
<template #actions>
|
||||
<clone-dropdown-button
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#import '~/graphql_shared/fragments/blobviewer.fragment.graphql'
|
||||
|
||||
fragment SnippetBase on Snippet {
|
||||
id
|
||||
title
|
||||
|
@ -9,6 +11,19 @@ fragment SnippetBase on Snippet {
|
|||
webUrl
|
||||
httpUrlToRepo
|
||||
sshUrlToRepo
|
||||
blob {
|
||||
binary
|
||||
name
|
||||
path
|
||||
rawPath
|
||||
size
|
||||
simpleViewer {
|
||||
...BlobViewer
|
||||
}
|
||||
richViewer {
|
||||
...BlobViewer
|
||||
}
|
||||
}
|
||||
userPermissions {
|
||||
adminSnippet
|
||||
updateSnippet
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
#import '~/graphql_shared/fragments/blobviewer.fragment.graphql'
|
||||
|
||||
query SnippetBlobFull($ids: [ID!]) {
|
||||
snippets(ids: $ids) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
blob {
|
||||
binary
|
||||
name
|
||||
path
|
||||
rawPath
|
||||
size
|
||||
simpleViewer {
|
||||
...BlobViewer
|
||||
}
|
||||
richViewer {
|
||||
...BlobViewer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
app/assets/javascripts/static_site_editor/constants.js
Normal file
12
app/assets/javascripts/static_site_editor/constants.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const BRANCH_SUFFIX_COUNT = 8;
|
||||
export const DEFAULT_TARGET_BRANCH = 'master';
|
||||
|
||||
export const SUBMIT_CHANGES_BRANCH_ERROR = s__('StaticSiteEditor|Branch could not be created.');
|
||||
export const SUBMIT_CHANGES_COMMIT_ERROR = s__(
|
||||
'StaticSiteEditor|Could not commit the content changes.',
|
||||
);
|
||||
export const SUBMIT_CHANGES_MERGE_REQUEST_ERROR = s__(
|
||||
'StaticSiteEditor|Could not create merge request.',
|
||||
);
|
|
@ -0,0 +1,8 @@
|
|||
import { BRANCH_SUFFIX_COUNT, DEFAULT_TARGET_BRANCH } from '../constants';
|
||||
|
||||
const generateBranchSuffix = () => `${Date.now()}`.substr(BRANCH_SUFFIX_COUNT);
|
||||
|
||||
const generateBranchName = (username, targetBranch = DEFAULT_TARGET_BRANCH) =>
|
||||
`${username}-${targetBranch}-patch-${generateBranchSuffix()}`;
|
||||
|
||||
export default generateBranchName;
|
|
@ -1,4 +1,76 @@
|
|||
// TODO implement
|
||||
const submitContentChanges = () => new Promise(resolve => setTimeout(resolve, 1000));
|
||||
import Api from '~/api';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
|
||||
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
|
||||
|
||||
import {
|
||||
DEFAULT_TARGET_BRANCH,
|
||||
SUBMIT_CHANGES_BRANCH_ERROR,
|
||||
SUBMIT_CHANGES_COMMIT_ERROR,
|
||||
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
|
||||
} from '../constants';
|
||||
|
||||
const createBranch = (projectId, branch) =>
|
||||
Api.createBranch(projectId, {
|
||||
ref: DEFAULT_TARGET_BRANCH,
|
||||
branch,
|
||||
}).catch(() => {
|
||||
throw new Error(SUBMIT_CHANGES_BRANCH_ERROR);
|
||||
});
|
||||
|
||||
const commitContent = (projectId, message, branch, sourcePath, content) =>
|
||||
Api.commitMultiple(
|
||||
projectId,
|
||||
convertObjectPropsToSnakeCase({
|
||||
branch,
|
||||
commitMessage: message,
|
||||
actions: [
|
||||
convertObjectPropsToSnakeCase({
|
||||
action: 'update',
|
||||
filePath: sourcePath,
|
||||
content,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
).catch(() => {
|
||||
throw new Error(SUBMIT_CHANGES_COMMIT_ERROR);
|
||||
});
|
||||
|
||||
const createMergeRequest = (projectId, title, sourceBranch, targetBranch = DEFAULT_TARGET_BRANCH) =>
|
||||
Api.createProjectMergeRequest(
|
||||
projectId,
|
||||
convertObjectPropsToSnakeCase({
|
||||
title,
|
||||
sourceBranch,
|
||||
targetBranch,
|
||||
}),
|
||||
).catch(() => {
|
||||
throw new Error(SUBMIT_CHANGES_MERGE_REQUEST_ERROR);
|
||||
});
|
||||
|
||||
const submitContentChanges = ({ username, projectId, sourcePath, content }) => {
|
||||
const branch = generateBranchName(username);
|
||||
const mergeRequestTitle = sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
|
||||
sourcePath,
|
||||
});
|
||||
const meta = {};
|
||||
|
||||
return createBranch(projectId, branch)
|
||||
.then(() => {
|
||||
Object.assign(meta, { branch: { label: branch } });
|
||||
|
||||
return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content);
|
||||
})
|
||||
.then(({ data: { short_id: label, web_url: url } }) => {
|
||||
Object.assign(meta, { commit: { label, url } });
|
||||
|
||||
return createMergeRequest(projectId, mergeRequestTitle, branch);
|
||||
})
|
||||
.then(({ data: { iid: label, web_url: url } }) => {
|
||||
Object.assign(meta, { mergeRequest: { label, url } });
|
||||
|
||||
return meta;
|
||||
});
|
||||
};
|
||||
|
||||
export default submitContentChanges;
|
||||
|
|
|
@ -10,6 +10,8 @@ const createState = (initialState = {}) => ({
|
|||
content: '',
|
||||
title: '',
|
||||
|
||||
savedContentMeta: null,
|
||||
|
||||
...initialState,
|
||||
});
|
||||
|
||||
|
|
|
@ -20,8 +20,17 @@ module Resolvers
|
|||
|
||||
args[:iids] ||= [args[:iid]].compact
|
||||
|
||||
args[:iids].map { |iid| batch_load(iid) }
|
||||
.select(&:itself) # .compact doesn't work on BatchLoader
|
||||
if args[:iids].any?
|
||||
batch_load_merge_requests(args[:iids])
|
||||
else
|
||||
args[:project_id] = project.id
|
||||
|
||||
MergeRequestsFinder.new(context[:current_user], args).execute
|
||||
end
|
||||
end
|
||||
|
||||
def batch_load_merge_requests(iids)
|
||||
iids.map { |iid| batch_load(iid) }.select(&:itself) # .compact doesn't work on BatchLoader
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Save changes in Static Site Editor using REST GitLab API
|
||||
merge_request: 29286
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Optimize protected branches usage data
|
||||
merge_request: 29148
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix pagination in Merge Request GraphQL api
|
||||
merge_request: 28667
|
||||
author: briankabiro
|
||||
type: fixed
|
|
@ -26,6 +26,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
scope '-' do
|
||||
get 'archive/*id', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+?/ }, to: 'repositories#archive', as: 'archive'
|
||||
|
||||
scope controller: :static_site_editor do
|
||||
get '/sse/*id', action: :show, as: :show_sse
|
||||
end
|
||||
|
||||
resources :artifacts, only: [:index, :destroy]
|
||||
|
||||
resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do
|
||||
|
|
|
@ -67,10 +67,6 @@ scope format: false do
|
|||
end
|
||||
end
|
||||
|
||||
scope controller: :static_site_editor do
|
||||
get '/sse/*id', action: :show, as: :show_sse
|
||||
end
|
||||
|
||||
get '/tree/*id', to: 'tree#show', as: :tree
|
||||
get '/raw/*id', to: 'raw#show', as: :raw
|
||||
get '/blame/*id', to: 'blame#show', as: :blame
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnCreatorIdCreatedAtIdToProjectsTable < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :projects, [:creator_id, :created_at, :id]
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :projects, [:creator_id, :created_at, :id]
|
||||
end
|
||||
end
|
|
@ -9884,6 +9884,8 @@ CREATE INDEX index_projects_on_created_at_and_id ON public.projects USING btree
|
|||
|
||||
CREATE INDEX index_projects_on_creator_id_and_created_at ON public.projects USING btree (creator_id, created_at);
|
||||
|
||||
CREATE INDEX index_projects_on_creator_id_and_created_at_and_id ON public.projects USING btree (creator_id, created_at, id);
|
||||
|
||||
CREATE INDEX index_projects_on_description_trigram ON public.projects USING gin (description public.gin_trgm_ops);
|
||||
|
||||
CREATE INDEX index_projects_on_id_and_archived_and_pending_delete ON public.projects USING btree (id) WHERE ((archived = false) AND (pending_delete = false));
|
||||
|
@ -13092,5 +13094,6 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200407094005
|
||||
20200407094923
|
||||
20200408110856
|
||||
20200408175424
|
||||
\.
|
||||
|
||||
|
|
|
@ -468,12 +468,16 @@ config.
|
|||
Manual failover is possible by updating `praefect['virtual_storages']` and
|
||||
nominating a new primary node.
|
||||
|
||||
NOTE: **Note:**: Automatic failover is not yet supported for setups with
|
||||
multiple Praefect nodes. There is currently no coordination between Praefect
|
||||
nodes, which could result in two Praefect instances thinking two different
|
||||
Gitaly nodes are the primary. Follow issue
|
||||
[#2547](https://gitlab.com/gitlab-org/gitaly/-/issues/2547) for
|
||||
updates.
|
||||
1. By default, Praefect will nominate a primary Gitaly node for each
|
||||
shard and store the state of the primary in local memory. This state
|
||||
does not persist across restarts and will cause a split brain
|
||||
if multiple Praefect nodes are used for redundancy.
|
||||
|
||||
To avoid this limitation, enable the SQL election strategy:
|
||||
|
||||
```ruby
|
||||
praefect['failover_election_strategy'] = 'sql'
|
||||
```
|
||||
|
||||
1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
|
||||
Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
|
||||
|
@ -677,8 +681,18 @@ current primary node is found to be unhealthy.
|
|||
checks fail for the current primary backend Gitaly node, and new primary will
|
||||
be elected. **Do not use with multiple Praefect nodes!** Using with multiple
|
||||
Praefect nodes is likely to result in a split brain.
|
||||
- **PostgreSQL:** Coming soon. See isse
|
||||
[#2547](https://gitlab.com/gitlab-org/gitaly/-/issues/2547) for updates.
|
||||
- **PostgreSQL:** Enabled by setting
|
||||
`praefect['failover_election_strategy'] = sql`. This configuration
|
||||
option will allow multiple Praefect nodes to coordinate via the
|
||||
PostgreSQL database to elect a primary Gitaly node. This configuration
|
||||
will cause Praefect nodes to elect a new primary, monitor its health,
|
||||
and elect a new primary if the current one has not been reachable in
|
||||
10 seconds by a majority of the Praefect nodes.
|
||||
|
||||
NOTE: **Note:**: Praefect does not yet account for replication lag on
|
||||
the secondaries during the election process, so data loss can occur
|
||||
during a failover. Follow issue
|
||||
[#2642](https://gitlab.com/gitlab-org/gitaly/-/issues/2642) for updates.
|
||||
|
||||
It is likely that we will implement support for Consul, and a cloud native
|
||||
strategy in the future.
|
||||
|
|
|
@ -5,7 +5,7 @@ module API
|
|||
class ProjectImportStatus < ProjectIdentity
|
||||
expose :import_status
|
||||
expose :correlation_id do |project, _options|
|
||||
project.import_state.correlation_id
|
||||
project.import_state&.correlation_id
|
||||
end
|
||||
|
||||
# TODO: Use `expose_nil` once we upgrade the grape-entity gem
|
||||
|
|
|
@ -98,7 +98,6 @@ module Gitlab
|
|||
preview
|
||||
raw
|
||||
refs
|
||||
sse
|
||||
tree
|
||||
update
|
||||
wikis
|
||||
|
|
|
@ -12189,9 +12189,6 @@ msgstr ""
|
|||
msgid "Loading"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading blob"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading contribution stats for group members"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19368,6 +19365,15 @@ msgstr ""
|
|||
msgid "Static Application Security Testing (SAST)"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Branch could not be created."
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Could not commit the content changes."
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Could not create merge request."
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Return to site"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19377,6 +19383,9 @@ msgstr ""
|
|||
msgid "StaticSiteEditor|Summary of changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Update %{sourcePath} file"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|View merge request"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -651,7 +651,7 @@ describe('Api', () => {
|
|||
|
||||
describe('when an error occurs while getting a raw file', () => {
|
||||
it('rejects the Promise', () => {
|
||||
mock.onDelete(expectedUrl).replyOnce(500);
|
||||
mock.onPost(expectedUrl).replyOnce(500);
|
||||
|
||||
return Api.getRawFile(dummyProjectPath, dummyFilePath).catch(() => {
|
||||
expect(mock.history.get).toHaveLength(1);
|
||||
|
@ -659,4 +659,36 @@ describe('Api', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProjectMergeRequest', () => {
|
||||
const dummyProjectPath = 'gitlab-org/gitlab';
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
|
||||
dummyProjectPath,
|
||||
)}/merge_requests`;
|
||||
const options = {
|
||||
source_branch: 'feature',
|
||||
target_branch: 'master',
|
||||
title: 'Add feature',
|
||||
};
|
||||
|
||||
describe('when the merge request is successfully created', () => {
|
||||
it('resolves the Promise', () => {
|
||||
mock.onPost(expectedUrl, options).replyOnce(201);
|
||||
|
||||
return Api.createProjectMergeRequest(dummyProjectPath, options).then(() => {
|
||||
expect(mock.history.post).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an error occurs while getting a raw file', () => {
|
||||
it('rejects the Promise', () => {
|
||||
mock.onPost(expectedUrl).replyOnce(500);
|
||||
|
||||
return Api.createProjectMergeRequest(dummyProjectPath).catch(() => {
|
||||
expect(mock.history.post).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
|
||||
import BlobHeader from '~/blob/components/blob_header.vue';
|
||||
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
|
||||
|
@ -19,23 +18,15 @@ describe('Blob Embeddable', () => {
|
|||
id: 'gid://foo.bar/snippet',
|
||||
webUrl: 'https://foo.bar',
|
||||
visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
|
||||
blob: BlobMock,
|
||||
};
|
||||
const dataMock = {
|
||||
blob: BlobMock,
|
||||
activeViewerType: SimpleViewerMock.type,
|
||||
};
|
||||
|
||||
function createComponent(
|
||||
props = {},
|
||||
data = dataMock,
|
||||
blobLoading = false,
|
||||
contentLoading = false,
|
||||
) {
|
||||
function createComponent(props = {}, data = dataMock, contentLoading = false) {
|
||||
const $apollo = {
|
||||
queries: {
|
||||
blob: {
|
||||
loading: blobLoading,
|
||||
},
|
||||
blobContent: {
|
||||
loading: contentLoading,
|
||||
},
|
||||
|
@ -87,12 +78,6 @@ describe('Blob Embeddable', () => {
|
|||
expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows loading icon while blob data is in flight', () => {
|
||||
createComponent({}, dataMock, true);
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(wrapper.find('.snippet-file-content').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('sets simple viewer correctly', () => {
|
||||
createComponent();
|
||||
expect(wrapper.find(SimpleViewer).exists()).toBe(true);
|
||||
|
@ -133,14 +118,14 @@ describe('Blob Embeddable', () => {
|
|||
});
|
||||
|
||||
it('renders simple viewer by default if URL contains hash', () => {
|
||||
createComponent();
|
||||
createComponent({}, {});
|
||||
|
||||
expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
|
||||
expect(wrapper.find(SimpleViewer).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('switchViewer()', () => {
|
||||
it('by default switches to the passed viewer', () => {
|
||||
it('switches to the passed viewer', () => {
|
||||
createComponent();
|
||||
|
||||
wrapper.vm.switchViewer(RichViewerMock.type);
|
||||
|
@ -157,22 +142,6 @@ describe('Blob Embeddable', () => {
|
|||
expect(wrapper.find(SimpleViewer).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('respects hash over richViewer in the blob when corresponding parameter is passed', () => {
|
||||
createComponent(
|
||||
{},
|
||||
{
|
||||
blob: BlobMock,
|
||||
},
|
||||
);
|
||||
expect(wrapper.vm.blob.richViewer).toEqual(expect.any(Object));
|
||||
|
||||
wrapper.vm.switchViewer(RichViewerMock.type, true);
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
|
||||
expect(wrapper.find(SimpleViewer).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,3 +34,11 @@ export const savedContentMeta = {
|
|||
};
|
||||
|
||||
export const submitChangesError = 'Could not save changes';
|
||||
export const commitMultipleResponse = {
|
||||
short_id: 'ed899a2f4b5',
|
||||
web_url: '/commit/ed899a2f4b5',
|
||||
};
|
||||
export const createMergeRequestResponse = {
|
||||
iid: '123',
|
||||
web_url: '/merge_requests/123',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { DEFAULT_TARGET_BRANCH, BRANCH_SUFFIX_COUNT } from '~/static_site_editor/constants';
|
||||
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
|
||||
|
||||
import { username } from '../mock_data';
|
||||
|
||||
describe('generateBranchName', () => {
|
||||
const timestamp = 12345678901234;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(timestamp);
|
||||
});
|
||||
|
||||
it('generates a name that includes the username and target branch', () => {
|
||||
expect(generateBranchName(username)).toMatch(`${username}-${DEFAULT_TARGET_BRANCH}`);
|
||||
});
|
||||
|
||||
it(`adds the first ${BRANCH_SUFFIX_COUNT} numbers of the current timestamp`, () => {
|
||||
expect(generateBranchName(username)).toMatch(
|
||||
timestamp.toString().substring(BRANCH_SUFFIX_COUNT),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,131 @@
|
|||
import Api from '~/api';
|
||||
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
|
||||
|
||||
import {
|
||||
DEFAULT_TARGET_BRANCH,
|
||||
SUBMIT_CHANGES_BRANCH_ERROR,
|
||||
SUBMIT_CHANGES_COMMIT_ERROR,
|
||||
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
|
||||
} from '~/static_site_editor/constants';
|
||||
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
|
||||
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
|
||||
|
||||
import {
|
||||
username,
|
||||
projectId,
|
||||
commitMultipleResponse,
|
||||
createMergeRequestResponse,
|
||||
sourcePath,
|
||||
sourceContent as content,
|
||||
} from '../mock_data';
|
||||
|
||||
jest.mock('~/static_site_editor/services/generate_branch_name');
|
||||
|
||||
describe('submitContentChanges', () => {
|
||||
const mergeRequestTitle = `Update ${sourcePath} file`;
|
||||
const branch = 'branch-name';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Api, 'createBranch').mockResolvedValue();
|
||||
jest.spyOn(Api, 'commitMultiple').mockResolvedValue({ data: commitMultipleResponse });
|
||||
jest
|
||||
.spyOn(Api, 'createProjectMergeRequest')
|
||||
.mockResolvedValue({ data: createMergeRequestResponse });
|
||||
|
||||
generateBranchName.mockReturnValue(branch);
|
||||
});
|
||||
|
||||
it('creates a branch named after the username and target branch', () => {
|
||||
return submitContentChanges({ username, projectId }).then(() => {
|
||||
expect(Api.createBranch).toHaveBeenCalledWith(projectId, {
|
||||
ref: DEFAULT_TARGET_BRANCH,
|
||||
branch,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('notifies error when branch could not be created', () => {
|
||||
Api.createBranch.mockRejectedValueOnce();
|
||||
|
||||
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
|
||||
SUBMIT_CHANGES_BRANCH_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
it('commits the content changes to the branch when creating branch succeeds', () => {
|
||||
return submitContentChanges({ username, projectId, sourcePath, content }).then(() => {
|
||||
expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, {
|
||||
branch,
|
||||
commit_message: mergeRequestTitle,
|
||||
actions: [
|
||||
{
|
||||
action: 'update',
|
||||
file_path: sourcePath,
|
||||
content,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('notifies error when content could not be committed', () => {
|
||||
Api.commitMultiple.mockRejectedValueOnce();
|
||||
|
||||
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
|
||||
SUBMIT_CHANGES_COMMIT_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a merge request when commiting changes succeeds', () => {
|
||||
return submitContentChanges({ username, projectId, sourcePath, content }).then(() => {
|
||||
expect(Api.createProjectMergeRequest).toHaveBeenCalledWith(
|
||||
projectId,
|
||||
convertObjectPropsToSnakeCase({
|
||||
title: mergeRequestTitle,
|
||||
targetBranch: DEFAULT_TARGET_BRANCH,
|
||||
sourceBranch: branch,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('notifies error when merge request could not be created', () => {
|
||||
Api.createProjectMergeRequest.mockRejectedValueOnce();
|
||||
|
||||
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
|
||||
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when changes are submitted successfully', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
return submitContentChanges({ username, projectId, sourcePath, content }).then(_result => {
|
||||
result = _result;
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the branch name', () => {
|
||||
expect(result).toMatchObject({ branch: { label: branch } });
|
||||
});
|
||||
|
||||
it('returns commit short id and web url', () => {
|
||||
expect(result).toMatchObject({
|
||||
commit: {
|
||||
label: commitMultipleResponse.short_id,
|
||||
url: commitMultipleResponse.web_url,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns merge request iid and web url', () => {
|
||||
expect(result).toMatchObject({
|
||||
mergeRequest: {
|
||||
label: createMergeRequestResponse.iid,
|
||||
url: createMergeRequestResponse.web_url,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
describe 'getting notes for a merge request' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:noteable) { create(:merge_request) }
|
||||
let_it_be(:noteable) { create(:merge_request) }
|
||||
|
||||
def noteable_query(noteable_fields)
|
||||
<<~QRY
|
||||
|
|
|
@ -93,4 +93,41 @@ describe 'getting merge request information nested in a project' do
|
|||
expect(merge_request_graphql_data['pipelines']['edges'].size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when limiting the number of results' do
|
||||
let(:merge_requests_graphql_data) { graphql_data['project']['mergeRequests']['edges'] }
|
||||
|
||||
let!(:merge_requests) do
|
||||
[
|
||||
create(:merge_request, source_project: project, source_branch: 'branch-1'),
|
||||
create(:merge_request, source_project: project, source_branch: 'branch-2'),
|
||||
create(:merge_request, source_project: project, source_branch: 'branch-3')
|
||||
]
|
||||
end
|
||||
|
||||
let(:fields) do
|
||||
<<~QUERY
|
||||
edges {
|
||||
node {
|
||||
iid,
|
||||
title
|
||||
}
|
||||
}
|
||||
QUERY
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
"mergeRequests(first: 2) { #{fields} }"
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the correct number of results' do
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
||||
expect(merge_requests_graphql_data.size).to eq 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue