| /** |
| * @license |
| * Copyright (C) 2016 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 '../../shared/gr-download-commands/gr-download-commands'; |
| import {changeBaseURL, getRevisionKey} from '../../../utils/change-util'; |
| import {ChangeInfo, DownloadInfo, PatchSetNum} from '../../../types/common'; |
| import {GrDownloadCommands} from '../../shared/gr-download-commands/gr-download-commands'; |
| import {GrButton} from '../../shared/gr-button/gr-button'; |
| import {hasOwnProperty, queryAndAssert} from '../../../utils/common-util'; |
| import {GrOverlayStops} from '../../shared/gr-overlay/gr-overlay'; |
| import {fireAlert, fireEvent} from '../../../utils/event-util'; |
| import {addShortcut} from '../../../utils/dom-util'; |
| import {fontStyles} from '../../../styles/gr-font-styles'; |
| import {sharedStyles} from '../../../styles/shared-styles'; |
| import {LitElement, PropertyValues, html, css} from 'lit'; |
| import {customElement, property, state, query} from 'lit/decorators'; |
| import {assertIsDefined} from '../../../utils/common-util'; |
| import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs'; |
| import {BindValueChangeEvent} from '../../../types/events'; |
| |
| @customElement('gr-download-dialog') |
| export class GrDownloadDialog extends LitElement { |
| /** |
| * Fired when the user presses the close button. |
| * |
| * @event close |
| */ |
| |
| @query('#download') protected download?: HTMLAnchorElement; |
| |
| @query('#downloadCommands') protected downloadCommands?: GrDownloadCommands; |
| |
| @query('#closeButton') protected closeButton?: GrButton; |
| |
| @property({type: Object}) |
| change: ChangeInfo | undefined; |
| |
| @property({type: Object}) |
| config?: DownloadInfo; |
| |
| @property({type: String}) |
| patchNum: PatchSetNum | undefined; |
| |
| @state() private selectedScheme?: string; |
| |
| /** Called in disconnectedCallback. */ |
| private cleanups: (() => void)[] = []; |
| |
| override disconnectedCallback() { |
| super.disconnectedCallback(); |
| for (const cleanup of this.cleanups) cleanup(); |
| this.cleanups = []; |
| } |
| |
| override connectedCallback() { |
| super.connectedCallback(); |
| for (const key of ['1', '2', '3', '4', '5']) { |
| this.cleanups.push( |
| addShortcut(this, {key}, e => this.handleNumberKey(e)) |
| ); |
| } |
| } |
| |
| static override get styles() { |
| return [ |
| fontStyles, |
| sharedStyles, |
| css` |
| :host { |
| display: block; |
| padding: var(--spacing-m) 0; |
| } |
| section { |
| display: flex; |
| padding: var(--spacing-m) var(--spacing-xl); |
| } |
| .flexContainer { |
| display: flex; |
| justify-content: space-between; |
| padding-top: var(--spacing-m); |
| } |
| .footer { |
| justify-content: flex-end; |
| } |
| .closeButtonContainer { |
| align-items: flex-end; |
| display: flex; |
| flex: 0; |
| justify-content: flex-end; |
| } |
| .patchFiles, |
| .archivesContainer { |
| padding-bottom: var(--spacing-m); |
| } |
| .patchFiles { |
| margin-right: var(--spacing-xxl); |
| } |
| .patchFiles a, |
| .archives a { |
| display: inline-block; |
| margin-right: var(--spacing-l); |
| } |
| .patchFiles a:last-of-type, |
| .archives a:last-of-type { |
| margin-right: 0; |
| } |
| gr-download-commands { |
| width: min(80vw, 1200px); |
| } |
| `, |
| ]; |
| } |
| |
| override render() { |
| const revisions = this.change?.revisions; |
| return html` |
| <section> |
| <h3 class="heading-3"> |
| Patch set ${this.patchNum} of |
| ${revisions ? Object.keys(revisions).length : 0} |
| </h3> |
| </section> |
| ${this.renderDownloadCommands()} |
| <section class="flexContainer"> |
| ${this.renderPatchFiles()} ${this.renderArchives()} |
| </section> |
| <section class="footer"> |
| <span class="closeButtonContainer"> |
| <gr-button |
| id="closeButton" |
| link |
| @click=${(e: Event) => { |
| this.handleCloseTap(e); |
| }} |
| >Close</gr-button |
| > |
| </span> |
| </section> |
| `; |
| } |
| |
| private renderDownloadCommands() { |
| const cssClass = this.schemes.length ? '' : 'hidden'; |
| |
| return html` |
| <section class=${cssClass}> |
| <gr-download-commands |
| id="downloadCommands" |
| .commands=${this.computeDownloadCommands()} |
| .schemes=${this.schemes} |
| .selectedScheme=${this.selectedScheme} |
| show-keyboard-shortcut-tooltips |
| @selected-scheme-changed=${(e: BindValueChangeEvent) => { |
| this.selectedScheme = e.detail.value; |
| }} |
| ></gr-download-commands> |
| </section> |
| `; |
| } |
| |
| private renderPatchFiles() { |
| if (this.computeHidePatchFile()) return; |
| |
| return html` |
| <div class="patchFiles"> |
| <label>Patch file</label> |
| <div> |
| <a id="download" .href=${this.computeDownloadLink()} download> |
| ${this.computeDownloadFilename()} |
| </a> |
| <a .href=${this.computeDownloadLink(true)} download> |
| ${this.computeDownloadFilename(true)} |
| </a> |
| </div> |
| </div> |
| `; |
| } |
| |
| private renderArchives() { |
| if (!this.config?.archives.length) return; |
| |
| return html` |
| <div class="archivesContainer"> |
| <label>Archive</label> |
| <div id="archives" class="archives"> |
| ${this.config.archives.map(format => this.renderArchivesLink(format))} |
| </div> |
| </div> |
| `; |
| } |
| |
| private renderArchivesLink(format: string) { |
| return html` |
| <a .href=${this.computeArchiveDownloadLink(format)} download> |
| ${format} |
| </a> |
| `; |
| } |
| |
| override firstUpdated(changedProperties: PropertyValues) { |
| super.firstUpdated(changedProperties); |
| if (!this.getAttribute('role')) this.setAttribute('role', 'dialog'); |
| } |
| |
| override willUpdate(changedProperties: PropertyValues) { |
| if (changedProperties.has('change') || changedProperties.has('patchNum')) { |
| this.schemesChanged(); |
| } |
| } |
| |
| get schemes() { |
| if (this.change === undefined || this.patchNum === undefined) { |
| return []; |
| } |
| |
| for (const rev of Object.values(this.change.revisions || {})) { |
| if (rev._number === this.patchNum) { |
| const fetch = rev.fetch; |
| if (fetch) { |
| return Object.keys(fetch).sort(); |
| } |
| break; |
| } |
| } |
| return []; |
| } |
| |
| private handleNumberKey(e: KeyboardEvent) { |
| const index = Number(e.key) - 1; |
| const commands = this.computeDownloadCommands(); |
| if (index > commands.length) return; |
| navigator.clipboard.writeText(commands[index].command).then(() => { |
| fireAlert(this, `${commands[index].title} command copied to clipboard`); |
| fireEvent(this, 'close'); |
| }); |
| } |
| |
| override focus() { |
| if (this.schemes.length) { |
| assertIsDefined(this.downloadCommands, 'downloadCommands'); |
| this.updateComplete.then(() => this.downloadCommands!.focusOnCopy()); |
| } else { |
| assertIsDefined(this.download, 'download'); |
| this.download.focus(); |
| } |
| } |
| |
| getFocusStops(): GrOverlayStops { |
| assertIsDefined(this.downloadCommands, 'downloadCommands'); |
| assertIsDefined(this.closeButton, 'closeButton'); |
| const downloadTabs = queryAndAssert<PaperTabsElement>( |
| this.downloadCommands, |
| '#downloadTabs' |
| ); |
| return { |
| start: downloadTabs, |
| end: this.closeButton, |
| }; |
| } |
| |
| private computeDownloadCommands() { |
| let commandObj; |
| if (!this.change || !this.selectedScheme) return []; |
| for (const rev of Object.values(this.change.revisions || {})) { |
| if ( |
| rev._number === this.patchNum && |
| rev && |
| rev.fetch && |
| hasOwnProperty(rev.fetch, this.selectedScheme) |
| ) { |
| commandObj = rev.fetch[this.selectedScheme].commands; |
| break; |
| } |
| } |
| const commands = []; |
| for (const [title, command] of Object.entries(commandObj ?? {})) { |
| commands.push({title, command}); |
| } |
| return commands; |
| } |
| |
| private computeDownloadLink(zip?: boolean) { |
| if (this.change === undefined || this.patchNum === undefined) { |
| return ''; |
| } |
| return ( |
| changeBaseURL(this.change.project, this.change._number, this.patchNum) + |
| '/patch?' + |
| (zip ? 'zip' : 'download') |
| ); |
| } |
| |
| private computeDownloadFilename(zip?: boolean) { |
| if (this.change === undefined || this.patchNum === undefined) { |
| return ''; |
| } |
| |
| const rev = getRevisionKey(this.change, this.patchNum) ?? ''; |
| const shortRev = rev.substr(0, 7); |
| |
| return shortRev + '.diff.' + (zip ? 'zip' : 'base64'); |
| } |
| |
| // private but used in test |
| computeHidePatchFile() { |
| if (this.change === undefined || this.patchNum === undefined) { |
| return false; |
| } |
| for (const rev of Object.values(this.change.revisions || {})) { |
| if (rev._number === this.patchNum) { |
| const parentLength = |
| rev.commit && rev.commit.parents ? rev.commit.parents.length : 0; |
| return parentLength === 0 || parentLength > 1; |
| } |
| } |
| return false; |
| } |
| |
| // private but used in test |
| computeArchiveDownloadLink(format?: string) { |
| if ( |
| this.change === undefined || |
| this.patchNum === undefined || |
| format === undefined |
| ) { |
| return ''; |
| } |
| return ( |
| changeBaseURL(this.change.project, this.change._number, this.patchNum) + |
| '/archive?format=' + |
| format |
| ); |
| } |
| |
| private handleCloseTap(e: Event) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| fireEvent(this, 'close'); |
| } |
| |
| private schemesChanged() { |
| if (this.schemes.length === 0) { |
| return; |
| } |
| if (!this.selectedScheme || !this.schemes.includes(this.selectedScheme)) { |
| this.selectedScheme = this.schemes.sort()[0]; |
| } |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-download-dialog': GrDownloadDialog; |
| } |
| } |