Remove old implementation of gr-related-changes-list

Change-Id: Ic42f9af760042cd0e080a29b09c3d21d20bd161c
(cherry picked from commit f819216d75fc9d171b1154acaa48e2dbb96bb35b)
This commit is contained in:
Milutin Kristofic
2021-04-21 22:12:59 +02:00
parent 42ce384e62
commit 9b04467e63
9 changed files with 1174 additions and 2877 deletions

View File

@@ -38,7 +38,6 @@ import '../gr-file-list-header/gr-file-list-header';
import '../gr-included-in-dialog/gr-included-in-dialog';
import '../gr-messages-list/gr-messages-list';
import '../gr-related-changes-list/gr-related-changes-list';
import '../gr-related-changes-list-experimental/gr-related-changes-list-experimental';
import '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
import '../gr-reply-dialog/gr-reply-dialog';
import '../gr-thread-list/gr-thread-list';
@@ -52,10 +51,7 @@ import {
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {pluralize} from '../../../utils/string-util';
import {
getComputedStyleValue,
windowLocationReload,
} from '../../../utils/dom-util';
import {windowLocationReload} from '../../../utils/dom-util';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@@ -172,7 +168,6 @@ import {GerritView} from '../../../services/router/router-model';
import {takeUntil} from 'rxjs/operators';
import {aPluginHasRegistered$} from '../../../services/checks/checks-model';
import {Subject} from 'rxjs';
import {GrRelatedChangesListExperimental} from '../gr-related-changes-list-experimental/gr-related-changes-list-experimental';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {Timing} from '../../../constants/reporting';
@@ -187,17 +182,6 @@ const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
const REVIEWERS_REGEX = /^(R|CC)=/gm;
const MIN_CHECK_INTERVAL_SECS = 0;
// These are the same as the breakpoint set in CSS. Make sure both are changed
// together.
const BREAKPOINT_RELATED_SMALL = '50em';
const BREAKPOINT_RELATED_MED = '75em';
// In the event that the related changes medium width calculation is too close
// to zero, provide some height.
const MINIMUM_RELATED_MAX_HEIGHT = 100;
const SMALL_RELATED_HEIGHT = 400;
const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
@@ -467,9 +451,6 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
})
_commitCollapsible?: boolean;
@property({type: Boolean})
_relatedChangesCollapsed = true;
@property({type: Number})
_updateCheckTimerHandle?: number | null;
@@ -479,9 +460,6 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
})
_editMode?: boolean;
@property({type: Boolean, observer: '_updateToggleContainerClass'})
_showRelatedToggle = false;
@property({
type: Boolean,
computed: '_isParentCurrent(_currentRevisionActions)',
@@ -1302,7 +1280,6 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
this._initialLoadComplete = false;
this._changeNum = value.changeNum;
this.getRelatedChangesList()?.clear();
this._reload(true).then(() => {
this._performPostLoadTasks();
});
@@ -2161,7 +2138,6 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
return Promise.resolve([]);
}
this._loading = true;
this._relatedChangesCollapsed = true;
this.reporting.time(Timing.CHANGE_RELOAD);
this.reporting.time(Timing.CHANGE_DATA);
@@ -2259,30 +2235,25 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
if (isLocationChange) {
this._editingCommitMessage = false;
const relatedChangesLoaded = coreDataPromise.then(() => {
this.getRelatedChangesList()?.reload();
if (this._isNewChangeSummaryUiEnabled) {
let relatedChangesPromise:
| Promise<RelatedChangesInfo | undefined>
| undefined;
const patchNum = this._computeLatestPatchNum(this._allPatchSets);
if (this._change && patchNum) {
relatedChangesPromise = this.restApiService
.getRelatedChanges(this._change._number, patchNum)
.then(response => {
if (this._change && response) {
this.hasParent = this._calculateHasParent(
this._change.change_id,
response.changes
);
}
return response;
});
}
// TODO: use returned Promise
this.getRelatedChangesListExperimental()?.reload(
relatedChangesPromise
);
let relatedChangesPromise:
| Promise<RelatedChangesInfo | undefined>
| undefined;
const patchNum = this._computeLatestPatchNum(this._allPatchSets);
if (this._change && patchNum) {
relatedChangesPromise = this.restApiService
.getRelatedChanges(this._change._number, patchNum)
.then(response => {
if (this._change && response) {
this.hasParent = this._calculateHasParent(
this._change.change_id,
response.changes
);
}
return response;
});
}
// TODO: use returned Promise
this.getRelatedChangesList()?.reload(relatedChangesPromise);
});
allDataPromises.push(relatedChangesLoaded);
}
@@ -2383,15 +2354,6 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
return collapsible && collapsed;
}
_computeRelatedChangesClass(collapsed: boolean) {
return collapsed ? 'collapsed' : '';
}
_computeCollapseText(collapsed: boolean) {
// Symbols are up and down triangles.
return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
}
/**
* Returns the text to be copied when
* click the copy icon next to change subject
@@ -2411,13 +2373,6 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
}
}
_toggleRelatedChangesCollapsed() {
this._relatedChangesCollapsed = !this._relatedChangesCollapsed;
if (this._relatedChangesCollapsed) {
window.scrollTo(0, 0);
}
}
_computeCommitCollapsible(commitMessage?: string) {
if (!commitMessage) {
return false;
@@ -2428,124 +2383,6 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
return commitMessage.split('\n').length >= MIN_LINES;
}
_getOffsetHeight(element: HTMLElement) {
return element.offsetHeight;
}
_getScrollHeight(element: HTMLElement) {
return element.scrollHeight;
}
/**
* Get the line height of an element to the nearest integer.
*/
_getLineHeight(element: Element) {
const lineHeightStr = getComputedStyle(element).lineHeight;
return Math.round(Number(lineHeightStr.slice(0, lineHeightStr.length - 2)));
}
/**
* New max height for the related changes section, shorter than the existing
* change info height.
*/
_updateRelatedChangeMaxHeight() {
// Takes into account approximate height for the expand button and
// bottom margin.
const EXTRA_HEIGHT = 30;
let newHeight;
if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`).matches) {
// In a small (mobile) view, give the relation chain some space.
newHeight = SMALL_RELATED_HEIGHT;
} else if (
window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`).matches
) {
// Since related changes are below the commit message, but still next to
// metadata, the height should be the height of the metadata minus the
// height of the commit message to reduce jank. However, if that doesn't
// result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT.
// Note: extraHeight is to take into account margin/padding.
const medRelatedHeight = Math.max(
this._getOffsetHeight(this.$.mainChangeInfo) -
this._getOffsetHeight(this.$.commitMessage) -
2 * EXTRA_HEIGHT,
MINIMUM_RELATED_MAX_HEIGHT
);
newHeight = medRelatedHeight;
} else {
if (this._commitCollapsible) {
// Make sure the content is lined up if both areas have buttons. If
// the commit message is not collapsed, instead use the change info
// height.
newHeight = this._getOffsetHeight(this.$.commitMessage);
} else {
newHeight =
this._getOffsetHeight(this.$.commitAndRelated) - EXTRA_HEIGHT;
}
}
const stylesToUpdate: {[key: string]: string} = {};
const relatedChanges = this.getRelatedChangesList();
// Get the line height of related changes, and convert it to the nearest
// integer.
const DEFAULT_LINE_HEIGHT = 20;
const lineHeight = relatedChanges
? this._getLineHeight(relatedChanges)
: DEFAULT_LINE_HEIGHT;
// Figure out a new height that is divisible by the rounded line height.
const remainder = newHeight % lineHeight;
newHeight = newHeight - remainder;
stylesToUpdate['--relation-chain-max-height'] = `${newHeight}px`;
// Update the max-height of the relation chain to this new height.
if (this._commitCollapsible) {
stylesToUpdate['--related-change-btn-top-padding'] = `${remainder}px`;
}
this.updateStyles(stylesToUpdate);
}
_computeShowRelatedToggle() {
// Make sure the max height has been applied, since there is now content
// to populate.
if (!getComputedStyleValue('--relation-chain-max-height', this)) {
this._updateRelatedChangeMaxHeight();
}
// Prevents showMore from showing when click on related change, since the
// line height would be positive, but related changes height is 0.
const relatedChanges = this.getRelatedChangesList();
if (relatedChanges) {
if (!this._getScrollHeight(relatedChanges)) {
return (this._showRelatedToggle = false);
}
if (
this._getScrollHeight(relatedChanges) >
this._getOffsetHeight(relatedChanges) +
this._getLineHeight(relatedChanges)
) {
return (this._showRelatedToggle = true);
}
}
return (this._showRelatedToggle = false);
}
_updateToggleContainerClass(showRelatedToggle: boolean) {
const relatedChangesToggle = this.shadowRoot!.querySelector<HTMLDivElement>(
'#relatedChangesToggle'
);
if (!relatedChangesToggle) {
return;
}
if (showRelatedToggle) {
relatedChangesToggle.classList.add('showToggle');
} else {
relatedChangesToggle.classList.remove('showToggle');
}
}
_startUpdateCheckTimer() {
if (
!this._serverConfig ||
@@ -2822,12 +2659,6 @@ export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
'#relatedChanges'
);
}
getRelatedChangesListExperimental() {
return this.shadowRoot!.querySelector<GrRelatedChangesListExperimental>(
'#relatedChangesExperimental'
);
}
}
declare global {

View File

@@ -165,12 +165,6 @@ export const htmlTemplate = html`
height: 0;
margin-bottom: var(--spacing-l);
}
#relatedChanges.collapsed {
margin-bottom: var(--spacing-l);
max-height: var(--relation-chain-max-height, 2em);
overflow: hidden;
position: relative; /* for arrowToCurrentChange to have position:absolute and be hidden */
}
.emptySpace {
flex-grow: 1;
}
@@ -185,19 +179,9 @@ export const htmlTemplate = html`
display: flex;
margin-bottom: 8px;
}
#relatedChangesToggle {
display: none;
}
#relatedChangesToggle.showToggle {
display: flex;
}
.collapseToggleContainer gr-button {
display: block;
}
#relatedChangesToggle {
margin-left: var(--spacing-l);
padding-top: var(--related-change-btn-top-padding, 0);
}
.showOnEdit {
display: none;
}
@@ -243,8 +227,6 @@ export const htmlTemplate = html`
/* temporary for old checks status */
margin-bottom: var(--spacing-m);
}
/* NOTE: If you update this breakpoint, also update the
BREAKPOINT_RELATED_MED in the JS */
@media screen and (max-width: 75em) {
.relatedChanges {
padding: 0;
@@ -266,8 +248,6 @@ export const htmlTemplate = html`
padding-right: 0;
}
}
/* NOTE: If you update this breakpoint, also update the
BREAKPOINT_RELATED_SMALL in the JS */
@media screen and (max-width: 50em) {
.mobile {
display: block;
@@ -533,36 +513,11 @@ export const htmlTemplate = html`
</gr-endpoint-decorator>
</div>
<div class="relatedChanges">
<template is="dom-if" if="[[_isNewChangeSummaryUiEnabled]]">
<gr-related-changes-list-experimental
change="[[_change]]"
id="relatedChangesExperimental"
patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
></gr-related-changes-list-experimental>
</template>
<template is="dom-if" if="[[!_isNewChangeSummaryUiEnabled]]">
<gr-related-changes-list
id="relatedChanges"
class$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]"
change="[[_change]]"
mergeable="[[_mergeable]]"
has-parent="{{hasParent}}"
on-update="_updateRelatedChangeMaxHeight"
patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
on-new-section-loaded="_computeShowRelatedToggle"
>
</gr-related-changes-list>
<div id="relatedChangesToggle" class="collapseToggleContainer">
<gr-button
link=""
id="relatedChangesToggleButton"
class="collapseToggleButton"
on-click="_toggleRelatedChangesCollapsed"
>
[[_computeCollapseText(_relatedChangesCollapsed)]]
</gr-button>
</div>
</template>
<gr-related-changes-list
change="[[_change]]"
id="relatedChanges"
patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
></gr-related-changes-list>
</div>
<div class="emptySpace"></div>
</div>

View File

@@ -29,7 +29,6 @@ import {
} from '../../../constants/constants';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
import {getComputedStyleValue} from '../../../utils/dom-util';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
@@ -38,7 +37,6 @@ import {EventType, PluginApi} from '../../../api/plugin';
import 'lodash/lodash';
import {
stubRestApi,
SinonSpyMember,
TestKeyboardShortcutBinder,
} from '../../../test/test-utils';
import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -46,7 +44,6 @@ import {
createAppElementChangeViewParams,
createApproval,
createChange,
createChangeConfig,
createChangeMessages,
createCommit,
createMergeable,
@@ -406,9 +403,6 @@ suite('gr-change-view tests', () => {
});
});
const getCustomCssValue = (cssParam: string) =>
getComputedStyleValue(cssParam, element);
test('_handleMessageAnchorTap', () => {
element._changeNum = 1 as NumericChangeId;
element._patchRange = {
@@ -1516,10 +1510,6 @@ suite('gr-change-view tests', () => {
.stub(element, '_reloadPatchNumDependentResources')
.callsFake(() => Promise.resolve([undefined, undefined, undefined]));
flush();
const relatedChanges = element.shadowRoot!.querySelector(
'#relatedChanges'
) as GrRelatedChangesList;
const relatedClearSpy = sinon.spy(relatedChanges, 'clear');
const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
const value: AppElementChangeViewParams = {
@@ -1529,7 +1519,6 @@ suite('gr-change-view tests', () => {
};
element._paramsChanged(value);
assert.isTrue(reloadStub.calledOnce);
assert.isTrue(relatedClearSpy.calledOnce);
element._initialLoadComplete = true;
@@ -1538,7 +1527,6 @@ suite('gr-change-view tests', () => {
element._paramsChanged(value);
assert.isFalse(reloadStub.calledTwice);
assert.isTrue(reloadPatchDependentStub.calledOnce);
assert.isTrue(relatedClearSpy.calledOnce);
assert.isTrue(collapseStub.calledTwice);
});
@@ -1551,10 +1539,6 @@ suite('gr-change-view tests', () => {
element.$.commentAPI,
'reloadPortedComments'
);
const relatedChanges = element.shadowRoot!.querySelector(
'#relatedChanges'
) as GrRelatedChangesList;
sinon.spy(relatedChanges, 'clear');
sinon.stub(element.$.fileList, 'collapseAllDiffs');
const value: AppElementChangeViewParams = {
@@ -2180,322 +2164,6 @@ suite('gr-change-view tests', () => {
});
});
suite('related changes expand/collapse', () => {
let updateHeightSpy: SinonSpyMember<
typeof element._updateRelatedChangeMaxHeight
>;
setup(() => {
updateHeightSpy = sinon.spy(element, '_updateRelatedChangeMaxHeight');
});
test('relatedChangesToggle shown height greater than changeInfo height', () => {
const relatedChangesToggle = element.shadowRoot!.querySelector(
'#relatedChangesToggle'
);
assert.isFalse(relatedChangesToggle!.classList.contains('showToggle'));
sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
sinon.stub(element, '_getScrollHeight').callsFake(() => 60);
sinon.stub(element, '_getLineHeight').callsFake(() => 5);
sinon
.stub(window, 'matchMedia')
.callsFake(() => ({matches: true} as MediaQueryList));
const relatedChanges = element.shadowRoot!.querySelector(
'#relatedChanges'
) as GrRelatedChangesList;
relatedChanges.dispatchEvent(new CustomEvent('new-section-loaded'));
assert.isTrue(relatedChangesToggle!.classList.contains('showToggle'));
assert.equal(updateHeightSpy.callCount, 1);
});
test('relatedChangesToggle hidden height less than changeInfo height', () => {
const relatedChangesToggle = element.shadowRoot!.querySelector(
'#relatedChangesToggle'
);
assert.isFalse(relatedChangesToggle!.classList.contains('showToggle'));
sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
sinon.stub(element, '_getScrollHeight').callsFake(() => 40);
sinon.stub(element, '_getLineHeight').callsFake(() => 5);
sinon
.stub(window, 'matchMedia')
.callsFake(() => ({matches: true} as MediaQueryList));
const relatedChanges = element.shadowRoot!.querySelector(
'#relatedChanges'
) as GrRelatedChangesList;
relatedChanges.dispatchEvent(new CustomEvent('new-section-loaded'));
assert.isFalse(relatedChangesToggle!.classList.contains('showToggle'));
assert.equal(updateHeightSpy.callCount, 1);
});
test('relatedChangesToggle functions', () => {
sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
sinon
.stub(window, 'matchMedia')
.callsFake(() => ({matches: false} as MediaQueryList));
assert.isTrue(element._relatedChangesCollapsed);
const relatedChangesToggleButton = element.shadowRoot!.querySelector(
'#relatedChangesToggleButton'
);
const relatedChanges = element.shadowRoot!.querySelector(
'#relatedChanges'
) as GrRelatedChangesList;
assert.isTrue(relatedChanges.classList.contains('collapsed'));
tap(relatedChangesToggleButton!);
assert.isFalse(element._relatedChangesCollapsed);
assert.isFalse(relatedChanges.classList.contains('collapsed'));
});
test('_updateRelatedChangeMaxHeight without commit toggle', () => {
sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
sinon.stub(element, '_getLineHeight').callsFake(() => 12);
sinon
.stub(window, 'matchMedia')
.callsFake(() => ({matches: false} as MediaQueryList));
// 50 (existing height) - 30 (extra height) = 20 (adjusted height).
// 20 (max existing height) % 12 (line height) = 6 (remainder).
// 20 (adjusted height) - 8 (remainder) = 12 (max height to set).
element._updateRelatedChangeMaxHeight();
assert.equal(getCustomCssValue('--relation-chain-max-height'), '12px');
assert.equal(getCustomCssValue('--related-change-btn-top-padding'), '');
});
test('_updateRelatedChangeMaxHeight with commit toggle', () => {
element._latestCommitMessage = _.times(31, String).join('\n');
sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
sinon.stub(element, '_getLineHeight').callsFake(() => 12);
sinon
.stub(window, 'matchMedia')
.callsFake(() => ({matches: false} as MediaQueryList));
// 50 (existing height) % 12 (line height) = 2 (remainder).
// 50 (existing height) - 2 (remainder) = 48 (max height to set).
element._updateRelatedChangeMaxHeight();
assert.equal(getCustomCssValue('--relation-chain-max-height'), '48px');
assert.equal(
getCustomCssValue('--related-change-btn-top-padding'),
'2px'
);
});
test('_updateRelatedChangeMaxHeight in small screen mode', () => {
element._latestCommitMessage = _.times(31, String).join('\n');
sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
sinon.stub(element, '_getLineHeight').callsFake(() => 12);
sinon
.stub(window, 'matchMedia')
.callsFake(() => ({matches: true} as MediaQueryList));
element._updateRelatedChangeMaxHeight();
// 400 (new height) % 12 (line height) = 4 (remainder).
// 400 (new height) - 4 (remainder) = 396.
assert.equal(getCustomCssValue('--relation-chain-max-height'), '396px');
});
test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
element._latestCommitMessage = _.times(31, String).join('\n');
sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
sinon.stub(element, '_getLineHeight').callsFake(() => 12);
const matchMediaStub = sinon.stub(window, 'matchMedia').callsFake(() => {
if (matchMediaStub.lastCall.args[0] === '(max-width: 75em)') {
return {matches: true} as MediaQueryList;
} else {
return {matches: false} as MediaQueryList;
}
});
// 100 (new height) % 12 (line height) = 4 (remainder).
// 100 (new height) - 4 (remainder) = 96.
element._updateRelatedChangeMaxHeight();
assert.equal(getCustomCssValue('--relation-chain-max-height'), '96px');
});
suite('update checks', () => {
let clock: SinonFakeTimers;
let startUpdateCheckTimerSpy: SinonSpyMember<
typeof element._startUpdateCheckTimer
>;
setup(() => {
clock = sinon.useFakeTimers();
startUpdateCheckTimerSpy = sinon.spy(element, '_startUpdateCheckTimer');
element._change = {
...createChangeViewChange(),
revisions: createRevisions(1),
messages: createChangeMessages(1),
};
});
test('_startUpdateCheckTimer negative delay', () => {
const getChangeDetailStub = stubRestApi('getChangeDetail').returns(
Promise.resolve({
...createChangeViewChange(),
// element has latest info
revisions: {rev1: createRevision()},
messages: createChangeMessages(1),
current_revision: 'rev1' as CommitId,
})
);
element._serverConfig = {
...createServerInfo(),
change: {...createChangeConfig(), update_delay: -1},
};
assert.isTrue(startUpdateCheckTimerSpy.called);
assert.isFalse(getChangeDetailStub.called);
});
test('_startUpdateCheckTimer up-to-date', async () => {
const getChangeDetailStub = stubRestApi('getChangeDetail').callsFake(
() =>
Promise.resolve({
...createChangeViewChange(),
// element has latest info
revisions: {rev1: createRevision()},
messages: createChangeMessages(1),
current_revision: 'rev1' as CommitId,
})
);
element._serverConfig = {
...createServerInfo(),
change: {...createChangeConfig(), update_delay: 12345},
};
clock.tick(12345 * 1000);
await flush();
assert.equal(startUpdateCheckTimerSpy.callCount, 2);
assert.isTrue(getChangeDetailStub.called);
});
test('_startUpdateCheckTimer out-of-date shows an alert', async () => {
stubRestApi('getChangeDetail').callsFake(() =>
Promise.resolve({
...createChange(),
// new patchset was uploaded
revisions: createRevisions(2),
current_revision: getCurrentRevision(2),
messages: createChangeMessages(1),
})
);
let alertMessage = 'alert not fired';
element.addEventListener('show-alert', e => {
alertMessage = e.detail.message;
});
element._serverConfig = {
...createServerInfo(),
change: {...createChangeConfig(), update_delay: 12345},
};
clock.tick(12345 * 1000);
await flush();
assert.equal(alertMessage, 'A newer patch set has been uploaded');
assert.equal(startUpdateCheckTimerSpy.callCount, 1);
});
test('_startUpdateCheckTimer respects _loading', async () => {
stubRestApi('getChangeDetail').callsFake(() =>
Promise.resolve({
...createChangeViewChange(),
// new patchset was uploaded
revisions: createRevisions(2),
current_revision: getCurrentRevision(2),
messages: createChangeMessages(1),
})
);
element._loading = true;
element._serverConfig = {
...createServerInfo(),
change: {...createChangeConfig(), update_delay: 12345},
};
clock.tick(12345 * 1000 * 2);
await flush();
// No toast, instead a second call to _startUpdateCheckTimer().
assert.equal(startUpdateCheckTimerSpy.callCount, 2);
});
test('_startUpdateCheckTimer new status shows an alert', async () => {
stubRestApi('getChangeDetail').callsFake(() =>
Promise.resolve({
...createChangeViewChange(),
// element has latest info
revisions: {rev1: createRevision()},
messages: createChangeMessages(1),
current_revision: 'rev1' as CommitId,
status: ChangeStatus.MERGED,
})
);
let alertMessage = 'alert not fired';
element.addEventListener('show-alert', e => {
alertMessage = e.detail.message;
});
element._serverConfig = {
...createServerInfo(),
change: {...createChangeConfig(), update_delay: 12345},
};
clock.tick(12345 * 1000);
await flush();
assert.equal(alertMessage, 'This change has been merged');
});
test('_startUpdateCheckTimer new messages shows an alert', async () => {
stubRestApi('getChangeDetail').callsFake(() =>
Promise.resolve({
...createChangeViewChange(),
revisions: {rev1: createRevision()},
// element has new message
messages: createChangeMessages(2),
current_revision: 'rev1' as CommitId,
})
);
let alertMessage = 'alert not fired';
element.addEventListener('show-alert', e => {
alertMessage = e.detail.message;
});
element._serverConfig = {
...createServerInfo(),
change: {...createChangeConfig(), update_delay: 12345},
};
clock.tick(12345 * 1000);
await flush();
assert.equal(alertMessage, 'There are new messages on this change');
});
});
test('canStartReview computation', () => {
const change1: ChangeInfo = createChange();
const change2: ChangeInfo = {
...createChangeViewChange(),
actions: {
ready: {
enabled: true,
},
},
};
const change3: ChangeInfo = {
...createChangeViewChange(),
actions: {
ready: {
label: 'Ready for Review',
},
},
};
assert.isFalse(element._computeCanStartReview(change1));
assert.isTrue(element._computeCanStartReview(change2));
assert.isFalse(element._computeCanStartReview(change3));
});
});
test('header class computation', () => {
assert.equal(element._computeHeaderClass(), 'header');
assert.equal(element._computeHeaderClass(true), 'header editMode');

View File

@@ -1,754 +0,0 @@
/**
* @license
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html, nothing} from 'lit-html';
import './gr-related-change';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
import {classMap} from 'lit-html/directives/class-map';
import {GrLitElement} from '../../lit/gr-lit-element';
import {
customElement,
property,
css,
internalProperty,
TemplateResult,
} from 'lit-element';
import {sharedStyles} from '../../../styles/shared-styles';
import {
SubmittedTogetherInfo,
ChangeInfo,
RelatedChangeAndCommitInfo,
RelatedChangesInfo,
PatchSetNum,
CommitId,
} from '../../../types/common';
import {appContext} from '../../../services/app-context';
import {ParsedChangeInfo} from '../../../types/types';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {pluralize} from '../../../utils/string-util';
import {
changeIsOpen,
getRevisionKey,
isChangeInfo,
} from '../../../utils/change-util';
/** What is the maximum number of shown changes in collapsed list? */
const DEFALT_NUM_CHANGES_WHEN_COLLAPSED = 3;
export interface ChangeMarkersInList {
showCurrentChangeArrow: boolean;
showWhenCollapsed: boolean;
showTopArrow: boolean;
showBottomArrow: boolean;
}
export enum Section {
RELATED_CHANGES = 'related changes',
SUBMITTED_TOGETHER = 'submitted together',
SAME_TOPIC = 'same topic',
MERGE_CONFLICTS = 'merge conflicts',
CHERRY_PICKS = 'cherry picks',
}
@customElement('gr-related-changes-list-experimental')
export class GrRelatedChangesListExperimental extends GrLitElement {
@property()
change?: ParsedChangeInfo;
@property({type: String})
patchNum?: PatchSetNum;
@property()
mergeable?: boolean;
@internalProperty()
submittedTogether?: SubmittedTogetherInfo = {
changes: [],
non_visible_changes: 0,
};
@internalProperty()
relatedChanges: RelatedChangeAndCommitInfo[] = [];
@internalProperty()
conflictingChanges: ChangeInfo[] = [];
@internalProperty()
cherryPickChanges: ChangeInfo[] = [];
@internalProperty()
sameTopicChanges: ChangeInfo[] = [];
private readonly restApiService = appContext.restApiService;
static get styles() {
return [
sharedStyles,
css`
.note {
color: var(--error-text-color);
margin-left: 1.2em;
}
section {
margin-bottom: var(--spacing-l);
}
gr-related-change {
display: flex;
}
.marker {
position: absolute;
margin-left: calc(-1 * var(--spacing-s));
}
.arrowToCurrentChange {
position: absolute;
}
`,
];
}
render() {
const sectionSize = this.sectionSizeFactory(
this.relatedChanges.length,
this.submittedTogether?.changes.length || 0,
this.sameTopicChanges.length,
this.conflictingChanges.length,
this.cherryPickChanges.length
);
const relatedChangesMarkersPredicate = this.markersPredicateFactory(
this.relatedChanges.length,
this.relatedChanges.findIndex(relatedChange =>
this._changesEqual(relatedChange, this.change)
),
sectionSize(Section.RELATED_CHANGES)
);
const connectedRevisions = this._computeConnectedRevisions(
this.change,
this.patchNum,
this.relatedChanges
);
let firstNonEmptySectionFound = false;
let isFirstNonEmpty =
!firstNonEmptySectionFound && !!this.relatedChanges.length;
firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
const relatedChangeSection = html` <section
id="relatedChanges"
?hidden=${!this.relatedChanges.length}
>
<gr-related-collapse
title="Relation chain"
class="${classMap({first: isFirstNonEmpty})}"
.length=${this.relatedChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.RELATED_CHANGES)}
>
${this.relatedChanges.map(
(change, index) =>
html`${this.renderMarkers(
relatedChangesMarkersPredicate(index)
)}<gr-related-change
class="${classMap({
['show-when-collapsed']: relatedChangesMarkersPredicate(index)
.showWhenCollapsed,
})}"
.change="${change}"
.connectedRevisions="${connectedRevisions}"
.href="${change?._change_number
? GerritNav.getUrlForChangeById(
change._change_number,
change.project,
change._revision_number as PatchSetNum
)
: ''}"
.showChangeStatus=${true}
>${change.commit.subject}</gr-related-change
>`
)}
</gr-related-collapse>
</section>`;
const submittedTogetherChanges = this.submittedTogether?.changes ?? [];
const countNonVisibleChanges =
this.submittedTogether?.non_visible_changes ?? 0;
const submittedTogetherMarkersPredicate = this.markersPredicateFactory(
submittedTogetherChanges.length,
submittedTogetherChanges.findIndex(relatedChange =>
this._changesEqual(relatedChange, this.change)
),
sectionSize(Section.SUBMITTED_TOGETHER)
);
isFirstNonEmpty =
!firstNonEmptySectionFound &&
(!!submittedTogetherChanges?.length ||
!!this.submittedTogether?.non_visible_changes);
firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
const submittedTogetherSection = html`<section
id="submittedTogether"
?hidden=${!submittedTogetherChanges?.length &&
!this.submittedTogether?.non_visible_changes}
>
<gr-related-collapse
title="Submitted together"
class="${classMap({first: isFirstNonEmpty})}"
.length=${submittedTogetherChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SUBMITTED_TOGETHER)}
>
${submittedTogetherChanges.map(
(change, index) =>
html`${this.renderMarkers(
submittedTogetherMarkersPredicate(index)
)}<gr-related-change
class="${classMap({
['show-when-collapsed']: submittedTogetherMarkersPredicate(
index
).showWhenCollapsed,
})}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
.showSubmittableCheck=${true}
>${change.project}: ${change.branch}:
${change.subject}</gr-related-change
>`
)}
</gr-related-collapse>
<div class="note" ?hidden=${!countNonVisibleChanges}>
(+ ${pluralize(countNonVisibleChanges, 'non-visible change')})
</div>
</section>`;
const sameTopicMarkersPredicate = this.markersPredicateFactory(
this.sameTopicChanges.length,
-1,
sectionSize(Section.SAME_TOPIC)
);
isFirstNonEmpty =
!firstNonEmptySectionFound && !!this.sameTopicChanges?.length;
firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
const sameTopicSection = html`<section
id="sameTopic"
?hidden=${!this.sameTopicChanges?.length}
>
<gr-related-collapse
title="Same topic"
class="${classMap({first: isFirstNonEmpty})}"
.length=${this.sameTopicChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SAME_TOPIC)}
>
${this.sameTopicChanges.map(
(change, index) =>
html`${this.renderMarkers(
sameTopicMarkersPredicate(index)
)}<gr-related-change
class="${classMap({
['show-when-collapsed']: sameTopicMarkersPredicate(index)
.showWhenCollapsed,
})}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
>${change.project}: ${change.branch}:
${change.subject}</gr-related-change
>`
)}
</gr-related-collapse>
</section>`;
const mergeConflictsMarkersPredicate = this.markersPredicateFactory(
this.conflictingChanges.length,
-1,
sectionSize(Section.MERGE_CONFLICTS)
);
isFirstNonEmpty =
!firstNonEmptySectionFound && !!this.conflictingChanges?.length;
firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
const mergeConflictsSection = html`<section
id="mergeConflicts"
?hidden=${!this.conflictingChanges?.length}
>
<gr-related-collapse
title="Merge conflicts"
class="${classMap({first: isFirstNonEmpty})}"
.length=${this.conflictingChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.MERGE_CONFLICTS)}
>
${this.conflictingChanges.map(
(change, index) =>
html`${this.renderMarkers(
mergeConflictsMarkersPredicate(index)
)}<gr-related-change
class="${classMap({
['show-when-collapsed']: mergeConflictsMarkersPredicate(index)
.showWhenCollapsed,
})}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
>${change.subject}</gr-related-change
>`
)}
</gr-related-collapse>
</section>`;
const cherryPicksMarkersPredicate = this.markersPredicateFactory(
this.cherryPickChanges.length,
-1,
sectionSize(Section.CHERRY_PICKS)
);
isFirstNonEmpty =
!firstNonEmptySectionFound && !!this.cherryPickChanges?.length;
firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
const cherryPicksSection = html`<section
id="cherryPicks"
?hidden=${!this.cherryPickChanges?.length}
>
<gr-related-collapse
title="Cherry picks"
class="${classMap({first: isFirstNonEmpty})}"
.length=${this.cherryPickChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.CHERRY_PICKS)}
>
${this.cherryPickChanges.map(
(change, index) =>
html`${this.renderMarkers(
cherryPicksMarkersPredicate(index)
)}<gr-related-change
class="${classMap({
['show-when-collapsed']: cherryPicksMarkersPredicate(index)
.showWhenCollapsed,
})}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
>${change.branch}: ${change.subject}</gr-related-change
>`
)}
</gr-related-collapse>
</section>`;
return html`<gr-endpoint-decorator name="related-changes-section">
<gr-endpoint-param
name="change"
.value=${this.change}
></gr-endpoint-param>
<gr-endpoint-slot name="top"></gr-endpoint-slot>
${relatedChangeSection} ${submittedTogetherSection} ${sameTopicSection}
${mergeConflictsSection} ${cherryPicksSection}
<gr-endpoint-slot name="bottom"></gr-endpoint-slot>
</gr-endpoint-decorator>`;
}
sectionSizeFactory(
relatedChangesLen: number,
submittedTogetherLen: number,
sameTopicLen: number,
mergeConflictsLen: number,
cherryPicksLen: number
) {
const calcDefaultSize = (length: number) =>
Math.min(length, DEFALT_NUM_CHANGES_WHEN_COLLAPSED);
const sectionSizes = [
{
section: Section.RELATED_CHANGES,
size: calcDefaultSize(relatedChangesLen),
len: relatedChangesLen,
},
{
section: Section.SUBMITTED_TOGETHER,
size: calcDefaultSize(submittedTogetherLen),
len: submittedTogetherLen,
},
{
section: Section.SAME_TOPIC,
size: calcDefaultSize(sameTopicLen),
len: sameTopicLen,
},
{
section: Section.MERGE_CONFLICTS,
size: calcDefaultSize(mergeConflictsLen),
len: mergeConflictsLen,
},
{
section: Section.CHERRY_PICKS,
size: calcDefaultSize(cherryPicksLen),
len: cherryPicksLen,
},
];
const FILLER = 1; // space for header
let totalSize = sectionSizes.reduce(
(acc, val) => acc + val.size + (val.size !== 0 ? FILLER : 0),
0
);
const MAX_SIZE = 16;
for (let i = 0; i < sectionSizes.length; i++) {
if (totalSize >= MAX_SIZE) break;
const sizeObj = sectionSizes[i];
if (sizeObj.size === sizeObj.len) continue;
const newSize = Math.min(
MAX_SIZE - totalSize + sizeObj.size,
sizeObj.len
);
totalSize += newSize - sizeObj.size;
sizeObj.size = newSize;
}
return (section: Section) => {
const sizeObj = sectionSizes.find(sizeObj => sizeObj.section === section);
if (sizeObj) return sizeObj.size;
return DEFALT_NUM_CHANGES_WHEN_COLLAPSED;
};
}
markersPredicateFactory(
length: number,
highlightIndex: number,
numChangesShownWhenCollapsed = DEFALT_NUM_CHANGES_WHEN_COLLAPSED
): (index: number) => ChangeMarkersInList {
const showWhenCollapsedPredicate = (index: number) => {
if (highlightIndex === -1) return index < numChangesShownWhenCollapsed;
if (highlightIndex === 0)
return index <= numChangesShownWhenCollapsed - 1;
if (highlightIndex === length - 1)
return index >= length - numChangesShownWhenCollapsed;
let numBeforeHighlight = Math.floor(numChangesShownWhenCollapsed / 2);
let numAfterHighlight =
Math.floor(numChangesShownWhenCollapsed / 2) -
(numChangesShownWhenCollapsed % 2 ? 0 : 1);
numBeforeHighlight += Math.max(
highlightIndex + numAfterHighlight - length + 1,
0
);
numAfterHighlight -= Math.min(0, highlightIndex - numBeforeHighlight);
return (
highlightIndex - numBeforeHighlight <= index &&
index <= highlightIndex + numAfterHighlight
);
};
return (index: number) => {
return {
showCurrentChangeArrow:
highlightIndex !== -1 && index === highlightIndex,
showWhenCollapsed: showWhenCollapsedPredicate(index),
showTopArrow:
index >= 1 &&
index !== highlightIndex &&
showWhenCollapsedPredicate(index) &&
!showWhenCollapsedPredicate(index - 1),
showBottomArrow:
index <= length - 2 &&
index !== highlightIndex &&
showWhenCollapsedPredicate(index) &&
!showWhenCollapsedPredicate(index + 1),
};
};
}
renderMarkers(changeMarkers: ChangeMarkersInList) {
if (changeMarkers.showCurrentChangeArrow) {
return html`<span
role="img"
class="arrowToCurrentChange"
aria-label="Arrow marking current change"
>➔</span
>`;
}
if (changeMarkers.showTopArrow) {
return html`<span
role="img"
class="marker"
aria-label="Arrow marking change has collapsed ancestors"
><iron-icon icon="gr-icons:arrowDropUp"></iron-icon
></span> `;
}
if (changeMarkers.showBottomArrow) {
return html`<span
role="img"
class="marker"
aria-label="Arrow marking change has collapsed descendants"
><iron-icon icon="gr-icons:arrowDropDown"></iron-icon
></span> `;
}
return nothing;
}
reload(getRelatedChanges?: Promise<RelatedChangesInfo | undefined>) {
const change = this.change;
if (!change) return Promise.reject(new Error('change missing'));
if (!this.patchNum) return Promise.reject(new Error('patchNum missing'));
if (!getRelatedChanges) {
getRelatedChanges = this.restApiService.getRelatedChanges(
change._number,
this.patchNum
);
}
const promises: Array<Promise<void>> = [
getRelatedChanges.then(response => {
if (!response) {
throw new Error('getRelatedChanges returned undefined response');
}
this.relatedChanges = response?.changes ?? [];
}),
this.restApiService
.getChangesSubmittedTogether(change._number)
.then(response => {
this.submittedTogether = response;
}),
this.restApiService
.getChangeCherryPicks(change.project, change.change_id, change._number)
.then(response => {
this.cherryPickChanges = response || [];
}),
];
// Get conflicts if change is open and is mergeable.
// Mergeable is output of restApiServict.getMergeable from gr-change-view
if (changeIsOpen(change) && this.mergeable) {
promises.push(
this.restApiService
.getChangeConflicts(change._number)
.then(response => {
this.conflictingChanges = response ?? [];
})
);
}
if (change.topic) {
const changeTopic = change.topic;
promises.push(
this.restApiService.getConfig().then(config => {
if (config && !config.change.submit_whole_topic) {
return this.restApiService
.getChangesWithSameTopic(changeTopic, change._number)
.then(response => {
if (changeTopic === this.change?.topic) {
this.sameTopicChanges = response ?? [];
}
});
}
this.sameTopicChanges = [];
return Promise.resolve();
})
);
}
return Promise.all(promises);
}
/**
* Do the given objects describe the same change? Compares the changes by
* their numbers.
*/
_changesEqual(
a?: ChangeInfo | RelatedChangeAndCommitInfo,
b?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
) {
const aNum = this._getChangeNumber(a);
const bNum = this._getChangeNumber(b);
return aNum === bNum;
}
/**
* Get the change number from either a ChangeInfo (such as those included in
* SubmittedTogetherInfo responses) or get the change number from a
* RelatedChangeAndCommitInfo (such as those included in a
* RelatedChangesInfo response).
*/
_getChangeNumber(
change?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
) {
// Default to 0 if change property is not defined.
if (!change) return 0;
if (isChangeInfo(change)) {
return change._number;
}
return change._change_number;
}
/*
* A list of commit ids connected to change to understand if other change
* is direct or indirect ancestor / descendant.
*/
_computeConnectedRevisions(
change?: ParsedChangeInfo,
patchNum?: PatchSetNum,
relatedChanges?: RelatedChangeAndCommitInfo[]
) {
if (!patchNum || !relatedChanges || !change) {
return [];
}
const connected: CommitId[] = [];
const changeRevision = getRevisionKey(change, patchNum);
const commits = relatedChanges.map(c => c.commit);
let pos = commits.length - 1;
while (pos >= 0) {
const commit: CommitId = commits[pos].commit;
connected.push(commit);
// TODO(TS): Ensure that both (commit and changeRevision) are string and use === instead
// eslint-disable-next-line eqeqeq
if (commit == changeRevision) {
break;
}
pos--;
}
while (pos >= 0) {
for (let i = 0; i < commits[pos].parents.length; i++) {
if (connected.includes(commits[pos].parents[i].commit)) {
connected.push(commits[pos].commit);
break;
}
}
--pos;
}
return connected;
}
}
@customElement('gr-related-collapse')
export class GrRelatedCollapse extends GrLitElement {
@property()
title = '';
@property()
showAll = false;
@property()
length = 0;
@property()
numChangesWhenCollapsed = DEFALT_NUM_CHANGES_WHEN_COLLAPSED;
private readonly reporting = appContext.reportingService;
static get styles() {
return [
sharedStyles,
css`
.title {
font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
}
h4 {
display: flex;
align-self: flex-end;
}
gr-button {
display: flex;
}
/* This is a hacky solution from old gr-related-change-list
* TODO(milutin): find layout without needing it
*/
h4:before,
gr-button:before,
::slotted(gr-related-change):before {
content: ' ';
flex-shrink: 0;
width: 1.2em;
}
.collapsed ::slotted(gr-related-change.show-when-collapsed) {
visibility: visible;
height: auto;
}
.collapsed ::slotted(.marker) {
display: block;
}
.show-all ::slotted(.marker) {
display: none;
}
/* keep width, so width of section and position of show all button
* are set according to width of all (even hidden) elements
*/
.collapsed ::slotted(gr-related-change) {
visibility: hidden;
height: 0px;
}
::slotted(gr-related-change) {
visibility: visible;
height: auto;
}
gr-button iron-icon {
color: inherit;
--iron-icon-height: 18px;
--iron-icon-width: 18px;
}
.container {
justify-content: space-between;
display: flex;
margin-bottom: var(--spacing-s);
}
:host(.first) .container {
margin-bottom: var(--spacing-m);
}
`,
];
}
render() {
const title = html`<h4 class="title">${this.title}</h4>`;
const collapsible = this.length > this.numChangesWhenCollapsed;
const items = html` <div
class="${!this.showAll && collapsible ? 'collapsed' : 'show-all'}"
>
<slot></slot>
</div>`;
let button: TemplateResult | typeof nothing = nothing;
if (collapsible) {
let buttonText = 'Show less';
let buttonIcon = 'expand-less';
if (!this.showAll) {
buttonText = `Show all (${this.length})`;
buttonIcon = 'expand-more';
}
button = html`<gr-button link="" @click="${this.toggle}"
>${buttonText}<iron-icon icon="gr-icons:${buttonIcon}"></iron-icon
></gr-button>`;
}
return html`<div class="container">${title}${button}</div>
${items}`;
}
private toggle(e: MouseEvent) {
e.stopPropagation();
this.showAll = !this.showAll;
this.reporting.reportInteraction('toggle show all button', {
sectionName: this.title,
toState: this.showAll ? 'Show all' : 'Show less',
});
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-related-changes-list-experimental': GrRelatedChangesListExperimental;
'gr-related-collapse': GrRelatedCollapse;
}
}

View File

@@ -1,630 +0,0 @@
/**
* @license
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {SinonStubbedMember} from 'sinon/pkg/sinon-esm';
import {PluginApi} from '../../../api/plugin';
import {ChangeStatus} from '../../../constants/constants';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
import '../../../test/common-test-setup-karma';
import {
createChange,
createCommitInfoWithRequiredCommit,
createParsedChange,
createRelatedChangeAndCommitInfo,
createRelatedChangesInfo,
createRevision,
createSubmittedTogetherInfo,
} from '../../../test/test-data-generators';
import {
queryAndAssert,
resetPlugins,
stubRestApi,
} from '../../../test/test-utils';
import {
ChangeId,
ChangeInfo,
CommitId,
NumericChangeId,
PatchSetNum,
RelatedChangeAndCommitInfo,
RelatedChangesInfo,
SubmittedTogetherInfo,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../types/types';
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import './gr-related-changes-list-experimental';
import {
ChangeMarkersInList,
GrRelatedChangesListExperimental,
GrRelatedCollapse,
Section,
} from './gr-related-changes-list-experimental';
const pluginApi = _testOnly_initGerritPluginApi();
const basicFixture = fixtureFromElement('gr-related-changes-list-experimental');
suite('gr-related-changes-list-experimental', () => {
let element: GrRelatedChangesListExperimental;
setup(() => {
element = basicFixture.instantiate();
});
suite('show when collapsed', () => {
function genBoolArray(
instructions: Array<{
len: number;
v: boolean;
}>
) {
return instructions
.map(inst => Array.from({length: inst.len}, () => inst.v))
.reduce((acc, val) => acc.concat(val), []);
}
function checkShowWhenCollapsed(
expected: boolean[],
markersPredicate: (index: number) => ChangeMarkersInList,
msg: string
) {
for (let i = 0; i < expected.length; i++) {
assert.equal(
markersPredicate(i).showWhenCollapsed,
expected[i],
`change on pos (${i}) ${msg}`
);
}
}
test('size 5', () => {
const markersPredicate = element.markersPredicateFactory(10, 4, 5);
const expectedCollapsing = genBoolArray([
{len: 2, v: false},
{len: 5, v: true},
{len: 3, v: false},
]);
checkShowWhenCollapsed(
expectedCollapsing,
markersPredicate,
'highlight 4, size 10, size 5'
);
const markersPredicate2 = element.markersPredicateFactory(10, 8, 5);
const expectedCollapsing2 = genBoolArray([
{len: 5, v: false},
{len: 5, v: true},
]);
checkShowWhenCollapsed(
expectedCollapsing2,
markersPredicate2,
'highlight 8, size 10, size 5'
);
const markersPredicate3 = element.markersPredicateFactory(10, 1, 5);
const expectedCollapsing3 = genBoolArray([
{len: 5, v: true},
{len: 5, v: false},
]);
checkShowWhenCollapsed(
expectedCollapsing3,
markersPredicate3,
'highlight 1, size 10, size 5'
);
});
test('size 4', () => {
const markersPredicate = element.markersPredicateFactory(10, 4, 4);
const expectedCollapsing = genBoolArray([
{len: 2, v: false},
{len: 4, v: true},
{len: 4, v: false},
]);
checkShowWhenCollapsed(
expectedCollapsing,
markersPredicate,
'highlight 4, len 10, size 4'
);
const markersPredicate2 = element.markersPredicateFactory(10, 8, 4);
const expectedCollapsing2 = genBoolArray([
{len: 6, v: false},
{len: 4, v: true},
]);
checkShowWhenCollapsed(
expectedCollapsing2,
markersPredicate2,
'highlight 8, len 10, size 4'
);
const markersPredicate3 = element.markersPredicateFactory(10, 1, 4);
const expectedCollapsing3 = genBoolArray([
{len: 4, v: true},
{len: 6, v: false},
]);
checkShowWhenCollapsed(
expectedCollapsing3,
markersPredicate3,
'highlight 1, len 10, size 4'
);
});
});
suite('section size', () => {
test('1 section', () => {
const sectionSize = element.sectionSizeFactory(20, 0, 0, 0, 0);
assert.equal(sectionSize(Section.RELATED_CHANGES), 15);
const sectionSize2 = element.sectionSizeFactory(0, 0, 10, 0, 0);
assert.equal(sectionSize2(Section.SAME_TOPIC), 10);
});
test('2 sections', () => {
const sectionSize = element.sectionSizeFactory(20, 20, 0, 0, 0);
assert.equal(sectionSize(Section.RELATED_CHANGES), 11);
assert.equal(sectionSize(Section.SUBMITTED_TOGETHER), 3);
const sectionSize2 = element.sectionSizeFactory(4, 0, 10, 0, 0);
assert.equal(sectionSize2(Section.RELATED_CHANGES), 4);
assert.equal(sectionSize2(Section.SAME_TOPIC), 10);
});
test('many sections', () => {
const sectionSize = element.sectionSizeFactory(20, 20, 3, 3, 3);
assert.equal(sectionSize(Section.RELATED_CHANGES), 3);
assert.equal(sectionSize(Section.SUBMITTED_TOGETHER), 3);
const sectionSize2 = element.sectionSizeFactory(4, 1, 10, 1, 1);
assert.equal(sectionSize2(Section.RELATED_CHANGES), 4);
assert.equal(sectionSize2(Section.SAME_TOPIC), 4);
});
});
suite('test first non-empty list', () => {
const relatedChangeInfo: RelatedChangesInfo = {
...createRelatedChangesInfo(),
changes: [createRelatedChangeAndCommitInfo()],
};
const submittedTogether: SubmittedTogetherInfo = {
...createSubmittedTogetherInfo(),
changes: [createChange()],
};
setup(() => {
element.change = createParsedChange();
element.patchNum = 1 as PatchSetNum;
});
test('first list', async () => {
stubRestApi('getRelatedChanges').returns(
Promise.resolve(relatedChangeInfo)
);
await element.reload();
const section = queryAndAssert<HTMLElement>(element, '#relatedChanges');
const relatedChanges = queryAndAssert<GrRelatedCollapse>(
section,
'gr-related-collapse'
);
assert.isTrue(relatedChanges!.classList.contains('first'));
});
test('first empty second non-empty', async () => {
stubRestApi('getRelatedChanges').returns(
Promise.resolve(createRelatedChangesInfo())
);
stubRestApi('getChangesSubmittedTogether').returns(
Promise.resolve(submittedTogether)
);
await element.reload();
const relatedChanges = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#relatedChanges'),
'gr-related-collapse'
);
assert.isFalse(relatedChanges!.classList.contains('first'));
const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#submittedTogether'),
'gr-related-collapse'
);
assert.isTrue(submittedTogetherSection!.classList.contains('first'));
});
test('first non-empty second empty third non-empty', async () => {
stubRestApi('getRelatedChanges').returns(
Promise.resolve(relatedChangeInfo)
);
stubRestApi('getChangesSubmittedTogether').returns(
Promise.resolve(createSubmittedTogetherInfo())
);
stubRestApi('getChangeCherryPicks').returns(
Promise.resolve([createChange()])
);
await element.reload();
const relatedChanges = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#relatedChanges'),
'gr-related-collapse'
);
assert.isTrue(relatedChanges!.classList.contains('first'));
const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#submittedTogether'),
'gr-related-collapse'
);
assert.isFalse(submittedTogetherSection!.classList.contains('first'));
const cherryPicks = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#cherryPicks'),
'gr-related-collapse'
);
assert.isFalse(cherryPicks!.classList.contains('first'));
});
});
test('_changesEqual', () => {
const change1: ChangeInfo = {
...createChange(),
change_id: '123' as ChangeId,
_number: 0 as NumericChangeId,
};
const change2: ChangeInfo = {
...createChange(),
change_id: '456' as ChangeId,
_number: 1 as NumericChangeId,
};
const change3: ChangeInfo = {
...createChange(),
change_id: '123' as ChangeId,
_number: 2 as NumericChangeId,
};
const change4: RelatedChangeAndCommitInfo = {
...createRelatedChangeAndCommitInfo(),
change_id: '123' as ChangeId,
_change_number: 1 as NumericChangeId,
};
assert.isTrue(element._changesEqual(change1, change1));
assert.isFalse(element._changesEqual(change1, change2));
assert.isFalse(element._changesEqual(change1, change3));
assert.isTrue(element._changesEqual(change2, change4));
});
test('_getChangeNumber', () => {
const change1: ChangeInfo = {
...createChange(),
change_id: '123' as ChangeId,
_number: 0 as NumericChangeId,
};
const change2: ChangeInfo = {
...createChange(),
change_id: '456' as ChangeId,
_number: 1 as NumericChangeId,
};
assert.equal(element._getChangeNumber(change1), 0);
assert.equal(element._getChangeNumber(change2), 1);
});
suite('get conflicts tests', () => {
let element: GrRelatedChangesListExperimental;
let conflictsStub: SinonStubbedMember<RestApiService['getChangeConflicts']>;
setup(() => {
element = basicFixture.instantiate();
conflictsStub = stubRestApi('getChangeConflicts').returns(
Promise.resolve(undefined)
);
});
test('request conflicts if open and mergeable', () => {
element.patchNum = 7 as PatchSetNum;
element.change = {
...createParsedChange(),
change_id: '123' as ChangeId,
status: ChangeStatus.NEW,
};
element.mergeable = true;
element.reload();
assert.isTrue(conflictsStub.called);
});
test('does not request conflicts if closed and mergeable', () => {
element.patchNum = 7 as PatchSetNum;
element.change = {
...createParsedChange(),
change_id: '123' as ChangeId,
status: ChangeStatus.NEW,
};
element.reload();
assert.isFalse(conflictsStub.called);
});
test('does not request conflicts if open and not mergeable', () => {
element.patchNum = 7 as PatchSetNum;
element.change = {
...createParsedChange(),
change_id: '123' as ChangeId,
status: ChangeStatus.NEW,
};
element.mergeable = false;
element.reload();
assert.isFalse(conflictsStub.called);
});
test('doesnt request conflicts if closed and not mergeable', () => {
element.patchNum = 7 as PatchSetNum;
element.change = {
...createParsedChange(),
change_id: '123' as ChangeId,
status: ChangeStatus.NEW,
};
element.mergeable = false;
element.reload();
assert.isFalse(conflictsStub.called);
});
});
test('connected revisions', () => {
const change: ParsedChangeInfo = {
...createParsedChange(),
revisions: {
e3c6d60783bfdec9ebae7dcfec4662360433449e: createRevision(1),
'26e5e4c9c7ae31cbd876271cca281ce22b413997': createRevision(2),
bf7884d695296ca0c91702ba3e2bc8df0f69a907: createRevision(7),
b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3: createRevision(5),
d6bcee67570859ccb684873a85cf50b1f0e96fda: createRevision(6),
cc960918a7f90388f4a9e05753d0f7b90ad44546: createRevision(3),
'9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': createRevision(4),
},
};
let patchNum = 7 as PatchSetNum;
let relatedChanges: RelatedChangeAndCommitInfo[] = [
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
),
parents: [
{
commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
subject: 'subject1',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'87ed20b241576b620bbaa3dfd47715ce6782b7dd'
),
parents: [
{
commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' as CommitId,
subject: 'subject2',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
),
parents: [
{
commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' as CommitId,
subject: 'subject3',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'b0ccb183494a8e340b8725a2dc553967d61e6dae'
),
parents: [
{
commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907' as CommitId,
subject: 'subject4',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'bf7884d695296ca0c91702ba3e2bc8df0f69a907'
),
parents: [
{
commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce' as CommitId,
subject: 'subject5',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'613bc4f81741a559c6667ac08d71dcc3348f73ce'
),
parents: [
{
commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75' as CommitId,
subject: 'subject6',
},
],
},
},
];
let connectedChanges = element._computeConnectedRevisions(
change,
patchNum,
relatedChanges
);
assert.deepEqual(connectedChanges, [
'613bc4f81741a559c6667ac08d71dcc3348f73ce',
'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
'b0ccb183494a8e340b8725a2dc553967d61e6dae',
'6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
'87ed20b241576b620bbaa3dfd47715ce6782b7dd',
'2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
]);
patchNum = 4 as PatchSetNum;
relatedChanges = [
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
),
parents: [
{
commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
subject: 'My parent commit',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'87ed20b241576b620bbaa3dfd47715ce6782b7dd'
),
parents: [
{
commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' as CommitId,
subject: 'My parent commit',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
),
parents: [
{
commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' as CommitId,
subject: 'My parent commit',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b'
),
parents: [
{
commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6' as CommitId,
subject: 'My parent commit',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6'
),
parents: [
{
commit: 'af815dac54318826b7f1fa468acc76349ffc588e' as CommitId,
subject: 'My parent commit',
},
],
},
},
{
...createRelatedChangeAndCommitInfo(),
commit: {
...createCommitInfoWithRequiredCommit(
'af815dac54318826b7f1fa468acc76349ffc588e'
),
parents: [
{
commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c' as CommitId,
subject: 'My parent commit',
},
],
},
},
];
connectedChanges = element._computeConnectedRevisions(
change,
patchNum,
relatedChanges
);
assert.deepEqual(connectedChanges, [
'af815dac54318826b7f1fa468acc76349ffc588e',
'9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
'9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
]);
});
suite('gr-related-changes-list plugin tests', () => {
let element: GrRelatedChangesListExperimental;
setup(() => {
resetPlugins();
element = basicFixture.instantiate();
});
teardown(() => {
resetPlugins();
});
test('endpoint params', done => {
element.change = {...createParsedChange(), labels: {}};
interface RelatedChangesListGrEndpointDecorator
extends GrEndpointDecorator {
plugin: PluginApi;
change: ParsedChangeInfo;
}
let hookEl: RelatedChangesListGrEndpointDecorator;
let plugin: PluginApi;
pluginApi.install(
p => {
plugin = p;
plugin
.hook('related-changes-section')
.getLastAttached()
.then(el => (hookEl = el as RelatedChangesListGrEndpointDecorator));
},
'0.1',
'http://some/plugins/url1.js'
);
getPluginLoader().loadPlugins([]);
flush(() => {
assert.strictEqual(hookEl.plugin, plugin);
assert.strictEqual(hookEl.change, element.change);
done();
});
});
});
});

View File

@@ -1,222 +0,0 @@
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
}
section {
margin-bottom: 1.4em; /* Same as line height for collapse purposes */
}
a {
display: block;
}
.changeContainer,
a {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.changeContainer {
display: flex;
}
.arrowToCurrentChange {
position: absolute;
}
h4,
section div {
display: flex;
}
h4:before,
section div:before {
content: ' ';
flex-shrink: 0;
width: 1.2em;
}
.note {
color: var(--error-text-color);
}
.relatedChanges a {
display: inline-block;
}
.strikethrough {
color: var(--deemphasized-text-color);
text-decoration: line-through;
}
.status {
color: var(--deemphasized-text-color);
font-weight: var(--font-weight-bold);
margin-left: var(--spacing-xs);
}
.notCurrent {
color: var(--warning-foreground);
}
.indirectAncestor {
color: var(--indirect-ancestor-text-color);
}
.submittableCheck {
padding-left: var(--spacing-s);
color: var(--positive-green-text-color);
display: none;
}
.submittableCheck.submittable {
display: inline;
}
.hidden,
.mobile {
display: none;
}
@media screen and (max-width: 60em) {
.mobile {
display: block;
}
}
</style>
<div>
<gr-endpoint-decorator name="related-changes-section">
<gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
<gr-endpoint-slot name="top"></gr-endpoint-slot>
<section
class="relatedChanges"
hidden$="[[!_relatedResponse.changes.length]]"
hidden=""
>
<h4>Relation chain</h4>
<template
is="dom-repeat"
items="[[_relatedResponse.changes]]"
as="related"
>
<template is="dom-if" if="[[_changesEqual(related, change)]]">
<span
role="img"
class="arrowToCurrentChange"
aria-label="Arrow marking current change"
>➔</span
>
</template>
<div class="rightIndent changeContainer">
<a
href$="[[_computeChangeURL(related._change_number, related.project, related._revision_number)]]"
class$="[[_computeLinkClass(related)]]"
title$="[[related.commit.subject]]"
on-click="_reportClick"
>
[[related.commit.subject]]
</a>
<span class$="[[_computeChangeStatusClass(related)]]">
([[_computeChangeStatus(related)]])
</span>
</div>
</template>
</section>
<section
id="submittedTogether"
class$="[[_computeSubmittedTogetherClass(_submittedTogether)]]"
>
<h4>Submitted together</h4>
<template
is="dom-repeat"
items="[[_submittedTogether.changes]]"
as="related"
>
<template is="dom-if" if="[[_changesEqual(related, change)]]">
<span
role="img"
class="arrowToCurrentChange"
aria-label="Arrow marking current change"
>➔</span
>
</template>
<div class="changeContainer">
<a
href$="[[_computeChangeURL(related._number, related.project)]]"
class$="[[_computeLinkClass(related)]]"
title$="[[related.project]]: [[related.branch]]: [[related.subject]]"
on-click="_reportClick"
>
[[related.project]]: [[related.branch]]: [[related.subject]]
</a>
<span
tabindex="-1"
title="Submittable"
class$="submittableCheck [[_computeLinkClass(related)]]"
role="img"
aria-label="Submittable"
>✓</span
>
</div>
</template>
<template is="dom-if" if="[[_submittedTogether.non_visible_changes]]">
<div class="note">
[[_computeNonVisibleChangesNote(_submittedTogether.non_visible_changes)]]
</div>
</template>
</section>
<section hidden$="[[!_sameTopic.length]]" hidden="">
<h4>Same topic</h4>
<template is="dom-repeat" items="[[_sameTopic]]" as="change">
<div>
<a
href$="[[_computeChangeURL(change._number, change.project)]]"
class$="[[_computeLinkClass(change)]]"
title$="[[change.project]]: [[change.branch]]: [[change.subject]]"
on-click="_reportClick"
>
[[change.project]]: [[change.branch]]: [[change.subject]]
</a>
</div>
</template>
</section>
<section hidden$="[[!_conflicts.length]]" hidden="">
<h4>Merge conflicts</h4>
<template is="dom-repeat" items="[[_conflicts]]" as="change">
<div>
<a
href$="[[_computeChangeURL(change._number, change.project)]]"
class$="[[_computeLinkClass(change)]]"
title$="[[change.subject]]"
on-click="_reportClick"
>
[[change.subject]]
</a>
</div>
</template>
</section>
<section hidden$="[[!_cherryPicks.length]]" hidden="">
<h4>Cherry picks</h4>
<template is="dom-repeat" items="[[_cherryPicks]]" as="change">
<div>
<a
href$="[[_computeChangeURL(change._number, change.project)]]"
class$="[[_computeLinkClass(change)]]"
title$="[[change.branch]]: [[change.subject]]"
on-click="_reportClick"
>
[[change.branch]]: [[change.subject]]
</a>
</div>
</template>
</section>
<gr-endpoint-slot name="bottom"></gr-endpoint-slot>
</gr-endpoint-decorator>
</div>
<div hidden$="[[!loading]]">Loading...</div>
`;