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