| /** |
| * @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 {classMap} from 'lit-html/directives/class-map'; |
| import {GrLitElement} from '../../lit/gr-lit-element'; |
| import {customElement, property, css} 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 {getRevisionKey, isChangeInfo} from '../../../utils/change-util'; |
| |
| /** What is the maximum number of shown changes in collapsed list? */ |
| const MAX_CHANGES_WHEN_COLLAPSED = 3; |
| |
| @customElement('gr-related-changes-list-experimental') |
| export class GrRelatedChangesListExperimental extends GrLitElement { |
| @property() |
| change?: ParsedChangeInfo; |
| |
| @property({type: String}) |
| patchNum?: PatchSetNum; |
| |
| @property() |
| _submittedTogether?: SubmittedTogetherInfo = { |
| changes: [], |
| non_visible_changes: 0, |
| }; |
| |
| @property() |
| _relatedResponse?: RelatedChangesInfo = {changes: []}; |
| |
| private readonly restApiService = appContext.restApiService; |
| |
| 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; |
| } |
| /* This is a hacky solution from old gr-related-change-list |
| * TODO(milutin): find layout without needing it |
| */ |
| h4:before, |
| gr-related-change:before { |
| content: ' '; |
| flex-shrink: 0; |
| width: 1.2em; |
| } |
| .note { |
| color: var(--error-text-color); |
| } |
| `, |
| ]; |
| } |
| |
| render() { |
| const relatedChanges = this._relatedResponse?.changes ?? []; |
| let showWhenCollapsedPredicate = this.showWhenCollapsedPredicateFactory( |
| relatedChanges.length, |
| relatedChanges.findIndex(relatedChange => |
| this._changesEqual(relatedChange, this.change) |
| ) |
| ); |
| const connectedRevisions = this._computeConnectedRevisions( |
| this.change, |
| this.patchNum, |
| relatedChanges |
| ); |
| const relatedChangeSection = html` <section |
| class="relatedChanges" |
| ?hidden=${!relatedChanges.length} |
| > |
| <h4 class="title">Relation chain</h4> |
| <gr-related-collapse .length=${relatedChanges.length}> |
| ${relatedChanges.map( |
| (change, index) => |
| html`<gr-related-change |
| class="${classMap({ |
| ['show-when-collapsed']: showWhenCollapsedPredicate(index), |
| })}" |
| .isCurrentChange="${this._changesEqual(change, this.change)}" |
| .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; |
| showWhenCollapsedPredicate = this.showWhenCollapsedPredicateFactory( |
| submittedTogetherChanges.length, |
| submittedTogetherChanges.findIndex(relatedChange => |
| this._changesEqual(relatedChange, this.change) |
| ) |
| ); |
| const submittedTogetherSection = html`<section |
| id="submittedTogether" |
| ?hidden=${!submittedTogetherChanges?.length && |
| !this._submittedTogether?.non_visible_changes} |
| > |
| <h4 class="title">Submitted together</h4> |
| <gr-related-collapse .length=${submittedTogetherChanges.length}> |
| ${submittedTogetherChanges.map( |
| (change, index) => |
| html`<gr-related-change |
| class="${classMap({ |
| ['show-when-collapsed']: showWhenCollapsedPredicate(index), |
| })}" |
| .currentChange="${this._changesEqual(change, this.change)}" |
| .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>`; |
| |
| return html`${relatedChangeSection}${submittedTogetherSection}`; |
| } |
| |
| showWhenCollapsedPredicateFactory(length: number, highlightIndex: number) { |
| return (index: number) => { |
| if (highlightIndex === 0) return index <= MAX_CHANGES_WHEN_COLLAPSED - 1; |
| if (highlightIndex === length - 1) |
| return index >= length - MAX_CHANGES_WHEN_COLLAPSED; |
| return ( |
| highlightIndex - MAX_CHANGES_WHEN_COLLAPSED + 2 <= index && |
| index <= highlightIndex + MAX_CHANGES_WHEN_COLLAPSED - 2 |
| ); |
| }; |
| } |
| |
| reload() { |
| if (!this.change) return Promise.reject(new Error('change missing')); |
| if (!this.patchNum) return Promise.reject(new Error('patchNum missing')); |
| const promises: Array<Promise<void>> = [ |
| this.restApiService |
| .getRelatedChanges(this.change._number, this.patchNum) |
| .then(response => { |
| if (!response) { |
| throw new Error('getRelatedChanges returned undefined response'); |
| } |
| this._relatedResponse = response; |
| }), |
| this.restApiService |
| .getChangesSubmittedTogether(this.change._number) |
| .then(response => { |
| this._submittedTogether = response; |
| }), |
| ]; |
| |
| 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() |
| showAll = false; |
| |
| @property() |
| length = 0; |
| |
| static get styles() { |
| return [ |
| sharedStyles, |
| css` |
| gr-button { |
| display: flex; |
| } |
| gr-button:before { |
| content: ' '; |
| flex-shrink: 0; |
| width: 1.2em; |
| } |
| .collapsed ::slotted(gr-related-change.show-when-collapsed) { |
| display: flex; |
| } |
| .collapsed ::slotted(gr-related-change) { |
| display: none; |
| } |
| ::slotted(gr-related-change) { |
| display: flex; |
| } |
| `, |
| ]; |
| } |
| |
| render() { |
| const collapsible = this.length > MAX_CHANGES_WHEN_COLLAPSED; |
| const items = html` <div |
| class="${!this.showAll && collapsible ? 'collapsed' : ''}" |
| > |
| <slot></slot> |
| </div>`; |
| let button = nothing; |
| if (collapsible) { |
| if (this.showAll) { |
| button = html`<gr-button link="" @click="${this.toggle}" |
| >Show less</gr-button |
| >`; |
| } else { |
| button = html`<gr-button link="" @click="${this.toggle}" |
| >+ ${this.length - MAX_CHANGES_WHEN_COLLAPSED} more</gr-button |
| >`; |
| } |
| } |
| |
| return html`${items}${button}`; |
| } |
| |
| private toggle(e: MouseEvent) { |
| e.stopPropagation(); |
| this.showAll = !this.showAll; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-related-changes-list-experimental': GrRelatedChangesListExperimental; |
| 'gr-related-collapse': GrRelatedCollapse; |
| } |
| } |