Commit f32bbfbb authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'ide-commit-form-improvements' into 'master'

Improve Web IDE commit form

Closes #47307

See merge request gitlab-org/gitlab-ce!19883
parents 64128509 2635a691
...@@ -10,7 +10,7 @@ export default { ...@@ -10,7 +10,7 @@ export default {
}, },
computed: { computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']), ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
...mapGetters(['currentProject']), ...mapGetters(['currentProject', 'currentBranch']),
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
__('Commit to %{branchName} branch'), __('Commit to %{branchName} branch'),
...@@ -22,17 +22,30 @@ export default { ...@@ -22,17 +22,30 @@ export default {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0; return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
}, },
}, },
watch: {
disableMergeRequestRadio() {
this.updateSelectedCommitAction();
},
},
mounted() { mounted() {
if (this.disableMergeRequestRadio) { this.updateSelectedCommitAction();
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
}, },
methods: { methods: {
...mapActions('commit', ['updateCommitAction']), ...mapActions('commit', ['updateCommitAction']),
updateSelectedCommitAction() {
if (this.currentBranch && !this.currentBranch.can_push) {
this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
} else if (this.disableMergeRequestRadio) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
},
}, },
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR, commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
currentBranchPermissionsTooltip: __(
"This option is disabled as you don't have write permissions for the current branch",
),
}; };
</script> </script>
...@@ -40,9 +53,11 @@ export default { ...@@ -40,9 +53,11 @@ export default {
<div class="append-bottom-15 ide-commit-radios"> <div class="append-bottom-15 ide-commit-radios">
<radio-group <radio-group
:value="$options.commitToCurrentBranch" :value="$options.commitToCurrentBranch"
:checked="true" :disabled="currentBranch && !currentBranch.can_push"
:title="$options.currentBranchPermissionsTooltip"
> >
<span <span
class="ide-radio-label"
v-html="commitToCurrentBranchText" v-html="commitToCurrentBranchText"
> >
</span> </span>
...@@ -56,6 +71,7 @@ export default { ...@@ -56,6 +71,7 @@ export default {
v-if="currentProject.merge_requests_enabled" v-if="currentProject.merge_requests_enabled"
:value="$options.commitToNewBranchMR" :value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')" :label="__('Create a new branch and merge request')"
:title="__('This option is disabled while you still have unstaged changes')"
:show-input="true" :show-input="true"
:disabled="disableMergeRequestRadio" :disabled="disableMergeRequestRadio"
/> />
......
...@@ -24,7 +24,7 @@ export default { ...@@ -24,7 +24,7 @@ export default {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']), ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']), ...mapGetters(['hasChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() { overviewText() {
return sprintf( return sprintf(
__( __(
...@@ -36,6 +36,9 @@ export default { ...@@ -36,6 +36,9 @@ export default {
}, },
); );
}, },
commitButtonText() {
return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
},
}, },
watch: { watch: {
currentActivityView() { currentActivityView() {
...@@ -136,14 +139,14 @@ export default { ...@@ -136,14 +139,14 @@ export default {
</transition> </transition>
<commit-message-field <commit-message-field
:text="commitMessage" :text="commitMessage"
:placeholder="preBuiltCommitMessage"
@input="updateCommitMessage" @input="updateCommitMessage"
/> />
<div class="clearfix prepend-top-15"> <div class="clearfix prepend-top-15">
<actions /> <actions />
<loading-button <loading-button
:loading="submitCommitLoading" :loading="submitCommitLoading"
:disabled="commitButtonDisabled" :label="commitButtonText"
:label="__('Commit')"
container-class="btn btn-success btn-sm float-left" container-class="btn btn-success btn-sm float-left"
@click="commitChanges" @click="commitChanges"
/> />
......
...@@ -16,6 +16,10 @@ export default { ...@@ -16,6 +16,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
placeholder: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -114,7 +118,7 @@ export default { ...@@ -114,7 +118,7 @@ export default {
</div> </div>
<textarea <textarea
ref="textarea" ref="textarea"
:placeholder="__('Write a commit message...')" :placeholder="placeholder"
:value="text" :value="text"
class="note-textarea ide-commit-message-textarea" class="note-textarea ide-commit-message-textarea"
name="commit-message" name="commit-message"
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
...@@ -32,14 +31,17 @@ export default { ...@@ -32,14 +31,17 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
title: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...mapState('commit', ['commitAction']), ...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']), ...mapGetters('commit', ['newBranchName']),
tooltipTitle() { tooltipTitle() {
return this.disabled return this.disabled ? this.title : '';
? __('This option is disabled while you still have unstaged changes')
: '';
}, },
}, },
methods: { methods: {
......
...@@ -28,7 +28,7 @@ export default { ...@@ -28,7 +28,7 @@ export default {
]), ]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), ...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() { showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
}, },
......
...@@ -82,10 +82,13 @@ export const getStagedFilesCountForPath = state => path => ...@@ -82,10 +82,13 @@ export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path); getChangesCountForFiles(state.stagedFiles, path);
export const lastCommit = (state, getters) => { export const lastCommit = (state, getters) => {
const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId]; const branch = getters.currentProject && getters.currentBranch;
return branch ? branch.commit : null; return branch ? branch.commit : null;
}; };
export const currentBranch = (state, getters) =>
getters.currentProject && getters.currentProject.branches[state.currentBranchId];
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -103,17 +103,24 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data } ...@@ -103,17 +103,24 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const stageFilesPromise = rootState.stagedFiles.length
? Promise.resolve()
: dispatch('stageAllChanges', null, { root: true });
commit(types.UPDATE_LOADING, true);
return stageFilesPromise
.then(() => {
const payload = createCommitPayload({ const payload = createCommitPayload({
branch: getters.branchName, branch: getters.branchName,
newBranch, newBranch,
getters,
state, state,
rootState, rootState,
}); });
commit(types.UPDATE_LOADING, true); return service.commit(rootState.currentProjectId, payload);
})
return service
.commit(rootState.currentProjectId, payload)
.then(({ data }) => { .then(({ data }) => {
commit(types.UPDATE_LOADING, false); commit(types.UPDATE_LOADING, false);
......
import { sprintf, n__ } from '../../../../locale';
import * as consts from './constants'; import * as consts from './constants';
const BRANCH_SUFFIX_COUNT = 5; const BRANCH_SUFFIX_COUNT = 5;
...@@ -5,9 +6,6 @@ const BRANCH_SUFFIX_COUNT = 5; ...@@ -5,9 +6,6 @@ const BRANCH_SUFFIX_COUNT = 5;
export const discardDraftButtonDisabled = state => export const discardDraftButtonDisabled = state =>
state.commitMessage === '' || state.submitCommitLoading; state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
export const newBranchName = (state, _, rootState) => export const newBranchName = (state, _, rootState) =>
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr( `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
-BRANCH_SUFFIX_COUNT, -BRANCH_SUFFIX_COUNT,
...@@ -28,5 +26,18 @@ export const branchName = (state, getters, rootState) => { ...@@ -28,5 +26,18 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId; return rootState.currentBranchId;
}; };
export const preBuiltCommitMessage = (state, _, rootState) => {
if (state.commitMessage) return state.commitMessage;
const files = (rootState.stagedFiles.length
? rootState.stagedFiles
: rootState.changedFiles
).reduce((acc, val) => acc.concat(val.path), []);
return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), {
files: files.join(', '),
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -105,9 +105,9 @@ export const setPageTitle = title => { ...@@ -105,9 +105,9 @@ export const setPageTitle = title => {
document.title = title; document.title = title;
}; };
export const createCommitPayload = ({ branch, newBranch, state, rootState }) => ({ export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({
branch, branch,
commit_message: state.commitMessage, commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: rootState.stagedFiles.map(f => ({ actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update', action: f.tempFile ? 'create' : 'update',
file_path: f.path, file_path: f.path,
......
...@@ -16,6 +16,7 @@ describe('IDE commit form', () => { ...@@ -16,6 +16,7 @@ describe('IDE commit form', () => {
store.state.changedFiles.push('test'); store.state.changedFiles.push('test');
store.state.currentProjectId = 'abcproject'; store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
Vue.set(store.state.projects, 'abcproject', { ...projectData }); Vue.set(store.state.projects, 'abcproject', { ...projectData });
vm = createComponentWithStore(Component, store).$mount(); vm = createComponentWithStore(Component, store).$mount();
...@@ -146,4 +147,16 @@ describe('IDE commit form', () => { ...@@ -146,4 +147,16 @@ describe('IDE commit form', () => {
}); });
}); });
}); });
describe('commitButtonText', () => {
it('returns commit text when staged files exist', () => {
vm.$store.state.stagedFiles.push('testing');
expect(vm.commitButtonText).toBe('Commit');
});
it('returns stage & commit text when staged files do not exist', () => {
expect(vm.commitButtonText).toBe('Stage & Commit');
});
});
}); });
...@@ -13,6 +13,7 @@ describe('IDE commit message field', () => { ...@@ -13,6 +13,7 @@ describe('IDE commit message field', () => {
Component, Component,
{ {
text: '', text: '',
placeholder: 'testing',
}, },
'#app', '#app',
); );
......
...@@ -114,4 +114,19 @@ describe('IDE commit sidebar radio group', () => { ...@@ -114,4 +114,19 @@ describe('IDE commit sidebar radio group', () => {
}); });
}); });
}); });
describe('tooltipTitle', () => {
it('returns title when disabled', () => {
vm.title = 'test title';
vm.disabled = true;
expect(vm.tooltipTitle).toBe('test title');
});
it('returns blank when not disabled', () => {
vm.title = 'test title';
expect(vm.tooltipTitle).not.toBe('test title');
});
});
}); });
...@@ -8,6 +8,7 @@ export const projectData = { ...@@ -8,6 +8,7 @@ export const projectData = {
branches: { branches: {
master: { master: {
treeId: 'abcproject/master', treeId: 'abcproject/master',
can_push: true,
}, },
}, },
mergeRequests: {}, mergeRequests: {},
......
...@@ -147,18 +147,36 @@ describe('IDE store getters', () => { ...@@ -147,18 +147,36 @@ describe('IDE store getters', () => {
const commitTitle = 'Example commit title'; const commitTitle = 'Example commit title';
const localGetters = { const localGetters = {
currentProject: { currentProject: {
branches: { name: 'test-project',
'example-branch': { },
currentBranch: {
commit: { commit: {
title: commitTitle, title: commitTitle,
}, },
}, },
},
},
}; };
localState.currentBranchId = 'example-branch'; localState.currentBranchId = 'example-branch';
expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle); expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle);
}); });
}); });
describe('currentBranch', () => {
it('returns current projects branch', () => {
const localGetters = {
currentProject: {
branches: {
master: {
name: 'master',
},
},
},
};
localState.currentBranchId = 'master';
expect(getters.currentBranch(localState, localGetters)).toEqual({
name: 'master',
});
});
});
}); });
...@@ -29,46 +29,6 @@ describe('IDE commit module getters', () => { ...@@ -29,46 +29,6 @@ describe('IDE commit module getters', () => {
}); });
}); });
describe('commitButtonDisabled', () => {
const localGetters = {
discardDraftButtonDisabled: false,
};
const rootState = {
stagedFiles: ['a'],
};
it('returns false when discardDraftButtonDisabled is false & stagedFiles is not empty', () => {
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
).toBeFalsy();
});
it('returns true when discardDraftButtonDisabled is false & stagedFiles is empty', () => {
rootState.stagedFiles.length = 0;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
).toBeTruthy();
});
it('returns true when discardDraftButtonDisabled is true', () => {
localGetters.discardDraftButtonDisabled = true;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
).toBeTruthy();
});
it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
localGetters.discardDraftButtonDisabled = false;
rootState.stagedFiles.length = 0;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
).toBeTruthy();
});
});
describe('newBranchName', () => { describe('newBranchName', () => {
it('includes username, currentBranchId, patch & random number', () => { it('includes username, currentBranchId, patch & random number', () => {
gon.current_username = 'username'; gon.current_username = 'username';
...@@ -108,9 +68,7 @@ describe('IDE commit module getters', () => { ...@@ -108,9 +68,7 @@ describe('IDE commit module getters', () => {
}); });
it('uses newBranchName when not empty', () => { it('uses newBranchName when not empty', () => {
expect(getters.branchName(state, localGetters, rootState)).toBe( expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName');
'state-newBranchName',
);
}); });
it('uses getters newBranchName when state newBranchName is empty', () => { it('uses getters newBranchName when state newBranchName is empty', () => {
...@@ -118,11 +76,53 @@ describe('IDE commit module getters', () => { ...@@ -118,11 +76,53 @@ describe('IDE commit module getters', () => {
newBranchName: '', newBranchName: '',
}); });
expect(getters.branchName(state, localGetters, rootState)).toBe( expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName');
'newBranchName', });
);
}); });
}); });
}); });
describe('preBuiltCommitMessage', () => {
let rootState = {};
beforeEach(() => {
rootState.changedFiles = [];
rootState.stagedFiles = [];
});
afterEach(() => {
rootState = {};
});
it('returns commitMessage when set', () => {
state.commitMessage = 'test commit message';
expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('test commit message');
});
['changedFiles', 'stagedFiles'].forEach(key => {
it('returns commitMessage with updated file', () => {
rootState[key].push({
path: 'test-file',
});
expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('Update test-file');
});
it('returns commitMessage with updated files', () => {
rootState[key].push(
{
path: 'test-file',
},
{
path: 'index.js',
},
);
expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe(
'Update test-file, index.js files',
);
});
});
}); });
}); });
...@@ -94,6 +94,7 @@ describe('Multi-file store utils', () => { ...@@ -94,6 +94,7 @@ describe('Multi-file store utils', () => {
newBranch: false, newBranch: false,
state, state,
rootState, rootState,
getters: {},
}); });
expect(payload).toEqual({ expect(payload).toEqual({
...@@ -118,5 +119,58 @@ describe('Multi-file store utils', () => { ...@@ -118,5 +119,58 @@ describe('Multi-file store utils', () => {
start_branch: undefined, start_branch: undefined,
}); });
}); });
it('uses prebuilt commit message when commit message is empty', () => {
const rootState = {
stagedFiles: [
{
...file('staged'),
path: 'staged',
content: 'updated file content',
lastCommitSha: '123456789',
},
{
...file('newFile'),
path: 'added',
tempFile: true,
content: 'new file content',
base64: true,
lastCommitSha: '123456789',
},
],
currentBranchId: 'master',
};
const payload = utils.createCommitPayload({
branch: 'master',
newBranch: false,
state: {},
rootState,
getters: {
preBuiltCommitMessage: 'prebuilt test commit message',
},
});
expect(payload).toEqual({
branch: 'master',
commit_message: 'prebuilt test commit message',
actions: [
{
action: 'update',
file_path: 'staged',
content: 'updated file content',
encoding: 'text',
last_commit_id: '123456789',
},
{
action: 'create',
file_path: 'added',
content: 'new file content',
encoding: 'base64',
last_commit_id: '123456789',
},
],
start_branch: undefined,
});
});
}); });
}); });
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