Commit 71752eae authored by Jacques Erasmus's avatar Jacques Erasmus Committed by Peter Hegman

Display fork suggestion when editing blob

Displays a fork suggestion when user should fork before editing
parent e9735782
......@@ -8,10 +8,12 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import BlobButtonGroup from './blob_button_group.vue';
import BlobEdit from './blob_edit.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer, viewerProps } from './blob_viewers';
export default {
......@@ -21,6 +23,7 @@ export default {
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
ForkSuggestion,
},
mixins: [getRefMixin],
inject: {
......@@ -65,6 +68,7 @@ export default {
},
data() {
return {
forkTarget: null,
legacyRichViewer: null,
legacySimpleViewer: null,
isBinary: false,
......@@ -74,6 +78,8 @@ export default {
userPermissions: {
pushCode: false,
downloadCode: false,
createMergeRequestIn: false,
forkProject: false,
},
pathLocks: {
nodes: [],
......@@ -92,12 +98,14 @@ export default {
path: '',
editBlobPath: '',
ideEditPath: '',
forkAndEditPath: '',
ideForkAndEditPath: '',
storedExternally: false,
canModifyBlob: false,
rawPath: '',
externalStorageUrl: '',
replacePath: '',
deletePath: '',
forkPath: '',
simpleViewer: {},
richViewer: null,
webPath: '',
......@@ -149,6 +157,17 @@ export default {
isLocked() {
return this.project.pathLocks.nodes.some((node) => node.path === this.path);
},
showForkSuggestion() {
const { createMergeRequestIn, forkProject } = this.project.userPermissions;
const { canModifyBlob } = this.blobInfo;
return this.isLoggedIn && !canModifyBlob && createMergeRequestIn && forkProject;
},
forkPath() {
return this.forkTarget === 'ide'
? this.blobInfo.ideForkAndEditPath
: this.blobInfo.forkAndEditPath;
},
},
methods: {
loadLegacyViewer(type) {
......@@ -187,6 +206,18 @@ export default {
this.loadLegacyViewer(this.activeViewerType);
}
},
editBlob(target) {
if (this.showForkSuggestion) {
this.setForkTarget(target);
return;
}
const { ideEditPath, editBlobPath } = this.blobInfo;
redirectTo(target === 'ide' ? ideEditPath : editBlobPath);
},
setForkTarget(target) {
this.forkTarget = target;
},
},
};
</script>
......@@ -208,6 +239,8 @@ export default {
:show-edit-button="!isBinaryFileType"
:edit-path="blobInfo.editBlobPath"
:web-ide-path="blobInfo.ideEditPath"
:needs-to-fork="showForkSuggestion"
@edit="editBlob"
/>
<blob-button-group
v-if="isLoggedIn"
......@@ -223,6 +256,11 @@ export default {
/>
</template>
</blob-header>
<fork-suggestion
v-if="forkTarget && showForkSuggestion"
:fork-path="forkPath"
@cancel="setForkTarget(null)"
/>
<blob-content
v-if="!blobViewer"
:rich-viewer="legacyRichViewer"
......
......@@ -27,6 +27,16 @@ export default {
type: String,
required: true,
},
needsToFork: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
onEdit(target) {
this.$emit('edit', target);
},
},
};
</script>
......@@ -38,7 +48,9 @@ export default {
class="gl-mr-3"
:edit-url="editPath"
:web-ide-url="webIdePath"
:needs-to-fork="needsToFork"
:is-blob="true"
@edit="onEdit"
/>
<div v-else>
<gl-button
......@@ -46,8 +58,8 @@ export default {
class="gl-mr-2"
category="primary"
variant="confirm"
:href="editPath"
data-testid="edit"
@click="onEdit('simple')"
>
{{ $options.i18n.edit }}
</gl-button>
......@@ -56,8 +68,8 @@ export default {
class="gl-mr-3"
category="primary"
variant="confirm"
:href="webIdePath"
data-testid="web-ide"
@click="onEdit('ide')"
>
{{ $options.i18n.webIde }}
</gl-button>
......
......@@ -4,6 +4,8 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
userPermissions {
pushCode
downloadCode
createMergeRequestIn
forkProject
}
pathLocks {
nodes {
......@@ -23,6 +25,9 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
path
editBlobPath
ideEditPath
forkAndEditPath
ideForkAndEditPath
canModifyBlob
storedExternally
rawPath
replacePath
......
......@@ -92,7 +92,10 @@ export default {
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-edit',
handle: () => this.showModal('#modal-confirm-fork-edit'),
handle: () => {
this.$emit('edit', 'simple');
this.showModal('#modal-confirm-fork-edit');
},
}
: { href: this.editUrl };
......@@ -128,7 +131,10 @@ export default {
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-webide',
handle: () => this.showModal('#modal-confirm-fork-webide'),
handle: () => {
this.$emit('edit', 'ide');
this.showModal('#modal-confirm-fork-webide');
},
}
: { href: this.webIdeUrl };
......
......@@ -11,13 +11,18 @@ import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import { isLoggedIn } from '~/lib/utils/common_utils';
jest.mock('~/repository/components/blob_viewers');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/lib/utils/common_utils');
let wrapper;
let mockResolver;
......@@ -34,12 +39,14 @@ const simpleMockData = {
webPath: 'some_file.js',
editBlobPath: 'some_file.js/edit',
ideEditPath: 'some_file.js/ide/edit',
forkAndEditPath: 'some_file.js/fork/edit',
ideForkAndEditPath: 'some_file.js/fork/ide',
canModifyBlob: true,
storedExternally: false,
rawPath: 'some_file.js',
externalStorageUrl: 'some_file.js',
replacePath: 'some_file.js/replace',
deletePath: 'some_file.js/delete',
forkPath: 'some_file.js/fork',
simpleViewer: {
fileType: 'text',
tooLarge: false,
......@@ -62,6 +69,8 @@ const projectMockData = {
userPermissions: {
pushCode: true,
downloadCode: true,
createMergeRequestIn: true,
forkProject: true,
},
repository: {
empty: false,
......@@ -82,6 +91,8 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => {
emptyRepo = defaultEmptyRepo,
canPushCode = defaultPushCode,
canDownloadCode = defaultDownloadCode,
createMergeRequestIn = projectMockData.userPermissions.createMergeRequestIn,
forkProject = projectMockData.userPermissions.forkProject,
pathLocks = [],
} = mockData;
......@@ -89,7 +100,12 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => {
data: {
project: {
id: '1234',
userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode },
userPermissions: {
pushCode: canPushCode,
downloadCode: canDownloadCode,
createMergeRequestIn,
forkProject,
},
pathLocks: {
nodes: pathLocks,
},
......@@ -158,9 +174,11 @@ describe('Blob content viewer component', () => {
const findBlobEdit = () => wrapper.findComponent(BlobEdit);
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
beforeEach(() => {
gon.features = { refactorTextViewer: true };
isLoggedIn.mockReturnValue(true);
});
afterEach(() => {
......@@ -469,7 +487,7 @@ describe('Blob content viewer component', () => {
});
it('does not render if not logged in', async () => {
window.gon.current_user_id = null;
isLoggedIn.mockReturnValueOnce(false);
fullFactory({
mockData: { blobInfo: simpleMockData },
......@@ -513,4 +531,60 @@ describe('Blob content viewer component', () => {
);
});
});
describe('edit blob', () => {
beforeEach(() => {
fullFactory({
mockData: { blobInfo: simpleMockData },
stubs: {
BlobContent: true,
BlobReplace: true,
},
});
});
it('simple edit redirects to the simple editor', () => {
findBlobEdit().vm.$emit('edit', 'simple');
expect(redirectTo).toHaveBeenCalledWith(simpleMockData.editBlobPath);
});
it('IDE edit redirects to the IDE editor', () => {
findBlobEdit().vm.$emit('edit', 'ide');
expect(redirectTo).toHaveBeenCalledWith(simpleMockData.ideEditPath);
});
it.each`
loggedIn | canModifyBlob | createMergeRequestIn | forkProject | showForkSuggestion
${true} | ${false} | ${true} | ${true} | ${true}
${false} | ${false} | ${true} | ${true} | ${false}
${true} | ${true} | ${false} | ${true} | ${false}
${true} | ${true} | ${true} | ${false} | ${false}
`(
'shows/hides a fork suggestion according to a set of conditions',
async ({
loggedIn,
canModifyBlob,
createMergeRequestIn,
forkProject,
showForkSuggestion,
}) => {
isLoggedIn.mockReturnValueOnce(loggedIn);
fullFactory({
mockData: {
blobInfo: { ...simpleMockData, canModifyBlob },
project: { userPermissions: { createMergeRequestIn, forkProject } },
},
stubs: {
BlobContent: true,
BlobButtonGroup: true,
},
});
findBlobEdit().vm.$emit('edit', 'simple');
await nextTick();
expect(findForkSuggestion().exists()).toBe(showForkSuggestion);
},
);
});
});
......@@ -7,6 +7,7 @@ const DEFAULT_PROPS = {
editPath: 'some_file.js/edit',
webIdePath: 'some_file.js/ide/edit',
showEditButton: true,
needsToFork: false,
};
describe('BlobEdit component', () => {
......@@ -56,7 +57,6 @@ describe('BlobEdit component', () => {
it('renders the Edit button', () => {
createComponent();
expect(findEditButton().attributes('href')).toBe(DEFAULT_PROPS.editPath);
expect(findEditButton().text()).toBe('Edit');
expect(findEditButton()).not.toBeDisabled();
});
......@@ -64,7 +64,6 @@ describe('BlobEdit component', () => {
it('renders the Web IDE button', () => {
createComponent();
expect(findWebIdeButton().attributes('href')).toBe(DEFAULT_PROPS.webIdePath);
expect(findWebIdeButton().text()).toBe('Web IDE');
expect(findWebIdeButton()).not.toBeDisabled();
});
......@@ -72,13 +71,14 @@ describe('BlobEdit component', () => {
it('renders WebIdeLink component', () => {
createComponent(true);
const { editPath: editUrl, webIdePath: webIdeUrl } = DEFAULT_PROPS;
const { editPath: editUrl, webIdePath: webIdeUrl, needsToFork } = DEFAULT_PROPS;
expect(findWebIdeLink().props()).toMatchObject({
editUrl,
webIdeUrl,
isBlob: true,
showEditButton: true,
needsToFork,
});
});
......
......@@ -160,4 +160,26 @@ describe('Web IDE link component', () => {
expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
});
});
describe('edit actions', () => {
it.each([
{
props: { showWebIdeButton: true, showEditButton: false },
expectedEventPayload: 'ide',
},
{
props: { showWebIdeButton: false, showEditButton: true },
expectedEventPayload: 'simple',
},
])(
'emits the correct event when an action handler is called',
async ({ props, expectedEventPayload }) => {
createComponent({ ...props, needsToFork: true });
findActionsButton().props('actions')[0].handle();
expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]);
},
);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment