Merge "Utilize gr-patch-range-select in the change view"
This commit is contained in:
commit
bf5ace98c2
@ -637,8 +637,6 @@
|
||||
this._patchRange.patchNum ||
|
||||
this.computeLatestPatchNum(this._allPatchSets));
|
||||
|
||||
this.$.fileListHeader.updateSelected();
|
||||
|
||||
const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
|
||||
this.fire('title-change', {title});
|
||||
},
|
||||
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
|
||||
<link rel="import" href="../../../styles/shared-styles.html">
|
||||
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
|
||||
<link rel="import" href="../../diff/gr-patch-range-select/gr-patch-range-select.html">
|
||||
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
<link rel="import" href="../../shared/gr-select/gr-select.html">
|
||||
@ -54,9 +55,6 @@ limitations under the License.
|
||||
.latestPatchContainer {
|
||||
display: none;
|
||||
}
|
||||
.patchSetSelect {
|
||||
max-width: 8em;
|
||||
}
|
||||
gr-editable-label.descriptionLabel {
|
||||
max-width: 100%;
|
||||
}
|
||||
@ -94,8 +92,8 @@ limitations under the License.
|
||||
.separator {
|
||||
margin: 0 .25em;
|
||||
}
|
||||
.patchSetSelect {
|
||||
max-width: 8em;
|
||||
.expandInline {
|
||||
padding-right: .25em;
|
||||
}
|
||||
.editLoaded .hideOnEdit {
|
||||
display: none;
|
||||
@ -103,32 +101,23 @@ limitations under the License.
|
||||
.editLoaded .showOnEdit {
|
||||
display: initial;
|
||||
}
|
||||
@media screen and (max-width: 50em) {
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchRange.patchNum, allPatchSets)]]">
|
||||
<div class="patchInfo-header-wrapper">
|
||||
<label class="patchSelectLabel" for="patchSetSelect">
|
||||
Patch set
|
||||
</label>
|
||||
<gr-select
|
||||
id="patchSetSelect"
|
||||
bind-value="{{_selectedPatchSet}}"
|
||||
class="patchSetSelect"
|
||||
on-change="_handlePatchChange">
|
||||
<select>
|
||||
<template is="dom-repeat" items="[[allPatchSets]]"
|
||||
as="patchNum">
|
||||
<option
|
||||
value$="[[patchNum.num]]"
|
||||
disabled$="[[_computePatchSetDisabled(patchNum.num, patchRange.basePatchNum, revisions)]]">
|
||||
[[patchNum.num]]
|
||||
/
|
||||
[[computeLatestPatchNum(allPatchSets)]]
|
||||
[[_computePatchSetCommentsString(comments, patchNum.num)]]
|
||||
[[_computePatchSetDescription(change, patchNum.num)]]
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
</gr-select>
|
||||
<gr-patch-range-select
|
||||
id="rangeSelect"
|
||||
comments="[[comments]]"
|
||||
change-num="[[changeNum]]"
|
||||
patch-range="[[patchRange]]"
|
||||
available-patches="[[allPatchSets]]"
|
||||
revisions="[[change.revisions]]"
|
||||
on-patch-range-change="_handlePatchChange">
|
||||
</gr-patch-range-select>
|
||||
/
|
||||
<gr-commit-info
|
||||
change="[[change]]"
|
||||
@ -149,7 +138,7 @@ limitations under the License.
|
||||
<gr-editable-label
|
||||
id="descriptionLabel"
|
||||
class="descriptionLabel"
|
||||
value="[[_computePatchSetDescription(change, _selectedPatchSet)]]"
|
||||
value="[[_computePatchSetDescription(change, patchRange.patchNum)]]"
|
||||
placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
|
||||
read-only="[[_descriptionReadOnly]]"
|
||||
on-changed="_handleDescriptionChanged"></gr-editable-label>
|
||||
@ -194,27 +183,6 @@ limitations under the License.
|
||||
<option value="UNIFIED_DIFF">Unified</option>
|
||||
</select>
|
||||
</gr-select>
|
||||
<span class="separator">/</span>
|
||||
<label>
|
||||
Diff against
|
||||
<gr-select id="patchChange" bind-value="{{_diffAgainst}}"
|
||||
class="patchSetSelect" on-change="_handleBasePatchChange">
|
||||
<select>
|
||||
<option value="PARENT">Base</option>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[allPatchSets]]"
|
||||
as="patchNum">
|
||||
<option
|
||||
disabled$="[[_computeBasePatchDisabled(patchNum.num, patchRange.patchNum, revisions)]]"
|
||||
value$="[[patchNum.num]]">
|
||||
[[patchNum.num]]
|
||||
[[patchNum.desc]]
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
</gr-select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
|
@ -23,6 +23,7 @@
|
||||
properties: {
|
||||
account: Object,
|
||||
allPatchSets: Array,
|
||||
/** @type {?} */
|
||||
change: Object,
|
||||
changeNum: String,
|
||||
changeUrl: String,
|
||||
@ -38,10 +39,7 @@
|
||||
notify: true,
|
||||
},
|
||||
/** @type {?} */
|
||||
patchRange: {
|
||||
type: Object,
|
||||
observer: 'updateSelected',
|
||||
},
|
||||
patchRange: Object,
|
||||
revisions: Array,
|
||||
// Caps the number of files that can be shown and have the 'show diffs' /
|
||||
// 'hide diffs' buttons still be functional.
|
||||
@ -54,8 +52,6 @@
|
||||
type: Boolean,
|
||||
computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
|
||||
},
|
||||
_selectedPatchSet: String,
|
||||
_diffAgainst: String,
|
||||
},
|
||||
|
||||
behaviors: [
|
||||
@ -70,11 +66,6 @@
|
||||
this.fire('collapse-diffs');
|
||||
},
|
||||
|
||||
updateSelected() {
|
||||
this._selectedPatchSet = this.patchRange.patchNum;
|
||||
this._diffAgainst = this.patchRange.basePatchNum;
|
||||
},
|
||||
|
||||
_computeDescriptionPlaceholder(readOnly) {
|
||||
return (readOnly ? 'No' : 'Add a') + ' patch set description';
|
||||
},
|
||||
@ -107,10 +98,10 @@
|
||||
_handleDescriptionChanged(e) {
|
||||
const desc = e.detail.trim();
|
||||
const rev = this.getRevisionByPatchNum(this.change.revisions,
|
||||
this._selectedPatchSet);
|
||||
this.patchRange.patchNum);
|
||||
const sha = this._getPatchsetHash(this.change.revisions, rev);
|
||||
this.$.restAPI.setDescription(this.changeNum,
|
||||
this._selectedPatchSet, desc)
|
||||
this.patchRange.patchNum, desc)
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
this.set(['_change', 'revisions', sha, 'description'], desc);
|
||||
@ -127,63 +118,6 @@
|
||||
return !loggedIn || !prefs;
|
||||
},
|
||||
|
||||
// Copied from gr-file-list
|
||||
_getCommentsForPath(comments, patchNum, path) {
|
||||
return (comments[path] || []).filter(c => {
|
||||
return this.patchNumEquals(c.patch_set, patchNum);
|
||||
});
|
||||
},
|
||||
|
||||
// Copied from gr-file-list
|
||||
_computeUnresolvedNum(comments, drafts, patchNum, path) {
|
||||
comments = this._getCommentsForPath(comments, patchNum, path);
|
||||
drafts = this._getCommentsForPath(drafts, patchNum, path);
|
||||
comments = comments.concat(drafts);
|
||||
|
||||
// Create an object where every comment ID is the key of an unresolved
|
||||
// comment.
|
||||
|
||||
const idMap = comments.reduce((acc, comment) => {
|
||||
if (comment.unresolved) {
|
||||
acc[comment.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Set false for the comments that are marked as parents.
|
||||
for (const comment of comments) {
|
||||
idMap[comment.in_reply_to] = false;
|
||||
}
|
||||
|
||||
// The unresolved comments are the comments that still have true.
|
||||
const unresolvedLeaves = Object.keys(idMap).filter(key => {
|
||||
return idMap[key];
|
||||
});
|
||||
|
||||
return unresolvedLeaves.length;
|
||||
},
|
||||
|
||||
_computePatchSetCommentsString(allComments, patchNum) {
|
||||
let numComments = 0;
|
||||
let numUnresolved = 0;
|
||||
for (const file in allComments) {
|
||||
if (allComments.hasOwnProperty(file)) {
|
||||
numComments += this._getCommentsForPath(
|
||||
allComments, patchNum, file).length;
|
||||
numUnresolved += this._computeUnresolvedNum(
|
||||
allComments, {}, patchNum, file);
|
||||
}
|
||||
}
|
||||
let commentsStr = '';
|
||||
if (numComments > 0) {
|
||||
commentsStr = '(' + numComments + ' comments';
|
||||
if (numUnresolved > 0) {
|
||||
commentsStr += ', ' + numUnresolved + ' unresolved';
|
||||
}
|
||||
commentsStr += ')';
|
||||
}
|
||||
return commentsStr;
|
||||
},
|
||||
|
||||
_fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
|
||||
return shownFileCount <= maxFilesForBulkActions;
|
||||
@ -211,7 +145,7 @@
|
||||
* always include the patch range, even if the requested patchNum is
|
||||
* known to be the latest.
|
||||
*/
|
||||
_changePatchNum(patchNum, basePatchNum, opt_forceParams) {
|
||||
_changePatchNum(basePatchNum, patchNum, opt_forceParams) {
|
||||
if (!opt_forceParams) {
|
||||
let currentPatchNum;
|
||||
if (this.change.current_revision) {
|
||||
@ -230,12 +164,8 @@
|
||||
basePatchNum);
|
||||
},
|
||||
|
||||
_handleBasePatchChange(e) {
|
||||
this._changePatchNum(this._selectedPatchSet, e.target.value, true);
|
||||
},
|
||||
|
||||
_handlePatchChange(e) {
|
||||
this._changePatchNum(e.target.value, this._diffAgainst, true);
|
||||
this._changePatchNum(e.detail.leftPatch, e.detail.rightPatch, true);
|
||||
},
|
||||
|
||||
_handlePrefsTap(e) {
|
||||
|
@ -123,42 +123,6 @@ limitations under the License.
|
||||
false);
|
||||
});
|
||||
|
||||
test('_computePatchSetCommentsString', () => {
|
||||
// Test string with unresolved comments.
|
||||
|
||||
comments = {
|
||||
foo: [{
|
||||
id: '27dcee4d_f7b77cfa',
|
||||
message: 'test',
|
||||
patch_set: 1,
|
||||
unresolved: true,
|
||||
}],
|
||||
bar: [{
|
||||
id: '27dcee4d_f7b77cfa',
|
||||
message: 'test',
|
||||
patch_set: 1,
|
||||
},
|
||||
{
|
||||
id: '27dcee4d_f7b77cfa',
|
||||
message: 'test',
|
||||
patch_set: 1,
|
||||
}],
|
||||
abc: [],
|
||||
};
|
||||
|
||||
assert.equal(element._computePatchSetCommentsString(comments, 1),
|
||||
'(3 comments, 1 unresolved)');
|
||||
|
||||
// Test string with no unresolved comments.
|
||||
delete comments['foo'];
|
||||
assert.equal(element._computePatchSetCommentsString(comments, 1),
|
||||
'(2 comments)');
|
||||
|
||||
// Test string with no comments.
|
||||
delete comments['bar'];
|
||||
assert.equal(element._computePatchSetCommentsString(comments, 1), '');
|
||||
});
|
||||
|
||||
test('_handleDescriptionChanged', () => {
|
||||
const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
|
||||
.returns(Promise.resolve({ok: true}));
|
||||
@ -169,7 +133,6 @@ limitations under the License.
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: 1,
|
||||
};
|
||||
element._selectedPatchNum = '1';
|
||||
element.change = {
|
||||
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
|
||||
revisions: {
|
||||
@ -195,130 +158,6 @@ limitations under the License.
|
||||
assert.equal(putDescStub.args[0][2], 'test2');
|
||||
});
|
||||
|
||||
test('patch num change', done => {
|
||||
element.patchRange = {
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: 2,
|
||||
};
|
||||
|
||||
element.allPatchSets = [
|
||||
{num: 1},
|
||||
{num: 2},
|
||||
{num: 3},
|
||||
{num: 13},
|
||||
];
|
||||
|
||||
flushAsynchronousOperations();
|
||||
|
||||
const selectEl = element.$$('.patchInfo-header gr-select');
|
||||
assert.ok(selectEl);
|
||||
const optionEls = Polymer.dom(element.root).querySelectorAll(
|
||||
'.patchInfo-header option');
|
||||
assert.equal(optionEls.length, 4);
|
||||
const select = element.$$('.patchInfo-header #patchSetSelect').bindValue;
|
||||
assert.notEqual(select, 1);
|
||||
assert.equal(select, 2);
|
||||
assert.notEqual(select, 3);
|
||||
assert.equal(optionEls[3].value, 13);
|
||||
|
||||
let numEvents = 0;
|
||||
selectEl.addEventListener('change', e => {
|
||||
assert.equal(element.diffViewMode, 'SIDE_BY_SIDE');
|
||||
numEvents++;
|
||||
if (numEvents == 1) {
|
||||
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
|
||||
element.change, '1', 'PARENT'));
|
||||
selectEl.nativeSelect.value = '3';
|
||||
element.fire('change', {}, {node: selectEl.nativeSelect});
|
||||
} else if (numEvents == 2) {
|
||||
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
|
||||
element.change, '3', 'PARENT'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
selectEl.nativeSelect.value = '1';
|
||||
element.fire('change', {}, {node: selectEl.nativeSelect});
|
||||
});
|
||||
|
||||
test('patch num change with missing current_revision', done => {
|
||||
element.patchRange = {
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: 2,
|
||||
};
|
||||
|
||||
element.allPatchSets = [
|
||||
{num: 1},
|
||||
{num: 2},
|
||||
{num: 3},
|
||||
{num: 13},
|
||||
];
|
||||
flushAsynchronousOperations();
|
||||
const selectEl = element.$$('.patchInfo-header gr-select');
|
||||
assert.ok(selectEl);
|
||||
const optionEls = Polymer.dom(element.root).querySelectorAll(
|
||||
'.patchInfo-header option');
|
||||
assert.equal(optionEls.length, 4);
|
||||
assert.notEqual(
|
||||
element.$$('.patchInfo-header #patchSetSelect').bindValue, 1);
|
||||
assert.equal(
|
||||
element.$$('.patchInfo-header #patchSetSelect').bindValue, 2);
|
||||
assert.notEqual(
|
||||
element.$$('.patchInfo-header #patchSetSelect').bindValue, 3);
|
||||
assert.equal(optionEls[3].value, 13);
|
||||
|
||||
let numEvents = 0;
|
||||
selectEl.addEventListener('change', e => {
|
||||
numEvents++;
|
||||
if (numEvents == 1) {
|
||||
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
|
||||
element.change, '1', 'PARENT'));
|
||||
selectEl.nativeSelect.value = '3';
|
||||
element.fire('change', {}, {node: selectEl.nativeSelect});
|
||||
} else if (numEvents == 2) {
|
||||
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
|
||||
element.change, '3', 'PARENT'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
selectEl.nativeSelect.value = '1';
|
||||
element.fire('change', {}, {node: selectEl.nativeSelect});
|
||||
});
|
||||
|
||||
test('diff against dropdown', done => {
|
||||
element.revisions = [
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
];
|
||||
element.allPatchSets = [
|
||||
{num: 1},
|
||||
{num: 2},
|
||||
{num: 3},
|
||||
{num: 'edit'},
|
||||
];
|
||||
element.patchRange = {
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: '3',
|
||||
};
|
||||
|
||||
flush(() => {
|
||||
const selectEl = element.$.patchChange;
|
||||
assert.equal(selectEl.nativeSelect.value, 'PARENT');
|
||||
assert.isTrue(element.$$('#patchChange option[value="3"]')
|
||||
.hasAttribute('disabled'));
|
||||
selectEl.addEventListener('change', () => {
|
||||
assert.equal(selectEl.nativeSelect.value, 'edit');
|
||||
assert(navigateToChangeStub.lastCall.calledWithExactly(
|
||||
element.change, '3', 'edit'),
|
||||
'Should navigate to /c/42/edit..3');
|
||||
done();
|
||||
});
|
||||
selectEl.nativeSelect.value = 'edit';
|
||||
element.fire('change', {}, {node: selectEl.nativeSelect});
|
||||
});
|
||||
});
|
||||
|
||||
test('expandAllDiffs called when expand button clicked', () => {
|
||||
element.shownFileCount = 1;
|
||||
flushAsynchronousOperations();
|
||||
@ -367,6 +206,15 @@ limitations under the License.
|
||||
assert.equal(select.nativeSelect.value, 'UNIFIED_DIFF');
|
||||
});
|
||||
|
||||
test('_changePatchNum called when range select changes', () => {
|
||||
const leftPatch = 1;
|
||||
const rightPatch = 2;
|
||||
sandbox.stub(element, '_changePatchNum');
|
||||
element.$.rangeSelect.fire('patch-range-change', {leftPatch, rightPatch});
|
||||
assert.isTrue(element._changePatchNum.lastCall
|
||||
.calledWithExactly(1, 2, true));
|
||||
});
|
||||
|
||||
test('include base patch when not parent', () => {
|
||||
element.changeNum = '42';
|
||||
element.patchRange = {
|
||||
@ -385,13 +233,13 @@ limitations under the License.
|
||||
labels: {},
|
||||
};
|
||||
|
||||
element._changePatchNum(13, 2);
|
||||
element._changePatchNum(2, 13);
|
||||
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
|
||||
element.change, 13, 2));
|
||||
|
||||
element.patchRange.basePatchNum = 'PARENT';
|
||||
|
||||
element._changePatchNum(3, 'PARENT');
|
||||
element._changePatchNum('PARENT', 3);
|
||||
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
|
||||
element.change, 3, 'PARENT'));
|
||||
});
|
||||
|
@ -37,6 +37,9 @@ limitations under the License.
|
||||
:host {
|
||||
background-color: var(--view-background-color);
|
||||
}
|
||||
gr-patch-range-select {
|
||||
display: block;
|
||||
}
|
||||
gr-diff {
|
||||
border: none;
|
||||
}
|
||||
@ -271,12 +274,13 @@ limitations under the License.
|
||||
<div class="subHeader">
|
||||
<div class="patchRangeLeft">
|
||||
<gr-patch-range-select
|
||||
path="[[_path]]"
|
||||
id="rangeSelect"
|
||||
change-num="[[_changeNum]]"
|
||||
patch-range="[[_patchRange]]"
|
||||
files-weblinks="[[_filesWeblinks]]"
|
||||
available-patches="[[_computeAvailablePatches(_change.revisions, _change.revisions.*)]]"
|
||||
revisions="[[_change.revisions]]">
|
||||
revisions="[[_change.revisions]]"
|
||||
on-patch-range-change="_handlePatchChange">
|
||||
</gr-patch-range-select>
|
||||
<span class="download desktop">
|
||||
<span class="separator">/</span>
|
||||
|
@ -580,7 +580,9 @@
|
||||
},
|
||||
|
||||
_computeAvailablePatches(revs) {
|
||||
return this.sortRevisions(Object.values(revs)).map(e => e._number);
|
||||
return this.sortRevisions(Object.values(revs)).map(e => {
|
||||
return {num: e._number};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -672,6 +674,13 @@
|
||||
this.$.dropdown.open();
|
||||
},
|
||||
|
||||
_handlePatchChange(e) {
|
||||
const rightPatch = e.detail.rightPatch;
|
||||
const leftPatch = e.detail.leftPatch;
|
||||
Gerrit.Nav.navigateToDiff(
|
||||
this._change, this._path, rightPatch, leftPatch);
|
||||
},
|
||||
|
||||
_handlePrefsTap(e) {
|
||||
e.preventDefault();
|
||||
this.$.diffPreferences.open();
|
||||
|
@ -446,6 +446,18 @@ limitations under the License.
|
||||
});
|
||||
});
|
||||
|
||||
test('_handlePatchChange calls navigateToDiff correctly', () => {
|
||||
const leftPatch = 'PARENT';
|
||||
const rightPatch = '3';
|
||||
const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
|
||||
element._change = {_number: 321, project: 'foo/bar'};
|
||||
element._path = 'path/to/file.txt';
|
||||
element.$.rangeSelect.fire('patch-range-change', {leftPatch, rightPatch});
|
||||
|
||||
assert(navigateStub.lastCall.calledWithExactly(element._change,
|
||||
element._path, rightPatch, leftPatch));
|
||||
});
|
||||
|
||||
test('download link', () => {
|
||||
element._changeNum = '42';
|
||||
element._patchRange = {
|
||||
|
@ -22,9 +22,6 @@ limitations under the License.
|
||||
<dom-module id="gr-patch-range-select">
|
||||
<template>
|
||||
<style include="shared-styles">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.patchRange {
|
||||
display: inline-block;
|
||||
}
|
||||
@ -46,11 +43,12 @@ limitations under the License.
|
||||
on-change="_handlePatchChange">
|
||||
<select>
|
||||
<option value="PARENT">Base</option>
|
||||
<template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
|
||||
<option value$="[[patchNum]]"
|
||||
disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">
|
||||
[[patchNum]]
|
||||
[[_computePatchSetDescription(revisions, patchNum)]]
|
||||
<template is="dom-repeat" items="{{availablePatches}}" as="basePatchNum">
|
||||
<option value$="[[basePatchNum.num]]"
|
||||
disabled$="[[_computeLeftDisabled(basePatchNum.num, patchRange.patchNum, _sortedRevisions)]]">
|
||||
[[basePatchNum.num]]
|
||||
[[_computePatchSetCommentsString(comments, basePatchNum.num)]]
|
||||
[[_computePatchSetDescription(revisions, basePatchNum.num)]]
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
@ -68,10 +66,11 @@ limitations under the License.
|
||||
on-change="_handlePatchChange">
|
||||
<select>
|
||||
<template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
|
||||
<option value$="[[patchNum]]"
|
||||
disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">
|
||||
[[patchNum]]
|
||||
[[_computePatchSetDescription(revisions, patchNum)]]
|
||||
<option value$="[[patchNum.num]]"
|
||||
disabled$="[[_computeRightDisabled(patchNum.num, patchRange.basePatchNum, _sortedRevisions)]]">
|
||||
[[patchNum.num]]
|
||||
[[_computePatchSetCommentsString(comments, patchNum.num)]]
|
||||
[[_computePatchSetDescription(revisions, patchNum.num)]]
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
|
@ -17,26 +17,36 @@
|
||||
// Maximum length for patch set descriptions.
|
||||
const PATCH_DESC_MAX_LENGTH = 500;
|
||||
|
||||
/**
|
||||
* Fired when the patch range changes
|
||||
*
|
||||
* @event patch-range-change
|
||||
*
|
||||
* @property {string} leftPatch
|
||||
* @property {string} rightPatch
|
||||
*/
|
||||
|
||||
Polymer({
|
||||
is: 'gr-patch-range-select',
|
||||
|
||||
properties: {
|
||||
availablePatches: Array,
|
||||
changeNum: String,
|
||||
comments: Array,
|
||||
/** @type {{ meta_a: !Array, meta_b: !Array}} */
|
||||
filesWeblinks: Object,
|
||||
path: String,
|
||||
patchRange: {
|
||||
type: Object,
|
||||
observer: '_updateSelected',
|
||||
},
|
||||
/** @type {?} */
|
||||
patchRange: Object,
|
||||
revisions: Object,
|
||||
_sortedRevisions: Array,
|
||||
_rightSelected: String,
|
||||
_leftSelected: String,
|
||||
},
|
||||
|
||||
observers: ['_updateSortedRevisions(revisions.*)'],
|
||||
observers: [
|
||||
'_updateSortedRevisions(revisions.*)',
|
||||
'_updateSelected(patchRange.*)',
|
||||
],
|
||||
|
||||
behaviors: [Gerrit.PatchSetBehavior],
|
||||
|
||||
@ -53,24 +63,20 @@
|
||||
_handlePatchChange(e) {
|
||||
const leftPatch = this._leftSelected;
|
||||
const rightPatch = this._rightSelected;
|
||||
let rangeStr = rightPatch;
|
||||
if (leftPatch != 'PARENT') {
|
||||
rangeStr = leftPatch + '..' + rangeStr;
|
||||
}
|
||||
page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path);
|
||||
this.fire('patch-range-change', {rightPatch, leftPatch});
|
||||
e.target.blur();
|
||||
},
|
||||
|
||||
_computeLeftDisabled(patchNum, patchRange) {
|
||||
return this.findSortedIndex(patchNum, this._sortedRevisions) >=
|
||||
this.findSortedIndex(patchRange.patchNum, this._sortedRevisions);
|
||||
_computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
|
||||
return this.findSortedIndex(basePatchNum, sortedRevisions) >=
|
||||
this.findSortedIndex(patchNum, sortedRevisions);
|
||||
},
|
||||
|
||||
_computeRightDisabled(patchNum, patchRange) {
|
||||
if (patchRange.basePatchNum == 'PARENT') { return false; }
|
||||
_computeRightDisabled(patchNum, basePatchNum, sortedRevisions) {
|
||||
if (basePatchNum == 'PARENT') { return false; }
|
||||
|
||||
return this.findSortedIndex(patchNum, this._sortedRevisions) <=
|
||||
this.findSortedIndex(patchRange.basePatchNum, this._sortedRevisions);
|
||||
return this.findSortedIndex(patchNum, sortedRevisions) <=
|
||||
this.findSortedIndex(basePatchNum, sortedRevisions);
|
||||
},
|
||||
|
||||
// On page load, the dom-if for options getting added occurs after
|
||||
@ -86,6 +92,68 @@
|
||||
this.$.leftPatchSelect.value = this._leftSelected;
|
||||
},
|
||||
|
||||
// Copied from gr-file-list
|
||||
// @todo(beckysiegel) clean up.
|
||||
_getCommentsForPath(comments, patchNum, path) {
|
||||
return (comments[path] || []).filter(c => {
|
||||
return this.patchNumEquals(c.patch_set, patchNum);
|
||||
});
|
||||
},
|
||||
|
||||
// Copied from gr-file-list
|
||||
// @todo(beckysiegel) clean up.
|
||||
_computeUnresolvedNum(comments, drafts, patchNum, path) {
|
||||
comments = this._getCommentsForPath(comments, patchNum, path);
|
||||
drafts = this._getCommentsForPath(drafts, patchNum, path);
|
||||
comments = comments.concat(drafts);
|
||||
|
||||
// Create an object where every comment ID is the key of an unresolved
|
||||
// comment.
|
||||
|
||||
const idMap = comments.reduce((acc, comment) => {
|
||||
if (comment.unresolved) {
|
||||
acc[comment.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Set false for the comments that are marked as parents.
|
||||
for (const comment of comments) {
|
||||
idMap[comment.in_reply_to] = false;
|
||||
}
|
||||
|
||||
// The unresolved comments are the comments that still have true.
|
||||
const unresolvedLeaves = Object.keys(idMap).filter(key => {
|
||||
return idMap[key];
|
||||
});
|
||||
|
||||
return unresolvedLeaves.length;
|
||||
},
|
||||
|
||||
_computePatchSetCommentsString(allComments, patchNum) {
|
||||
// todo (beckysiegel) get comment strings for diff view also.
|
||||
if (!allComments) { return ''; }
|
||||
let numComments = 0;
|
||||
let numUnresolved = 0;
|
||||
for (const file in allComments) {
|
||||
if (allComments.hasOwnProperty(file)) {
|
||||
numComments += this._getCommentsForPath(
|
||||
allComments, patchNum, file).length;
|
||||
numUnresolved += this._computeUnresolvedNum(
|
||||
allComments, {}, patchNum, file);
|
||||
}
|
||||
}
|
||||
let commentsStr = '';
|
||||
if (numComments > 0) {
|
||||
commentsStr = '(' + numComments + ' comments';
|
||||
if (numUnresolved > 0) {
|
||||
commentsStr += ', ' + numUnresolved + ' unresolved';
|
||||
}
|
||||
commentsStr += ')';
|
||||
}
|
||||
return commentsStr;
|
||||
},
|
||||
|
||||
_computePatchSetDescription(revisions, patchNum) {
|
||||
const rev = this.getRevisionByPatchNum(revisions, patchNum);
|
||||
return (rev && rev.description) ?
|
||||
|
@ -50,38 +50,105 @@ limitations under the License.
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: '3',
|
||||
};
|
||||
element._sortedRevisions = [
|
||||
const sortedRevisions = [
|
||||
{_number: 1},
|
||||
{_number: 2},
|
||||
{_number: element.EDIT_NAME, basePatchNum: 2},
|
||||
{_number: 3},
|
||||
];
|
||||
for (const patchNum of ['1', '2', '3']) {
|
||||
assert.isFalse(element._computeRightDisabled(patchNum, patchRange));
|
||||
assert.isFalse(element._computeRightDisabled(patchNum,
|
||||
patchRange.basePatchNum, sortedRevisions));
|
||||
}
|
||||
for (const patchNum of ['PARENT', '1', '2']) {
|
||||
assert.isFalse(element._computeLeftDisabled(patchNum, patchRange));
|
||||
assert.isFalse(element._computeLeftDisabled(patchNum,
|
||||
patchRange.patchNum, sortedRevisions));
|
||||
}
|
||||
assert.isTrue(element._computeLeftDisabled('3', patchRange));
|
||||
assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
|
||||
|
||||
patchRange.basePatchNum = element.EDIT_NAME;
|
||||
assert.isTrue(element._computeLeftDisabled('3', patchRange));
|
||||
assert.isTrue(element._computeRightDisabled('1', patchRange));
|
||||
assert.isTrue(element._computeRightDisabled('2', patchRange));
|
||||
assert.isFalse(element._computeRightDisabled('3', patchRange));
|
||||
assert.isTrue(element._computeRightDisabled(element.EDIT_NAME, patchRange));
|
||||
assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
|
||||
sortedRevisions));
|
||||
assert.isTrue(element._computeRightDisabled('1', patchRange.basePatchNum,
|
||||
sortedRevisions));
|
||||
assert.isTrue(element._computeRightDisabled('2', patchRange.basePatchNum,
|
||||
sortedRevisions));
|
||||
assert.isFalse(element._computeRightDisabled('3', patchRange.basePatchNum,
|
||||
sortedRevisions));
|
||||
assert.isTrue(element._computeRightDisabled(element.EDIT_NAME,
|
||||
patchRange.basePatchNum, sortedRevisions));
|
||||
});
|
||||
|
||||
test('navigation', done => {
|
||||
test('_updateSelected called with subproperty changes', () => {
|
||||
sandbox.stub(element, '_updateSelected');
|
||||
element.patchRange = {patchNum: 1, basePatchNum: 'PARENT'};
|
||||
assert.equal(element._updateSelected.callCount, 1);
|
||||
|
||||
element.set('patchRange.patchNum', 2);
|
||||
assert.equal(element._updateSelected.callCount, 2);
|
||||
|
||||
element.set('patchRange.basePatchNum', 1);
|
||||
assert.equal(element._updateSelected.callCount, 3);
|
||||
});
|
||||
|
||||
test('_computeLeftDisabled called when patchNum updates', () => {
|
||||
element.revisions = [
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
];
|
||||
element.availablePatches = [
|
||||
{num: 1},
|
||||
{num: 2},
|
||||
{num: 3},
|
||||
{num: 'edit'},
|
||||
];
|
||||
element.patchRange = {patchNum: 2, basePatchNum: 'PARENT'};
|
||||
flushAsynchronousOperations();
|
||||
|
||||
// Should be recomputed for each available patch
|
||||
sandbox.stub(element, '_computeLeftDisabled');
|
||||
element.set('patchRange.patchNum', '1');
|
||||
assert.equal(element._computeLeftDisabled.callCount, 4);
|
||||
});
|
||||
|
||||
test('_computeRightDisabled called when basePatchNum updates', () => {
|
||||
element.revisions = [
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
];
|
||||
element.availablePatches = [
|
||||
{num: 1},
|
||||
{num: 2},
|
||||
{num: 3},
|
||||
{num: 'edit'},
|
||||
];
|
||||
element.patchRange = {patchNum: 2, basePatchNum: 'PARENT'};
|
||||
flushAsynchronousOperations();
|
||||
|
||||
// Should be recomputed for each available patch
|
||||
sandbox.stub(element, '_computeRightDisabled');
|
||||
element.set('patchRange.basePatchNum', '1');
|
||||
assert.equal(element._computeRightDisabled.callCount, 4);
|
||||
});
|
||||
|
||||
|
||||
test('changes in patch range fire event', done => {
|
||||
sandbox.stub(element, '_computeLeftDisabled').returns(false);
|
||||
sandbox.stub(element, '_computeRightDisabled').returns(false);
|
||||
const showStub = sandbox.stub(page, 'show');
|
||||
const patchRangeChangedStub = sandbox.stub();
|
||||
element.addEventListener('patch-range-change', patchRangeChangedStub);
|
||||
|
||||
const leftSelectEl = element.$.leftPatchSelect;
|
||||
const rightSelectEl = element.$.rightPatchSelect;
|
||||
const blurSpy = sandbox.spy(leftSelectEl, 'blur');
|
||||
element.changeNum = '42';
|
||||
element.path = 'path/to/file.txt';
|
||||
element.availablePatches = ['1', '2', '3'];
|
||||
element.availablePatches =
|
||||
[{num: '1'}, {num: '2'}, {num: '3'}, {num: 'edit'}];
|
||||
element.patchRange = {
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: '3',
|
||||
@ -91,25 +158,66 @@ limitations under the License.
|
||||
let numEvents = 0;
|
||||
leftSelectEl.addEventListener('change', e => {
|
||||
numEvents++;
|
||||
if (numEvents == 1) {
|
||||
assert(showStub.lastCall.calledWithExactly(
|
||||
'/c/42/3/path/to/file.txt'),
|
||||
'Should navigate to /c/42/3/path/to/file.txt');
|
||||
leftSelectEl.nativeSelect.value = '1';
|
||||
if (numEvents === 1) {
|
||||
assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
|
||||
{rightPatch: '3', leftPatch: 'PARENT'});
|
||||
leftSelectEl.nativeSelect.value = 'edit';
|
||||
element.fire('change', {}, {node: leftSelectEl});
|
||||
assert(blurSpy.called, 'Dropdown should be blurred after selection');
|
||||
} else if (numEvents == 2) {
|
||||
assert(showStub.lastCall.calledWithExactly(
|
||||
'/c/42/1..3/path/to/file.txt'),
|
||||
'Should navigate to /c/42/1..3/path/to/file.txt');
|
||||
done();
|
||||
} else if (numEvents === 2) {
|
||||
assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
|
||||
{rightPatch: '3', leftPatch: 'edit'});
|
||||
rightSelectEl.nativeSelect.value = '1';
|
||||
element.fire('change', {}, {node: rightSelectEl});
|
||||
}
|
||||
});
|
||||
rightSelectEl.addEventListener('change', e => {
|
||||
assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
|
||||
{rightPatch: '1', leftPatch: 'edit'});
|
||||
done();
|
||||
});
|
||||
leftSelectEl.nativeSelect.value = 'PARENT';
|
||||
rightSelectEl.nativeSelect.value = '3';
|
||||
element.fire('change', {}, {node: leftSelectEl});
|
||||
});
|
||||
|
||||
test('diff against dropdown', done => {
|
||||
element.revisions = [
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
{commit: {}},
|
||||
];
|
||||
element.availablePatches = [
|
||||
{num: 1},
|
||||
{num: 2},
|
||||
{num: 3},
|
||||
{num: 'edit'},
|
||||
];
|
||||
element.patchRange = {
|
||||
basePatchNum: 'PARENT',
|
||||
patchNum: '3',
|
||||
};
|
||||
|
||||
const patchRangeChangedStub = sandbox.stub();
|
||||
element.addEventListener('patch-range-change', patchRangeChangedStub);
|
||||
|
||||
flush(() => {
|
||||
const selectEl = element.$.leftPatchSelect;
|
||||
assert.equal(selectEl.nativeSelect.value, 'PARENT');
|
||||
assert.isTrue(element.$$('#leftPatchSelect option[value="3"]')
|
||||
.hasAttribute('disabled'));
|
||||
selectEl.addEventListener('change', () => {
|
||||
assert.equal(selectEl.nativeSelect.value, 'edit');
|
||||
assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
|
||||
{leftPatch: 'edit', rightPatch: '3'});
|
||||
done();
|
||||
});
|
||||
selectEl.nativeSelect.value = 'edit';
|
||||
element.fire('change', {}, {node: selectEl.nativeSelect});
|
||||
});
|
||||
});
|
||||
|
||||
test('filesWeblinks', () => {
|
||||
element.filesWeblinks = {
|
||||
meta_a: [
|
||||
@ -132,5 +240,40 @@ limitations under the License.
|
||||
assert.equal(
|
||||
domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
|
||||
});
|
||||
|
||||
test('_computePatchSetCommentsString', () => {
|
||||
// Test string with unresolved comments.
|
||||
comments = {
|
||||
foo: [{
|
||||
id: '27dcee4d_f7b77cfa',
|
||||
message: 'test',
|
||||
patch_set: 1,
|
||||
unresolved: true,
|
||||
}],
|
||||
bar: [{
|
||||
id: '27dcee4d_f7b77cfa',
|
||||
message: 'test',
|
||||
patch_set: 1,
|
||||
},
|
||||
{
|
||||
id: '27dcee4d_f7b77cfa',
|
||||
message: 'test',
|
||||
patch_set: 1,
|
||||
}],
|
||||
abc: [],
|
||||
};
|
||||
|
||||
assert.equal(element._computePatchSetCommentsString(comments, 1),
|
||||
'(3 comments, 1 unresolved)');
|
||||
|
||||
// Test string with no unresolved comments.
|
||||
delete comments['foo'];
|
||||
assert.equal(element._computePatchSetCommentsString(comments, 1),
|
||||
'(2 comments)');
|
||||
|
||||
// Test string with no comments.
|
||||
delete comments['bar'];
|
||||
assert.equal(element._computePatchSetCommentsString(comments, 1), '');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user