Merge "Fix target area for expanding/collapsing check result rows"
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index b2dcfb8..4c594892 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -367,6 +367,7 @@
* @polymer/paper-dialog-behavior
* @polymer/paper-dialog-scrollable
* @polymer/paper-dropdown-menu
+* @polymer/paper-fab
* @polymer/paper-icon-button
* @polymer/paper-input
* @polymer/paper-item
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 63601d2..45b1e66 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3326,6 +3326,7 @@
* @polymer/paper-dialog-behavior
* @polymer/paper-dialog-scrollable
* @polymer/paper-dropdown-menu
+* @polymer/paper-fab
* @polymer/paper-icon-button
* @polymer/paper-input
* @polymer/paper-item
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 9b86a4f..269d1c4 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -30,6 +30,7 @@
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.ORIGIN;
import static com.google.common.net.HttpHeaders.VARY;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_REMOVE_REVISION_ETAG;
import static java.math.RoundingMode.CEILING;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -110,7 +111,9 @@
import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
import com.google.gerrit.server.cache.PerThreadCache;
import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.group.GroupAuditService;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.PerformanceLogContext;
@@ -252,6 +255,7 @@
final PluginSetContext<ExceptionHook> exceptionHooks;
final Injector injector;
final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
+ final ExperimentFeatures experimentFeatures;
@Inject
Globals(
@@ -269,7 +273,8 @@
RetryHelper retryHelper,
PluginSetContext<ExceptionHook> exceptionHooks,
Injector injector,
- DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
+ DynamicMap<DynamicOptions.DynamicBean> dynamicBeans,
+ ExperimentFeatures experimentFeatures) {
this.currentUser = currentUser;
this.webSession = webSession;
this.paramParser = paramParser;
@@ -286,6 +291,7 @@
allowOrigin = makeAllowOrigin(config);
this.injector = injector;
this.dynamicBeans = dynamicBeans;
+ this.experimentFeatures = experimentFeatures;
}
private static Pattern makeAllowOrigin(Config cfg) {
@@ -775,6 +781,11 @@
TraceContext.newTimer(
"RestApiServlet#getEtagWithRetry:resource",
Metadata.builder().restViewName(rsrc.getClass().getSimpleName()).build())) {
+ if (rsrc instanceof RevisionResource
+ && globals.experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_REMOVE_REVISION_ETAG)) {
+ return null;
+ }
return invokeRestEndpointWithRetry(
req,
traceContext,
@@ -1056,7 +1067,7 @@
if (rsrc instanceof RestResource.HasETag) {
String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
- if (have != null) {
+ if (!Strings.isNullOrEmpty(have)) {
String eTag = getEtagWithRetry(req, traceContext, (RestResource.HasETag) rsrc);
return have.equals(eTag);
}
@@ -1134,7 +1145,9 @@
res.setHeader(HttpHeaders.ETAG, eTag);
} else if (rsrc instanceof RestResource.HasETag) {
String eTag = getEtagWithRetry(req, traceContext, (RestResource.HasETag) rsrc);
- res.setHeader(HttpHeaders.ETAG, eTag);
+ if (!Strings.isNullOrEmpty(eTag)) {
+ res.setHeader(HttpHeaders.ETAG, eTag);
+ }
}
if (rsrc instanceof RestResource.HasLastModified) {
res.setDateHeader(
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index af49438..0f85578 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -22,6 +22,9 @@
/** Features that are known experiments and can be referenced in the code. */
public static String UI_FEATURE_PATCHSET_COMMENTS = "UiFeature__patchset_comments";
+ public static String GERRIT_BACKEND_REQUEST_FEATURE_REMOVE_REVISION_ETAG =
+ "GerritBackendRequestFeature__remove_revision_etag";
+
/** Features, enabled by default in the current release. */
public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES =
ImmutableSet.of(UI_FEATURE_PATCHSET_COMMENTS);
diff --git a/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java b/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
index 527129c..f3c0fb8 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRevisionActions.java
@@ -34,6 +34,6 @@
@Override
public Response<Map<String, ActionInfo>> apply(RevisionResource rsrc) {
- return Response.withMustRevalidate(delegate.format(rsrc));
+ return Response.ok(delegate.format(rsrc));
}
}
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index f7d5a59..da7f4e8 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -264,6 +264,19 @@
buttonType: ContextButtonType;
}
+export declare type ImageDiffAction =
+ | {
+ type: 'overview-image-clicked';
+ }
+ | {
+ type: 'overview-frame-dragged';
+ }
+ | {type: 'magnifier-clicked'}
+ | {type: 'magnifier-dragged'}
+ | {type: 'version-switcher-clicked'; button: 'base' | 'revision'}
+ | {type: 'zoom-level-changed'; scale: number | 'fit'}
+ | {type: 'follow-mouse-changed'; value: boolean};
+
export enum GrDiffLineType {
ADD = 'add',
BOTH = 'both',
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 363f6e5..689347a 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -58,6 +58,13 @@
}
/**
+ * @desc Templates that can be used in change log messages.
+ */
+export enum ChangeMessageTemplate {
+ ACCOUNT_TEMPLATE = '<GERRIT_ACCOUNT_(\\d+)>',
+}
+
+/**
* @desc Modes for gr-diff-cursor
* The scroll behavior for the cursor. Values are 'never' and
* 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 696b9e4..89f7b89 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -19,7 +19,6 @@
import {GrLitElement} from '../../lit/gr-lit-element';
import {sharedStyles} from '../../../styles/shared-styles';
import {appContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {
CheckResult,
CheckRun,
@@ -264,10 +263,6 @@
@customElement('gr-change-summary')
export class GrChangeSummary extends GrLitElement {
- private readonly newChangeSummaryUiEnabled = appContext.flagsService.isEnabled(
- KnownExperimentId.NEW_CHANGE_SUMMARY_UI
- );
-
@property({type: Object})
changeComments?: ChangeComments;
@@ -312,9 +307,6 @@
display: block;
color: var(--deemphasized-text-color);
max-width: 650px;
- /* temporary for old checks status */
- }
- :host.new-change-summary-true {
margin-bottom: var(--spacing-m);
}
.zeroState {
@@ -500,7 +492,7 @@
)}${this.renderChecksChipForStatus(RunStatus.RUNNING, isRunning)}
</td>
</tr>
- <tr ?hidden=${!this.newChangeSummaryUiEnabled}>
+ <tr>
<td class="key">Comments</td>
<td class="value">
<span
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index b753cf9..c34703e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -38,7 +38,6 @@
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 @@
} 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 {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';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
@@ -189,17 +184,6 @@
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 ACCIDENTAL_STARRING_LIMIT_MS = 10 * 1000;
@@ -471,9 +455,6 @@
})
_commitCollapsible?: boolean;
- @property({type: Boolean})
- _relatedChangesCollapsed = true;
-
@property({type: Number})
_updateCheckTimerHandle?: number | null;
@@ -483,9 +464,6 @@
})
_editMode?: boolean;
- @property({type: Boolean, observer: '_updateToggleContainerClass'})
- _showRelatedToggle = false;
-
@property({
type: Boolean,
computed: '_isParentCurrent(_currentRevisionActions)',
@@ -1331,7 +1309,6 @@
this._initialLoadComplete = false;
this._changeNum = value.changeNum;
- this.getRelatedChangesList()?.clear();
this._reload(true).then(() => {
this._performPostLoadTasks();
});
@@ -1965,6 +1942,9 @@
this.restApiService.getChange(changeId)
)
).then(changes => {
+ changes = changes.filter(
+ change => change?.status !== ChangeStatus.ABANDONED
+ );
if (!changes.length) return;
const change = changes.find(
change => change?.status === ChangeStatus.MERGED
@@ -2201,7 +2181,6 @@
return Promise.resolve([]);
}
this._loading = true;
- this._relatedChangesCollapsed = true;
this.reporting.time(Timing.CHANGE_RELOAD);
this.reporting.time(Timing.CHANGE_DATA);
@@ -2299,30 +2278,25 @@
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);
}
@@ -2423,15 +2397,6 @@
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
@@ -2451,13 +2416,6 @@
}
}
- _toggleRelatedChangesCollapsed() {
- this._relatedChangesCollapsed = !this._relatedChangesCollapsed;
- if (this._relatedChangesCollapsed) {
- window.scrollTo(0, 0);
- }
- }
-
_computeCommitCollapsible(commitMessage?: string) {
if (!commitMessage) {
return false;
@@ -2468,124 +2426,6 @@
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 ||
@@ -2872,12 +2712,6 @@
'#relatedChanges'
);
}
-
- getRelatedChangesListExperimental() {
- return this.shadowRoot!.querySelector<GrRelatedChangesListExperimental>(
- '#relatedChangesExperimental'
- );
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index de08318..211810c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -165,12 +165,6 @@
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 @@
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 @@
/* 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 @@
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;
@@ -520,7 +500,6 @@
</div>
</template>
<gr-change-summary
- class$="new-change-summary-[[_isNewChangeSummaryUiEnabled]]"
change-comments="[[_changeComments]]"
comment-threads="[[_commentThreads]]"
self-account="[[_account]]"
@@ -537,36 +516,11 @@
</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>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index fc0d289..3807ca9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -30,7 +30,6 @@
} 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';
@@ -39,7 +38,6 @@
import 'lodash/lodash';
import {
stubRestApi,
- SinonSpyMember,
TestKeyboardShortcutBinder,
} from '../../../test/test-utils';
import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -47,7 +45,6 @@
createAppElementChangeViewParams,
createApproval,
createChange,
- createChangeConfig,
createChangeMessages,
createCommit,
createMergeable,
@@ -409,9 +406,6 @@
});
});
- const getCustomCssValue = (cssParam: string) =>
- getComputedStyleValue(cssParam, element);
-
test('_handleMessageAnchorTap', () => {
element._changeNum = 1 as NumericChangeId;
element._patchRange = {
@@ -1308,6 +1302,46 @@
});
});
+ test('do not show any chip if all reverts are abandoned', done => {
+ const change = {
+ ...createChange(),
+ messages: createChangeMessages(2),
+ };
+ change.messages[0].message = 'Created a revert of this change as 12345';
+ change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag;
+
+ change.messages[1].message = 'Created a revert of this change as 23456';
+ change.messages[1].tag = MessageTag.TAG_REVERT as ReviewInputTag;
+
+ const getChangeStub = stubRestApi('getChange');
+ getChangeStub.onFirstCall().returns(
+ Promise.resolve({
+ ...createChange(),
+ status: ChangeStatus.ABANDONED,
+ })
+ );
+ getChangeStub.onSecondCall().returns(
+ Promise.resolve({
+ ...createChange(),
+ status: ChangeStatus.ABANDONED,
+ })
+ );
+ element._change = change;
+ element._mergeable = true;
+ element._submitEnabled = true;
+ flush();
+ element.computeRevertSubmitted(element._change);
+ flush(() => {
+ assert.isFalse(
+ element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
+ );
+ assert.isFalse(
+ element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
+ );
+ done();
+ });
+ });
+
test('show revert created if no revert is merged', done => {
const change = {
...createChange(),
@@ -1315,6 +1349,10 @@
};
change.messages[0].message = 'Created a revert of this change as 12345';
change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag;
+
+ change.messages[1].message = 'Created a revert of this change as 23456';
+ change.messages[1].tag = MessageTag.TAG_REVERT as ReviewInputTag;
+
const getChangeStub = stubRestApi('getChange');
getChangeStub.onFirstCall().returns(
Promise.resolve({
@@ -1342,7 +1380,7 @@
});
});
- test('show revert created if no revert is merged', done => {
+ test('show revert submitted if revert is merged', done => {
const change = {
...createChange(),
messages: createChangeMessages(2),
@@ -1622,10 +1660,6 @@
.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 = {
@@ -1635,7 +1669,6 @@
};
element._paramsChanged(value);
assert.isTrue(reloadStub.calledOnce);
- assert.isTrue(relatedClearSpy.calledOnce);
element._initialLoadComplete = true;
@@ -1644,7 +1677,6 @@
element._paramsChanged(value);
assert.isFalse(reloadStub.calledTwice);
assert.isTrue(reloadPatchDependentStub.calledOnce);
- assert.isTrue(relatedClearSpy.calledOnce);
assert.isTrue(collapseStub.calledTwice);
});
@@ -1657,10 +1689,6 @@
element.$.commentAPI,
'reloadPortedComments'
);
- const relatedChanges = element.shadowRoot!.querySelector(
- '#relatedChanges'
- ) as GrRelatedChangesList;
- sinon.spy(relatedChanges, 'clear');
sinon.stub(element.$.fileList, 'collapseAllDiffs');
const value: AppElementChangeViewParams = {
@@ -2286,322 +2314,6 @@
});
});
- 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');
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 3ea9f68..fed02a7 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -25,7 +25,11 @@
import '../../../styles/gr-voting-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-message_html';
-import {MessageTag, SpecialFilePath} from '../../../constants/constants';
+import {
+ ChangeMessageTemplate,
+ MessageTag,
+ SpecialFilePath,
+} from '../../../constants/constants';
import {customElement, property, computed, observe} from '@polymer/decorators';
import {
ChangeInfo,
@@ -40,6 +44,7 @@
PatchSetNum,
AccountInfo,
BasePatchSetNum,
+ AccountId,
} from '../../../types/common';
import {CommentThread} from '../../../utils/comment-util';
import {hasOwnProperty} from '../../../utils/common-util';
@@ -176,14 +181,19 @@
@property({
type: String,
- computed: '_computeMessageContentExpanded(message.message, message.tag)',
+ computed:
+ '_computeMessageContentExpanded(message.message,' +
+ ' message.accountsInMessage,' +
+ ' message.tag)',
})
_messageContentExpanded = '';
@property({
type: String,
computed:
- '_computeMessageContentCollapsed(message.message, message.tag,' +
+ '_computeMessageContentCollapsed(message.message,' +
+ ' message.accountsInMessage,' +
+ ' message.tag,' +
' message.commentThreads)',
})
_messageContentCollapsed = '';
@@ -231,8 +241,12 @@
return pluralize(threadsLength, 'comment');
}
- _computeMessageContentExpanded(content?: string, tag?: ReviewInputTag) {
- return this._computeMessageContent(true, content, tag);
+ _computeMessageContentExpanded(
+ content?: string,
+ accountsInMessage?: AccountInfo[],
+ tag?: ReviewInputTag
+ ) {
+ return this._computeMessageContent(true, content, accountsInMessage, tag);
}
_patchsetCommentSummary(commentThreads: CommentThread[] = []) {
@@ -261,10 +275,16 @@
_computeMessageContentCollapsed(
content?: string,
+ accountsInMessage?: AccountInfo[],
tag?: ReviewInputTag,
commentThreads?: CommentThread[]
) {
- const summary = this._computeMessageContent(false, content, tag);
+ const summary = this._computeMessageContent(
+ false,
+ content,
+ accountsInMessage,
+ tag
+ );
if (summary || !commentThreads) return summary;
return this._patchsetCommentSummary(commentThreads);
}
@@ -319,11 +339,22 @@
_computeMessageContent(
isExpanded: boolean,
content?: string,
+ accountsInMessage?: AccountInfo[],
tag?: ReviewInputTag
) {
if (!content) return '';
const isNewPatchSet = this._isNewPatchsetTag(tag);
+ if (accountsInMessage) {
+ content = content.replace(
+ new RegExp(ChangeMessageTemplate.ACCOUNT_TEMPLATE, 'g'),
+ (_accountIdTemplate, accountId) =>
+ accountsInMessage.find(
+ account => account._account_id === (Number(accountId) as AccountId)
+ )?.name || `Gerrit Account ${accountId}`
+ );
+ }
+
const lines = content.split('\n');
const filteredLines = lines.filter(line => {
if (!isExpanded && line.startsWith('>')) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
index b8f3c73..97568dc 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
@@ -19,6 +19,7 @@
import './gr-message';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {
+ createAccountWithIdNameAndEmail,
createChange,
createChangeMessage,
createComment,
@@ -349,11 +350,21 @@
suite('compute messages', () => {
test('empty', () => {
assert.equal(
- element._computeMessageContent(true, '', '' as ReviewInputTag),
+ element._computeMessageContent(
+ true,
+ '',
+ undefined,
+ '' as ReviewInputTag
+ ),
''
);
assert.equal(
- element._computeMessageContent(false, '', '' as ReviewInputTag),
+ element._computeMessageContent(
+ false,
+ '',
+ undefined,
+ '' as ReviewInputTag
+ ),
''
);
});
@@ -361,13 +372,13 @@
test('new patchset', () => {
const original = 'Uploaded patch set 1.';
const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
- let actual = element._computeMessageContent(true, original, tag);
+ let actual = element._computeMessageContent(true, original, [], tag);
assert.equal(
actual,
- element._computeMessageContentCollapsed(original, tag, [])
+ element._computeMessageContentCollapsed(original, [], tag, [])
);
assert.equal(actual, original);
- actual = element._computeMessageContent(false, original, tag);
+ actual = element._computeMessageContent(false, original, [], tag);
assert.equal(actual, original);
});
@@ -375,13 +386,13 @@
const original = 'Patch Set 27: Patch Set 26 was rebased';
const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
const expected = 'Patch Set 26 was rebased';
- let actual = element._computeMessageContent(true, original, tag);
+ let actual = element._computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
assert.equal(
actual,
- element._computeMessageContentCollapsed(original, tag, [])
+ element._computeMessageContentCollapsed(original, [], tag, [])
);
- actual = element._computeMessageContent(false, original, tag);
+ actual = element._computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
@@ -389,13 +400,13 @@
const original = 'Patch Set 1:\n\nThis change is ready for review.';
const tag = undefined;
const expected = 'This change is ready for review.';
- let actual = element._computeMessageContent(true, original, tag);
+ let actual = element._computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
assert.equal(
actual,
- element._computeMessageContentCollapsed(original, tag, [])
+ element._computeMessageContentCollapsed(original, [], tag, [])
);
- actual = element._computeMessageContent(false, original, tag);
+ actual = element._computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
@@ -403,9 +414,9 @@
const original = 'Patch Set 1: Code-Style+1';
const tag = undefined;
const expected = '';
- let actual = element._computeMessageContent(true, original, tag);
+ let actual = element._computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
- actual = element._computeMessageContent(false, original, tag);
+ actual = element._computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
@@ -413,9 +424,47 @@
const original = 'Patch Set 1:\n\n(3 comments)';
const tag = undefined;
const expected = '';
- let actual = element._computeMessageContent(true, original, tag);
+ let actual = element._computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
- actual = element._computeMessageContent(false, original, tag);
+ actual = element._computeMessageContent(false, original, [], tag);
+ assert.equal(actual, expected);
+ });
+
+ test('message template', () => {
+ const original =
+ 'Removed vote: \n\n * Code-Style+1 by <GERRIT_ACCOUNT_0000001>\n * Code-Style-1 by <GERRIT_ACCOUNT_0000002>';
+ const tag = undefined;
+ const expected =
+ 'Removed vote: \n\n * Code-Style+1 by User-1\n * Code-Style-1 by User-2';
+ const accountsInMessage = [
+ createAccountWithIdNameAndEmail(1),
+ createAccountWithIdNameAndEmail(2),
+ ];
+ let actual = element._computeMessageContent(
+ true,
+ original,
+ accountsInMessage,
+ tag
+ );
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(
+ false,
+ original,
+ accountsInMessage,
+ tag
+ );
+ assert.equal(actual, expected);
+ });
+
+ test('message template missing accounts', () => {
+ const original =
+ 'Removed vote: \n\n * Code-Style+1 by <GERRIT_ACCOUNT_0000001>\n * Code-Style-1 by <GERRIT_ACCOUNT_0000002>';
+ const tag = undefined;
+ const expected =
+ 'Removed vote: \n\n * Code-Style+1 by Gerrit Account 0000001\n * Code-Style-1 by Gerrit Account 0000002';
+ let actual = element._computeMessageContent(true, original, [], tag);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
});
@@ -570,10 +619,18 @@
},
];
assert.equal(
- element._computeMessageContentCollapsed('', undefined, threads),
+ element._computeMessageContentCollapsed(
+ '',
+ undefined,
+ undefined,
+ threads
+ ),
'testing the load'
);
- assert.equal(element._computeMessageContent(false, '', undefined), '');
+ assert.equal(
+ element._computeMessageContent(false, '', undefined, undefined),
+ ''
+ );
});
test('single patchset comment with reply', () => {
@@ -610,10 +667,18 @@
},
];
assert.equal(
- element._computeMessageContentCollapsed('', undefined, threads),
+ element._computeMessageContentCollapsed(
+ '',
+ undefined,
+ undefined,
+ threads
+ ),
'n'
);
- assert.equal(element._computeMessageContent(false, '', undefined), '');
+ assert.equal(
+ element._computeMessageContent(false, '', undefined, undefined),
+ ''
+ );
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
deleted file mode 100644
index 8c83b58..0000000
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental_test.ts
deleted file mode 100644
index 971da40..0000000
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental_test.ts
+++ /dev/null
@@ -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();
- });
- });
- });
-});
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
similarity index 100%
rename from polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts
rename to polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 927f3c9..8d5bc33 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,217 +14,547 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
+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 {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-related-changes-list_html';
+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 {ChangeStatus} from '../../../constants/constants';
-
+import {pluralize} from '../../../utils/string-util';
import {
changeIsOpen,
getRevisionKey,
isChangeInfo,
} from '../../../utils/change-util';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {customElement, observe, property} from '@polymer/decorators';
-import {
- ChangeId,
- ChangeInfo,
- CommitId,
- NumericChangeId,
- PatchSetNum,
- RelatedChangeAndCommitInfo,
- RelatedChangesInfo,
- RepoName,
- SubmittedTogetherInfo,
-} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
-import {pluralize} from '../../../utils/string-util';
-import {ParsedChangeInfo} from '../../../types/types';
-function getEmptySubmitTogetherInfo(): SubmittedTogetherInfo {
- return {changes: [], non_visible_changes: 0};
+/** 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')
-export class GrRelatedChangesList extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- /**
- * Fired when a new section is loaded so that the change view can determine
- * a show more button is needed, sometimes before all the sections finish
- * loading.
- *
- * @event new-section-loaded
- */
-
- @property({type: Object})
+export class GrRelatedChangesList extends GrLitElement {
+ @property()
change?: ParsedChangeInfo;
- @property({type: Boolean, notify: true})
- hasParent = false;
-
@property({type: String})
patchNum?: PatchSetNum;
- @property({type: Boolean, reflectToAttribute: true})
- hidden = false;
-
- @property({type: Boolean, notify: true})
- loading?: boolean;
-
- @property({type: Boolean})
+ @property()
mergeable?: boolean;
- @property({
- type: Array,
- computed:
- '_computeConnectedRevisions(change, patchNum, ' +
- '_relatedResponse.changes)',
- })
- _connectedRevisions?: CommitId[];
+ @internalProperty()
+ submittedTogether?: SubmittedTogetherInfo = {
+ changes: [],
+ non_visible_changes: 0,
+ };
- @property({type: Object})
- _relatedResponse: RelatedChangesInfo = {changes: []};
+ @internalProperty()
+ relatedChanges: RelatedChangeAndCommitInfo[] = [];
- @property({type: Object})
- _submittedTogether?: SubmittedTogetherInfo = getEmptySubmitTogetherInfo();
+ @internalProperty()
+ conflictingChanges: ChangeInfo[] = [];
- @property({type: Array})
- _conflicts: ChangeInfo[] = [];
+ @internalProperty()
+ cherryPickChanges: ChangeInfo[] = [];
- @property({type: Array})
- _cherryPicks: ChangeInfo[] = [];
-
- @property({type: Array})
- _sameTopic?: ChangeInfo[] = [];
+ @internalProperty()
+ sameTopicChanges: ChangeInfo[] = [];
private readonly restApiService = appContext.restApiService;
- private readonly reportingService = appContext.reportingService;
-
- clear() {
- this.loading = true;
- this.hidden = true;
-
- this._relatedResponse = {changes: []};
- this._submittedTogether = getEmptySubmitTogetherInfo();
- this._conflicts = [];
- this._cherryPicks = [];
- this._sameTopic = [];
+ 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;
+ }
+ `,
+ ];
}
- reload() {
- if (!this.change || !this.patchNum) {
- return Promise.resolve();
+ 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;
- this.loading = true;
+ 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>> = [
- this.restApiService
- .getRelatedChanges(change._number, this.patchNum)
- .then(response => {
- if (!response) {
- throw new Error('getRelatedChanges returned undefined response');
- }
- this._relatedResponse = response;
- this._fireReloadEvent();
- this.hasParent = this._calculateHasParent(
- change.change_id,
- response.changes
- );
- }),
+ 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._fireReloadEvent();
+ this.submittedTogether = response;
}),
this.restApiService
.getChangeCherryPicks(change.project, change.change_id, change._number)
.then(response => {
- this._cherryPicks = response || [];
- this._fireReloadEvent();
+ 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 => {
- // Because the server doesn't always return a response and the
- // template expects an array, always return an array.
- this._conflicts = response ? response : [];
- this._fireReloadEvent();
+ this.conflictingChanges = response ?? [];
})
);
}
-
- promises.push(
- this._getServerConfig().then(config => {
- if (change.topic) {
- if (!config) {
- throw new Error('_getServerConfig returned undefined ');
- }
- if (!config.change.submit_whole_topic) {
+ 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(change.topic, change._number)
+ .getChangesWithSameTopic(changeTopic, change._number)
.then(response => {
- this._sameTopic = response;
+ if (changeTopic === this.change?.topic) {
+ this.sameTopicChanges = response ?? [];
+ }
});
}
- }
- this._sameTopic = [];
- return Promise.resolve();
- })
- );
+ this.sameTopicChanges = [];
+ return Promise.resolve();
+ })
+ );
+ }
- return Promise.all(promises).then(() => {
- this.loading = false;
- });
- }
-
- _fireReloadEvent() {
- // The listener on the change computes height of the related changes
- // section, so they have to be rendered first, and inside a dom-repeat,
- // that requires a flush.
- flush();
- this.dispatchEvent(new CustomEvent('new-section-loaded'));
- }
-
- /**
- * Determines whether or not the given change has a parent change. If there
- * is a relation chain, and the change id is not the last item of the
- * relation chain, there is a parent.
- */
- _calculateHasParent(
- currentChangeId: ChangeId,
- relatedChanges: RelatedChangeAndCommitInfo[]
- ) {
- return (
- relatedChanges.length > 0 &&
- relatedChanges[relatedChanges.length - 1].change_id !== currentChangeId
- );
- }
-
- _getServerConfig() {
- return this.restApiService.getConfig();
- }
-
- _computeChangeURL(
- changeNum: NumericChangeId,
- project: RepoName,
- patchNum?: PatchSetNum
- ) {
- return GerritNav.getUrlForChangeById(changeNum, project, patchNum);
+ return Promise.all(promises);
}
/**
@@ -232,8 +562,8 @@
* their numbers.
*/
_changesEqual(
- a: ChangeInfo | RelatedChangeAndCommitInfo,
- b: ChangeInfo | RelatedChangeAndCommitInfo
+ a?: ChangeInfo | RelatedChangeAndCommitInfo,
+ b?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
) {
const aNum = this._getChangeNumber(a);
const bNum = this._getChangeNumber(b);
@@ -246,7 +576,9 @@
* RelatedChangeAndCommitInfo (such as those included in a
* RelatedChangesInfo response).
*/
- _getChangeNumber(change?: ChangeInfo | RelatedChangeAndCommitInfo) {
+ _getChangeNumber(
+ change?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
+ ) {
// Default to 0 if change property is not defined.
if (!change) return 0;
@@ -256,136 +588,10 @@
return change._change_number;
}
- _computeLinkClass(change: ParsedChangeInfo) {
- const statuses = [];
- if (change.status === ChangeStatus.ABANDONED) {
- statuses.push('strikethrough');
- }
- if (change.submittable) {
- statuses.push('submittable');
- }
- return statuses.join(' ');
- }
-
- _computeChangeStatusClass(change: RelatedChangeAndCommitInfo) {
- const classes = ['status'];
- if (change._revision_number !== change._current_revision_number) {
- classes.push('notCurrent');
- } else if (this._isIndirectAncestor(change)) {
- classes.push('indirectAncestor');
- } else if (change.submittable) {
- classes.push('submittable');
- } else if (change.status === ChangeStatus.NEW) {
- classes.push('hidden');
- }
- return classes.join(' ');
- }
-
- _computeChangeStatus(change: RelatedChangeAndCommitInfo) {
- switch (change.status) {
- case ChangeStatus.MERGED:
- return 'Merged';
- case ChangeStatus.ABANDONED:
- return 'Abandoned';
- }
- if (change._revision_number !== change._current_revision_number) {
- return 'Not current';
- } else if (this._isIndirectAncestor(change)) {
- return 'Indirect ancestor';
- } else if (change.submittable) {
- return 'Submittable';
- }
- return '';
- }
-
- /** @override */
- connectedCallback() {
- super.connectedCallback();
- // We listen to `new-section-loaded` events to allow plugins to trigger
- // visibility computations, if their content or visibility changed.
- this.addEventListener('new-section-loaded', () =>
- this._handleNewSectionLoaded()
- );
- }
-
- _handleNewSectionLoaded() {
- // A plugin sent a `new-section-loaded` event, so its visibility likely
- // changed. Hence, we update our visibility if needed.
- this._resultsChanged(
- this._relatedResponse,
- this._submittedTogether,
- this._conflicts,
- this._cherryPicks,
- this._sameTopic
- );
- }
-
- @observe(
- '_relatedResponse',
- '_submittedTogether',
- '_conflicts',
- '_cherryPicks',
- '_sameTopic'
- )
- _resultsChanged(
- related: RelatedChangesInfo,
- submittedTogether: SubmittedTogetherInfo | undefined,
- conflicts: ChangeInfo[],
- cherryPicks: ChangeInfo[],
- sameTopic?: ChangeInfo[]
- ) {
- if (!submittedTogether || !sameTopic) {
- return;
- }
- const submittedTogetherChangesCount =
- (submittedTogether.changes || []).length +
- (submittedTogether.non_visible_changes || 0);
- const results = [
- related && related.changes,
- // If there are either visible or non-visible changes, we need a
- // non-empty list to fire the event and set visibility.
- submittedTogetherChangesCount ? [{}] : [],
- conflicts,
- cherryPicks,
- sameTopic,
- ];
- for (let i = 0; i < results.length; i++) {
- if (results[i] && results[i].length > 0) {
- this.hidden = false;
- this.dispatchEvent(
- new CustomEvent('update', {
- composed: true,
- bubbles: false,
- })
- );
- return;
- }
- }
-
- this._computeHidden();
- }
-
- _computeHidden() {
- // None of the built-in change lists had elements. So all of them are
- // hidden. But since plugins might have injected visible content, we need
- // to check for that and stay visible if we find any such visible content.
- // (We consider plugins visible except if it's main element has the hidden
- // attribute set to true.)
- const plugins = getPluginEndpoints().getDetails('related-changes-section');
- this.hidden = !plugins.some(
- plugin =>
- !plugin.domHook ||
- plugin.domHook.getAllAttached().some(instance => !instance.hidden)
- );
- }
-
- _isIndirectAncestor(change: RelatedChangeAndCommitInfo) {
- return (
- this._connectedRevisions &&
- !this._connectedRevisions.includes(change.commit.commit)
- );
- }
-
+ /*
+ * A list of commit ids connected to change to understand if other change
+ * is direct or indirect ancestor / descendant.
+ */
_computeConnectedRevisions(
change?: ParsedChangeInfo,
patchNum?: PatchSetNum,
@@ -421,40 +627,121 @@
}
return connected;
}
+}
- _computeSubmittedTogetherClass(submittedTogether?: SubmittedTogetherInfo) {
- if (
- !submittedTogether ||
- (submittedTogether.changes.length === 0 &&
- !submittedTogether.non_visible_changes)
- ) {
- return 'hidden';
+@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 '';
+
+ return html`<div class="container">${title}${button}</div>
+ ${items}`;
}
- _computeNonVisibleChangesNote(n: number) {
- return `(+ ${pluralize(n, 'non-visible change')})`;
- }
-
- // TODO(milutin): Temporary for data collection, remove when data collected
- _reportClick(e: Event) {
- const target = e.target as HTMLAnchorElement | undefined;
- const section = target?.parentElement?.parentElement;
- const sectionName = section?.getElementsByTagName('h4')[0]?.innerText;
- const sectionLinks = [...(section?.getElementsByTagName('a') ?? [])];
- const currentChange = section
- ?.getElementsByClassName('arrowToCurrentChange')[0]
- ?.nextElementSibling?.nextElementSibling?.getElementsByTagName('a')[0];
-
- if (!target) return;
- this.reportingService.reportInteraction('related-change-click', {
- sectionName,
- index: sectionLinks.indexOf(target) + 1,
- countChanges: sectionLinks.length,
- currentChangeIndex: !currentChange
- ? undefined
- : sectionLinks.indexOf(currentChange) + 1,
+ 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',
});
}
}
@@ -462,5 +749,6 @@
declare global {
interface HTMLElementTagNameMap {
'gr-related-changes-list': GrRelatedChangesList;
+ 'gr-related-collapse': GrRelatedCollapse;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
deleted file mode 100644
index 2f53319..0000000
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
+++ /dev/null
@@ -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>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
index 631c077..ae9af4a 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -15,70 +15,314 @@
* 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,
- createCommit,
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,
- RepoName,
+ RelatedChangesInfo,
+ SubmittedTogetherInfo,
} from '../../../types/common';
-import './gr-related-changes-list';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+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 {query, queryAndAssert} from '../../../test/test-utils';
-import {GrRelatedChangesList} from './gr-related-changes-list';
-import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import './gr-related-changes-list';
+import {
+ ChangeMarkersInList,
+ GrRelatedChangesList,
+ GrRelatedCollapse,
+ Section,
+} from './gr-related-changes-list';
const pluginApi = _testOnly_initGerritPluginApi();
const basicFixture = fixtureFromElement('gr-related-changes-list');
-suite('gr-related-changes-list tests', () => {
+suite('gr-related-changes-list', () => {
let element: GrRelatedChangesList;
setup(() => {
- // Since pluginEndpoints are global, must reset state.
- _testOnly_resetEndpoints();
element = basicFixture.instantiate();
});
- // obsolete
- test('event for section loaded fires for each section ', () => {
- const loadedStub = sinon.stub();
- element.patchNum = 7 as PatchSetNum;
- element.change = {
- ...createParsedChange(),
- change_id: '123' as ChangeId,
- status: ChangeStatus.NEW,
- };
- element.mergeable = true;
- element.addEventListener('new-section-loaded', loadedStub);
+ 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), []);
+ }
- return element.reload().then(() => {
- assert.equal(loadedStub.callCount, 4);
+ 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'
+ );
});
});
- // trivial
- suite('getChangeConflicts resolves undefined', () => {
+ 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: GrRelatedChangesList;
+ let conflictsStub: SinonStubbedMember<RestApiService['getChangeConflicts']>;
setup(() => {
element = basicFixture.instantiate();
+ conflictsStub = stubRestApi('getChangeConflicts').returns(
+ Promise.resolve(undefined)
+ );
});
- test('_conflicts are an empty array', () => {
+ test('request conflicts if open and mergeable', () => {
element.patchNum = 7 as PatchSetNum;
element.change = {
...createParsedChange(),
@@ -87,383 +331,300 @@
};
element.mergeable = true;
element.reload();
- assert.equal(element._conflicts.length, 0);
+ 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);
});
});
- suite('hidden attribute and update event', () => {
- const changes: ChangeInfo[] = [
- {
- ...createChange(),
- project: 'foo/bar' as RepoName,
- change_id: 'Ideadbeef' as ChangeId,
- status: ChangeStatus.NEW,
+ 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),
},
- ];
- const relatedChanges: RelatedChangeAndCommitInfo[] = [
+ };
+ let patchNum = 7 as PatchSetNum;
+ let relatedChanges: RelatedChangeAndCommitInfo[] = [
{
- ...createCommitInfoWithRequiredCommit(),
- project: 'foo/bar' as RepoName,
- change_id: 'Ideadbeef' as ChangeId,
+ ...createRelatedChangeAndCommitInfo(),
commit: {
- ...createCommit(),
- commit: 'deadbeef' as CommitId,
+ ...createCommitInfoWithRequiredCommit(
+ '2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
+ ),
parents: [
{
- commit: 'abc123' as CommitId,
- subject: 'abc123',
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
+ subject: 'subject1',
},
],
- subject: 'do that thing',
},
- _change_number: 12345 as NumericChangeId,
- _revision_number: 1,
- _current_revision_number: 1,
- status: ChangeStatus.NEW,
+ },
+ {
+ ...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',
+ },
+ ],
+ },
},
];
- // obsolete
- test('clear and empties', () => {
- element._relatedResponse = {changes: relatedChanges};
- element._submittedTogether = {
- changes,
- non_visible_changes: 0,
- };
- element._conflicts = changes;
- element._cherryPicks = changes;
- element._sameTopic = changes;
-
- element.hidden = false;
- element.clear();
- assert.isTrue(element.hidden);
- assert.equal(element._relatedResponse.changes.length, 0);
- assert.equal(element._submittedTogether?.changes.length, 0);
- assert.equal(element._conflicts.length, 0);
- assert.equal(element._cherryPicks.length, 0);
- assert.equal(element._sameTopic?.length, 0);
- });
-
- // obsolete
- test('update fires', () => {
- const updateHandler = sinon.stub();
- element.addEventListener('update', updateHandler);
-
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isTrue(element.hidden);
- assert.isFalse(updateHandler.called);
-
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- changes
- );
- assert.isFalse(element.hidden);
- assert.isTrue(updateHandler.called);
- updateHandler.reset();
-
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isTrue(element.hidden);
- assert.isFalse(updateHandler.called);
-
- element._resultsChanged(
- {changes: []},
- {changes, non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isFalse(element.hidden);
- assert.isTrue(updateHandler.called);
- updateHandler.reset();
-
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 1},
- [],
- [],
- []
- );
- assert.isFalse(element.hidden);
- assert.isTrue(updateHandler.called);
- });
-
- suite('hiding and unhiding', () => {
- test('related response', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged(
- {changes: relatedChanges},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isFalse(element.hidden);
- });
-
- test('submitted together', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged(
- {changes: []},
- {changes, non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isFalse(element.hidden);
- });
-
- test('conflicts', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- changes,
- [],
- []
- );
- assert.isFalse(element.hidden);
- });
-
- test('cherrypicks', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- changes,
- []
- );
- assert.isFalse(element.hidden);
- });
-
- test('same topic', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- changes
- );
- assert.isFalse(element.hidden);
- });
- });
- });
-
- // trivial
- test('_computeChangeURL uses GerritNav', () => {
- const getUrlStub = sinon.stub(GerritNav, 'getUrlForChangeById');
- element._computeChangeURL(
- 123 as NumericChangeId,
- 'abc/def' as RepoName,
- 12 as PatchSetNum
+ let connectedChanges = element._computeConnectedRevisions(
+ change,
+ patchNum,
+ relatedChanges
);
- assert.isTrue(getUrlStub.called);
- });
+ assert.deepEqual(connectedChanges, [
+ '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ ]);
- // trivial
- suite('submitted together changes', () => {
- const change: ChangeInfo = {
- ...createChange(),
- project: 'foo/bar' as RepoName,
- change_id: 'Ideadbeef' as ChangeId,
- status: ChangeStatus.NEW,
- };
-
- test('_computeSubmittedTogetherClass', () => {
- assert.strictEqual(
- element._computeSubmittedTogetherClass(undefined),
- 'hidden'
- );
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [],
- non_visible_changes: 0,
- }),
- 'hidden'
- );
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [change],
- non_visible_changes: 0,
- }),
- ''
- );
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [],
- non_visible_changes: 0,
- }),
- 'hidden'
- );
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [],
- non_visible_changes: 1,
- }),
- ''
- );
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [],
- non_visible_changes: 1,
- }),
- ''
- );
- });
-
- test('no submitted together changes', () => {
- flush();
- assert.include(element.$.submittedTogether.className, 'hidden');
- });
-
- test('no non-visible submitted together changes', () => {
- element._submittedTogether = {changes: [change], non_visible_changes: 0};
- flush();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.isUndefined(query(element, '.note'));
- });
-
- test('no visible submitted together changes', () => {
- // Technically this should never happen, but worth asserting the logic.
- element._submittedTogether = {changes: [], non_visible_changes: 1};
- flush();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.strictEqual(
- queryAndAssert<HTMLDivElement>(element, '.note').innerText.trim(),
- '(+ 1 non-visible change)'
- );
- });
-
- test('visible and non-visible submitted together changes', () => {
- element._submittedTogether = {changes: [change], non_visible_changes: 2};
- flush();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.strictEqual(
- queryAndAssert<HTMLDivElement>(element, '.note').innerText.trim(),
- '(+ 2 non-visible changes)'
- );
- });
- });
-
- // obsolete
- test('hiding and unhiding', done => {
- element.change = {...createParsedChange(), labels: {}};
- let hookEl: HTMLElement;
- let plugin;
-
- // No changes, and no plugin. The element is still hidden.
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isTrue(element.hidden);
- pluginApi.install(
- p => {
- plugin = p;
- plugin
- .hook('related-changes-section')
- .getLastAttached()
- .then(el => (hookEl = el));
+ patchNum = 4 as PatchSetNum;
+ relatedChanges = [
+ {
+ ...createRelatedChangeAndCommitInfo(),
+ commit: {
+ ...createCommitInfoWithRequiredCommit(
+ '2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
+ ),
+ parents: [
+ {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
+ subject: 'My parent commit',
+ },
+ ],
+ },
},
- '0.1',
- 'http://some/plugins/url2.js'
+ {
+ ...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
);
- getPluginLoader().loadPlugins([]);
- flush(() => {
- // No changes, and plugin without hidden attribute. So it's visible.
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isFalse(element.hidden);
+ assert.deepEqual(connectedChanges, [
+ 'af815dac54318826b7f1fa468acc76349ffc588e',
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
+ ]);
+ });
- // No changes, but plugin with true hidden attribute. So it's invisible.
- hookEl.hidden = true;
+ suite('gr-related-changes-list plugin tests', () => {
+ let element: GrRelatedChangesList;
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isTrue(element.hidden);
+ setup(() => {
+ resetPlugins();
+ element = basicFixture.instantiate();
+ });
- // No changes, and plugin with false hidden attribute. So it's visible.
- hookEl.hidden = false;
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- []
- );
- assert.isFalse(element.hidden);
+ teardown(() => {
+ resetPlugins();
+ });
- // Hiding triggered by plugin itself
- hookEl.hidden = true;
- hookEl.dispatchEvent(
- new CustomEvent('new-section-loaded', {
- composed: true,
- bubbles: true,
- })
+ 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'
);
- assert.isTrue(element.hidden);
-
- // Unhiding triggered by plugin itself
- hookEl.hidden = false;
- hookEl.dispatchEvent(
- new CustomEvent('new-section-loaded', {
- composed: true,
- bubbles: true,
- })
- );
- assert.isFalse(element.hidden);
-
- // Hiding plugin keeps list visible, if there are changes
- hookEl.hidden = false;
- const change = createChange();
- element._sameTopic = [change];
- element._resultsChanged(
- {changes: []},
- {changes: [], non_visible_changes: 0},
- [],
- [],
- [change]
- );
- assert.isFalse(element.hidden);
- hookEl.hidden = true;
- hookEl.dispatchEvent(
- new CustomEvent('new-section-loaded', {
- composed: true,
- bubbles: true,
- })
- );
- assert.isFalse(element.hidden);
-
- done();
+ getPluginLoader().loadPlugins([]);
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element.change);
+ done();
+ });
});
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index 86934c2..adf4f71 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -41,6 +41,7 @@
import {isRemovableReviewer} from '../../../utils/change-util';
import {ReviewerState} from '../../../constants/constants';
import {appContext} from '../../../services/app-context';
+import {fireAlert} from '../../../utils/event-util';
@customElement('gr-reviewer-list')
export class GrReviewerList extends PolymerElement {
@@ -261,32 +262,37 @@
_handleRemove(e: Event) {
e.preventDefault();
const target = (dom(e) as EventApi).rootTarget as GrAccountChip;
- if (!target.account || !this.change) {
- return;
- }
+ if (!target.account || !this.change?.reviewers) return;
const accountID = target.account._account_id || target.account.email;
- this.disabled = true;
if (!accountID) return;
- this._xhrPromise = this._removeReviewer(accountID)
- .then((response: Response | undefined) => {
- this.disabled = false;
- if (!response || !response.ok) {
- return response;
+ const reviewers = this.change.reviewers;
+ let removedAccount: AccountInfo | undefined;
+ let removedType: ReviewerState | undefined;
+ for (const type of [ReviewerState.REVIEWER, ReviewerState.CC]) {
+ const reviewerStateByType = reviewers[type] || [];
+ reviewers[type] = reviewerStateByType;
+ for (let i = 0; i < reviewerStateByType.length; i++) {
+ if (
+ reviewerStateByType[i]._account_id === accountID ||
+ reviewerStateByType[i].email === accountID
+ ) {
+ removedAccount = reviewerStateByType[i];
+ removedType = type;
+ this.splice(`change.reviewers.${type}`, i, 1);
+ break;
}
- if (!this.change || !this.change.reviewers) return;
- const reviewers = this.change.reviewers;
- for (const type of [ReviewerState.REVIEWER, ReviewerState.CC]) {
- const reviewerStateByType = reviewers[type] || [];
- reviewers[type] = reviewerStateByType;
- for (let i = 0; i < reviewerStateByType.length; i++) {
- if (
- reviewerStateByType[i]._account_id === accountID ||
- reviewerStateByType[i].email === accountID
- ) {
- this.splice('change.reviewers.' + type, i, 1);
- break;
- }
- }
+ }
+ }
+ const curChange = this.change;
+ this.disabled = true;
+ this._xhrPromise = this._removeReviewer(accountID)
+ .then(response => {
+ this.disabled = false;
+ if (!this.change?.reviewers || this.change !== curChange) return;
+ if (!response?.ok) {
+ this.push(`change.reviewers.${removedType}`, removedAccount);
+ fireAlert(this, `Cannot remove a ${removedType}`);
+ return response;
}
return;
})
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 794e9c9..64179f7 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -84,8 +84,9 @@
type="radio"
on-click="_handleOnlyDrafts"
checked="[[_draftsOnly]]"
+ hidden$="[[!loggedIn]]"
/>
- <label for="draftsRadio">
+ <label for="draftsRadio" hidden$="[[!loggedIn]]">
Drafts ([[_countDrafts(threads)]])
</label>
<input
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
index 7b47bd7..1fe00ff 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
@@ -270,6 +270,17 @@
});
});
+ test('draft toggle only appears when logged in', () => {
+ element.loggedIn = false;
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#draftsRadio')).display,
+ 'none');
+ element.loggedIn = true;
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('#draftsRadio')).display,
+ 'none');
+ });
+
test('show all threads by default', () => {
assert.equal(dom(element.root)
.querySelectorAll('gr-comment-thread').length, element.threads.length);
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index d61dc36..03d50cc 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -60,7 +60,7 @@
import {toggleClass, whenVisible} from '../../utils/dom-util';
import {durationString} from '../../utils/date-util';
import {charsOnly, pluralize} from '../../utils/string-util';
-import {fireRunSelectionReset, isSelected} from './gr-checks-util';
+import {fireRunSelectionReset, isAttemptSelected} from './gr-checks-util';
import {ChecksTabState} from '../../types/events';
import {ConfigInfo, PatchSetNumber} from '../../types/common';
import {latestPatchNum$} from '../../services/change/change-model';
@@ -465,9 +465,17 @@
@internalProperty()
filterRegExp = new RegExp('');
+ /** All runs. Shown should only the selected/filtered ones. */
@property()
runs: CheckRun[] = [];
+ /**
+ * Check names of runs that are selected in the runs panel. When this array
+ * is empty, then no run is selected and all runs should be shown.
+ */
+ @property()
+ selectedRuns: string[] = [];
+
@property()
actions: Action[] = [];
@@ -483,14 +491,6 @@
@property()
latestPatchsetNumber: PatchSetNumber | undefined = undefined;
- /**
- * How many runs are selected in the runs panel?
- * If 0, then the `runs` property contains all the runs there are.
- * If >0, then it only contains the data of certain selected runs.
- */
- @property()
- selectedRunsCount = 0;
-
/** Maps checkName to selected attempt number. `undefined` means `latest`. */
@property()
selectedAttempts: Map<string, number | undefined> = new Map<
@@ -759,11 +759,19 @@
});
}
- renderFilter() {
- const runs = this.runs.filter(run =>
- isSelected(this.selectedAttempts, run)
+ isRunSelected(run: {checkName: string}) {
+ return (
+ this.selectedRuns.length === 0 ||
+ this.selectedRuns.includes(run.checkName)
);
- if (this.selectedRunsCount === 0 && allResults(runs).length <= 3) {
+ }
+
+ renderFilter() {
+ const runs = this.runs.filter(
+ run =>
+ this.isRunSelected(run) && isAttemptSelected(this.selectedAttempts, run)
+ );
+ if (this.selectedRuns.length === 0 && allResults(runs).length <= 3) {
if (this.filterRegExp.source.length > 0) {
this.filterRegExp = new RegExp('');
}
@@ -783,7 +791,7 @@
}
renderSelectionFilter() {
- const count = this.selectedRunsCount;
+ const count = this.selectedRuns.length;
if (count === 0) return;
return html`
<iron-icon class="filter" icon="gr-icons:filter"></iron-icon>
@@ -806,20 +814,23 @@
renderSection(category: Category | 'SUCCESS') {
const catString = category.toString().toLowerCase();
- let runs = this.runs.filter(run => isSelected(this.selectedAttempts, run));
+ let allRuns = this.runs.filter(run =>
+ isAttemptSelected(this.selectedAttempts, run)
+ );
if (category === 'SUCCESS') {
- runs = runs.filter(hasCompletedWithoutResults);
+ allRuns = allRuns.filter(hasCompletedWithoutResults);
} else {
- runs = runs.filter(r => hasResultsOf(r, category));
+ allRuns = allRuns.filter(r => hasResultsOf(r, category));
}
- const all = runs.reduce(
- (allResults: RunResult[], run) => [
- ...allResults,
+ const all = allRuns.reduce(
+ (results: RunResult[], run) => [
+ ...results,
...this.computeRunResults(category, run),
],
[]
);
- const filtered = all.filter(
+ const selected = all.filter(result => this.isRunSelected(result));
+ const filtered = selected.filter(
result =>
this.filterRegExp.test(result.checkName) ||
this.filterRegExp.test(result.summary)
@@ -827,7 +838,7 @@
let expanded = this.isSectionExpanded.get(category);
const expandedByUser = this.isSectionExpandedByUser.get(category) ?? false;
if (!expandedByUser || expanded === undefined) {
- expanded = all.length > 0;
+ expanded = selected.length > 0;
this.isSectionExpanded.set(category, expanded);
}
const expandedClass = expanded ? 'expanded' : 'collapsed';
@@ -844,22 +855,28 @@
class="statusIcon ${catString}"
></iron-icon>
<span class="title">${catString}</span>
- <span class="count">${this.renderCount(all, filtered)}</span>
+ <span class="count"
+ >${this.renderCount(all, selected, filtered)}</span
+ >
</h3>
- ${this.renderResults(all, filtered)}
+ ${this.renderResults(all, selected, filtered)}
</div>
`;
}
- renderResults(all: RunResult[], filtered: RunResult[]) {
- if (all.length === 0 && this.selectedRunsCount > 0) {
+ renderResults(
+ all: RunResult[],
+ selected: RunResult[],
+ filtered: RunResult[]
+ ) {
+ if (all.length === 0) {
+ return html`<div class="noResultsMessage">No results</div>`;
+ }
+ if (selected.length === 0) {
return html`<div class="noResultsMessage">
No results for this filtered view
</div>`;
}
- if (all.length === 0) {
- return html`<div class="noResultsMessage">No results</div>`;
- }
if (filtered.length === 0) {
return html`<div class="noResultsMessage">
No results match the regular expression
@@ -891,15 +908,14 @@
`;
}
- renderCount(all: RunResult[], filtered: RunResult[]) {
- if (this.selectedRunsCount > 0) {
- return html`<span class="filtered"> - filtered</span>`;
- }
+ renderCount(all: RunResult[], selected: RunResult[], filtered: RunResult[]) {
if (all.length === filtered.length) {
return html`(${all.length})`;
- } else {
- return html`(${filtered.length} of ${all.length})`;
}
+ if (all.length !== selected.length) {
+ return html`<span class="filtered"> - filtered</span>`;
+ }
+ return html`(${filtered.length} of ${all.length})`;
}
toggleExpanded(category: Category | 'SUCCESS') {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index beec4c8..acabb22 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -108,11 +108,6 @@
}
render() {
- const filteredRuns = this.runs.filter(
- r =>
- this.selectedRuns.length === 0 ||
- this.selectedRuns.includes(r.checkName)
- );
return html`
<div class="container">
<gr-checks-runs
@@ -127,8 +122,8 @@
<gr-checks-results
class="results"
.tabState="${this.tabState}"
- .runs="${filteredRuns}"
- .selectedRunsCount="${this.selectedRuns.length}"
+ .runs="${this.runs}"
+ .selectedRuns="${this.selectedRuns}"
.selectedAttempts="${this.selectedAttempts}"
@run-selected="${this.handleRunSelected}"
></gr-checks-results>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-util.ts b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
index 9a68e5b..05a87a4 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-util.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
@@ -76,7 +76,7 @@
);
}
-export function isSelected(
+export function isAttemptSelected(
selectedAttempts: Map<string, number | undefined>,
run: CheckRun
) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index 77fe299..bf4805c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -18,6 +18,7 @@
import '@polymer/paper-card/paper-card';
import '@polymer/paper-checkbox/paper-checkbox';
import '@polymer/paper-dropdown-menu/paper-dropdown-menu';
+import '@polymer/paper-fab/paper-fab';
import '@polymer/paper-item/paper-item';
import '@polymer/paper-listbox/paper-listbox';
import './gr-overview-image';
@@ -36,7 +37,14 @@
import {classMap} from 'lit-html/directives/class-map';
import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
-import {Dimensions, fitToFrame, FrameConstrainer, Point, Rect} from './util';
+import {
+ createEvent,
+ Dimensions,
+ fitToFrame,
+ FrameConstrainer,
+ Point,
+ Rect,
+} from './util';
const DRAG_DEAD_ZONE_PIXELS = 5;
@@ -197,12 +205,27 @@
}
#version-switcher {
display: flex;
+ align-items: center;
margin: var(--spacing-xl);
}
#version-switcher paper-button {
- flex-basis: 0;
flex-grow: 1;
margin: 0;
+ /*
+ The floating action button below overlaps part of the version buttons.
+ This min-width ensures the button text still appears somewhat balanced.
+ */
+ min-width: 7rem;
+ }
+ #version-switcher paper-fab {
+ /* Round button overlaps Base and Revision buttons. */
+ z-index: 10;
+ margin: 0 -12px;
+ /* Styled as an outlined button. */
+ color: var(--primary-button-background-color);
+ border: 1px solid var(--primary-button-background-color);
+ --paper-fab-background: var(--primary-background-color);
+ --paper-fab-keyboard-focus-background: var(--primary-background-color);
}
#version-explanation {
color: var(--deemphasized-text-color);
@@ -253,6 +276,8 @@
>
Base
</paper-button>
+ <paper-fab mini icon="gr-icons:swapHoriz" @click="${this.toggleImage}">
+ </paper-fab>
<paper-button
class="right"
?unelevated=${!this.baseSelected}
@@ -335,13 +360,15 @@
paper-button.left {
--paper-button: {
border-radius: 4px 0 0 4px;
- border-width: 1px 0 1px 1px;
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(--primary-button-background-color);
}
}
paper-button.left[outlined] {
--paper-button: {
border-radius: 4px 0 0 4px;
- border-width: 1px 0 1px 1px;
+ border-width: 1px;
border-style: solid;
border-color: var(--primary-button-background-color);
}
@@ -349,13 +376,15 @@
paper-button.right {
--paper-button: {
border-radius: 0 4px 4px 0;
- border-width: 1px 1px 1px 0;
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(--primary-button-background-color);
}
}
paper-button.right[outlined] {
--paper-button: {
border-radius: 0 4px 4px 0;
- border-width: 1px 1px 1px 0;
+ border-width: 1px;
border-style: solid;
border-color: var(--primary-button-background-color);
}
@@ -439,11 +468,17 @@
selectBase() {
if (!this.baseUrl) return;
this.baseSelected = true;
+ this.dispatchEvent(
+ createEvent({type: 'version-switcher-clicked', button: 'base'})
+ );
}
selectRevision() {
if (!this.revisionUrl) return;
this.baseSelected = false;
+ this.dispatchEvent(
+ createEvent({type: 'version-switcher-clicked', button: 'revision'})
+ );
}
toggleImage() {
@@ -457,16 +492,25 @@
if (!value) return;
if (value === 'fit') {
this.scaledSelected = true;
+ this.dispatchEvent(
+ createEvent({type: 'zoom-level-changed', scale: 'fit'})
+ );
}
if (value > 0) {
this.scaledSelected = false;
this.scale = value;
+ this.dispatchEvent(
+ createEvent({type: 'zoom-level-changed', scale: value})
+ );
}
this.updateSizes();
}
followMouseChanged() {
this.followMouse = !this.followMouse;
+ this.dispatchEvent(
+ createEvent({type: 'follow-mouse-changed', value: this.followMouse})
+ );
}
mousedownMagnifier(event: MouseEvent) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
index aae559d..d7b6916 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
@@ -25,8 +25,9 @@
query,
} from 'lit-element';
import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
+import {ImageDiffAction} from '../../../api/diff';
-import {Dimensions, fitToFrame, Point, Rect} from './util';
+import {createEvent, Dimensions, fitToFrame, Point, Rect} from './util';
/**
* Displays a scaled-down version of an image with a draggable frame for
@@ -179,9 +180,12 @@
this.overlay.addEventListener('mousemove', (event: MouseEvent) =>
this.maybeDragFrame(event)
);
- this.overlay.addEventListener('mouseleave', (event: MouseEvent) =>
- this.releaseFrame(event)
- );
+ this.overlay.addEventListener('mouseleave', (event: MouseEvent) => {
+ // Ignore mouseleave events that are due to closeOverlay() calls.
+ if (this.overlay?.style.display !== 'none') {
+ this.releaseFrame(event);
+ }
+ });
this.overlay.addEventListener('mouseup', (event: MouseEvent) =>
this.releaseFrame(event)
);
@@ -254,6 +258,12 @@
releaseFrame(event: MouseEvent) {
event.preventDefault();
+
+ const detail: ImageDiffAction = {
+ type: this.dragging ? 'overview-frame-dragged' : 'overview-image-clicked',
+ };
+ this.dispatchEvent(createEvent(detail));
+
this.dragging = false;
this.closeOverlay();
this.grabOffset = {x: 0, y: 0};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts
index b42eea9..7036ce4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts
@@ -15,6 +15,8 @@
* limitations under the License.
*/
+import {ImageDiffAction} from '../../../api/diff';
+
export interface Point {
x: number;
y: number;
@@ -234,3 +236,13 @@
};
}
}
+
+export function createEvent(
+ detail: ImageDiffAction
+): CustomEvent<ImageDiffAction> {
+ return new CustomEvent('image-diff-action', {
+ detail,
+ bubbles: true,
+ composed: true,
+ });
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index dc27a49..2957320 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -140,6 +140,8 @@
<g id="download"><path d="M0 0h24v24H0z" fill="none"/><path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z"/></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#system_update-->
<g id="system-update"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z"/></g>
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Aswap_horiz-->
+ <g id="swapHoriz"><path d="M0 0h24v24H0z" fill="none"/><path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z"/></g>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index 8dcb80e..33e1afef 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -240,6 +240,10 @@
license: SharedLicenses.Polymer2015
},
{
+ name: "@polymer/paper-fab",
+ license: SharedLicenses.Polymer2015
+ },
+ {
name: "@polymer/paper-icon-button",
license: SharedLicenses.Polymer2015
},
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index c943bff..bd0ee89 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -22,6 +22,7 @@
"@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.2.0",
+ "@polymer/paper-fab": "^3.0.1",
"@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1",
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 0f229df..324a912 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -528,6 +528,7 @@
real_author?: AccountInfo;
date: Timestamp;
message: string;
+ accountsInMessage?: AccountInfo[];
tag?: ReviewInputTag;
_revision_number?: PatchSetNum;
}
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 77d3972..5240c8c 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -270,6 +270,17 @@
"@polymer/paper-styles" "^3.0.0-pre.26"
"@polymer/polymer" "^3.3.1"
+"@polymer/paper-fab@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@polymer/paper-fab/-/paper-fab-3.0.1.tgz#2636359e7fb70dd5a549ed92ba9b3bdb9ff86bf8"
+ integrity sha512-LO8ckgd72MnAtC1WiPd5CFR27WC/dEuY/lOIQuHYdEjwI62+iiV7Bmr7uoQ9wvvV71qMFdMIOyq/03KklsuAzw==
+ dependencies:
+ "@polymer/iron-flex-layout" "^3.0.0-pre.26"
+ "@polymer/iron-icon" "^3.0.0-pre.26"
+ "@polymer/paper-behaviors" "^3.0.0-pre.27"
+ "@polymer/paper-styles" "^3.0.0-pre.26"
+ "@polymer/polymer" "^3.0.0"
+
"@polymer/paper-icon-button@^3.0.0-pre.26":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@polymer/paper-icon-button/-/paper-icon-button-3.0.2.tgz#a1254faadc2c8dd135ce1ae33bcc161a94c31f65"