Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-detail/mr-issue-page/mr-issue-header.js b/static_src/elements/issue-detail/mr-issue-page/mr-issue-header.js
new file mode 100644
index 0000000..0d04d32
--- /dev/null
+++ b/static_src/elements/issue-detail/mr-issue-page/mr-issue-header.js
@@ -0,0 +1,379 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+
+import 'elements/issue-detail/mr-flipper/mr-flipper.js';
+import 'elements/chops/chops-dialog/chops-dialog.js';
+import 'elements/chops/chops-timestamp/chops-timestamp.js';
+import {store, connectStore} from 'reducers/base.js';
+import * as issueV0 from 'reducers/issueV0.js';
+import * as userV0 from 'reducers/userV0.js';
+import * as projectV0 from 'reducers/projectV0.js';
+import {userIsMember} from 'shared/helpers.js';
+import {SHARED_STYLES} from 'shared/shared-styles.js';
+import 'elements/framework/links/mr-user-link/mr-user-link.js';
+import 'elements/framework/links/mr-crbug-link/mr-crbug-link.js';
+import 'elements/framework/mr-pref-toggle/mr-pref-toggle.js';
+import 'elements/framework/mr-dropdown/mr-dropdown.js';
+import {ISSUE_EDIT_PERMISSION, ISSUE_DELETE_PERMISSION,
+ ISSUE_FLAGSPAM_PERMISSION} from 'shared/consts/permissions.js';
+import {issueToIssueRef} from 'shared/convertersV0.js';
+import {prpcClient} from 'prpc-client-instance.js';
+import {AVAILABLE_MD_PROJECTS, DEFAULT_MD_PROJECTS} from 'shared/md-helper.js';
+
+const DELETE_ISSUE_CONFIRMATION_NOTICE = `\
+Normally, you would just close issues by setting their status to a closed value.
+Are you sure you want to delete this issue?`;
+
+
+/**
+ * `<mr-issue-header>`
+ *
+ * The header for a given launch issue.
+ *
+ */
+export class MrIssueHeader extends connectStore(LitElement) {
+ /** @override */
+ static get styles() {
+ return [
+ SHARED_STYLES,
+ css`
+ :host {
+ width: 100%;
+ margin-top: 0;
+ font-size: var(--chops-large-font-size);
+ background-color: var(--monorail-metadata-toggled-bg);
+ border-bottom: var(--chops-normal-border);
+ padding: 0.25em 8px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+ h1 {
+ font-size: 100%;
+ line-height: 140%;
+ font-weight: bolder;
+ padding: 0;
+ margin: 0;
+ }
+ mr-flipper {
+ border-left: var(--chops-normal-border);
+ padding-left: 8px;
+ margin-left: 4px;
+ font-size: var(--chops-main-font-size);
+ }
+ mr-pref-toggle {
+ margin-right: 2px;
+ }
+ .issue-actions {
+ min-width: fit-content;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-size: var(--chops-main-font-size);
+ }
+ .issue-actions div {
+ min-width: 70px;
+ display: flex;
+ justify-content: space-between;
+ }
+ .spam-notice {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1px 6px;
+ border-radius: 3px;
+ background: #F44336;
+ color: var(--chops-white);
+ font-weight: bold;
+ font-size: var(--chops-main-font-size);
+ margin-right: 4px;
+ }
+ .byline {
+ display: block;
+ font-size: var(--chops-main-font-size);
+ width: 100%;
+ line-height: 140%;
+ color: var(--chops-primary-font-color);
+ }
+ .role-label {
+ background-color: var(--chops-gray-600);
+ border-radius: 3px;
+ color: var(--chops-white);
+ display: inline-block;
+ padding: 2px 4px;
+ font-size: 75%;
+ font-weight: bold;
+ line-height: 14px;
+ vertical-align: text-bottom;
+ margin-left: 16px;
+ }
+ .main-text-outer {
+ flex-basis: 100%;
+ display: flex;
+ justify-content: flex-start;
+ flex-direction: row;
+ align-items: center;
+ }
+ .main-text {
+ flex-basis: 100%;
+ }
+ @media (max-width: 840px) {
+ :host {
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+ .main-text {
+ width: 100%;
+ margin-bottom: 0.5em;
+ }
+ }
+ `,
+ ];
+ }
+
+ /** @override */
+ render() {
+ const reporterIsMember = userIsMember(
+ this.issue.reporterRef, this.issue.projectName, this.usersProjects);
+ const markdownEnabled = AVAILABLE_MD_PROJECTS.has(this.projectName);
+ const markdownDefaultOn = DEFAULT_MD_PROJECTS.has(this.projectName);
+ return html`
+ <div class="main-text-outer">
+ <div class="main-text">
+ <h1>
+ ${this.issue.isSpam ? html`
+ <span class="spam-notice">Spam</span>
+ `: ''}
+ Issue ${this.issue.localId}: ${this.issue.summary}
+ </h1>
+ <small class="byline">
+ Reported by
+ <mr-user-link
+ .userRef=${this.issue.reporterRef}
+ aria-label="issue reporter"
+ ></mr-user-link>
+ on <chops-timestamp .timestamp=${this.issue.openedTimestamp}></chops-timestamp>
+ ${reporterIsMember ? html`
+ <span class="role-label">Project Member</span>` : ''}
+ </small>
+ </div>
+ </div>
+ <div class="issue-actions">
+ <div>
+ <mr-crbug-link .issue=${this.issue}></mr-crbug-link>
+ <mr-pref-toggle
+ .userDisplayName=${this.userDisplayName}
+ label="Code"
+ title="Code font"
+ prefName="code_font"
+ ></mr-pref-toggle>
+ ${markdownEnabled ? html`
+ <mr-pref-toggle
+ .userDisplayName=${this.userDisplayName}
+ initialValue=${markdownDefaultOn}
+ label="Markdown"
+ title="Render in markdown"
+ prefName="render_markdown"
+ ></mr-pref-toggle> ` : ''}
+ </div>
+ ${this._issueOptions.length ? html`
+ <mr-dropdown
+ .items=${this._issueOptions}
+ icon="more_vert"
+ label="Issue options"
+ ></mr-dropdown>
+ ` : ''}
+ <mr-flipper></mr-flipper>
+ </div>
+ `;
+ }
+
+ /** @override */
+ static get properties() {
+ return {
+ userDisplayName: {type: String},
+ issue: {type: Object},
+ issuePermissions: {type: Object},
+ isRestricted: {type: Boolean},
+ projectTemplates: {type: Array},
+ projectName: {type: String},
+ usersProjects: {type: Object},
+ _action: {type: String},
+ _targetProjectError: {type: String},
+ };
+ }
+
+ /** @override */
+ constructor() {
+ super();
+ this.issuePermissions = [];
+ this.projectTemplates = [];
+ this.projectName = '';
+ this.issue = {};
+ this.usersProjects = new Map();
+ this.isRestricted = false;
+ }
+
+ /** @override */
+ stateChanged(state) {
+ this.issue = issueV0.viewedIssue(state);
+ this.issuePermissions = issueV0.permissions(state);
+ this.projectTemplates = projectV0.viewedTemplates(state);
+ this.projectName = projectV0.viewedProjectName(state);
+ this.usersProjects = userV0.projectsPerUser(state);
+
+ const restrictions = issueV0.restrictions(state);
+ this.isRestricted = restrictions && Object.keys(restrictions).length;
+ }
+
+ /**
+ * @return {Array<MenuItem>} Actions the user can take on the issue.
+ * @private
+ */
+ get _issueOptions() {
+ // We create two edit Arrays for the top and bottom half of the menu,
+ // to be separated by a separator in the UI.
+ const editOptions = [];
+ const riskyOptions = [];
+ const isSpam = this.issue.isSpam;
+ const isRestricted = this.isRestricted;
+
+ const permissions = this.issuePermissions;
+ const templates = this.projectTemplates;
+
+
+ if (permissions.includes(ISSUE_EDIT_PERMISSION)) {
+ editOptions.push({
+ text: 'Edit issue description',
+ handler: this._openEditDescription.bind(this),
+ });
+ if (templates.length) {
+ riskyOptions.push({
+ text: 'Convert issue template',
+ handler: this._openConvertIssue.bind(this),
+ });
+ }
+ }
+
+ if (permissions.includes(ISSUE_DELETE_PERMISSION)) {
+ riskyOptions.push({
+ text: 'Delete issue',
+ handler: this._deleteIssue.bind(this),
+ });
+ if (!isRestricted) {
+ editOptions.push({
+ text: 'Move issue',
+ handler: this._openMoveCopyIssue.bind(this, 'Move'),
+ });
+ editOptions.push({
+ text: 'Copy issue',
+ handler: this._openMoveCopyIssue.bind(this, 'Copy'),
+ });
+ }
+ }
+
+ if (permissions.includes(ISSUE_FLAGSPAM_PERMISSION)) {
+ const text = (isSpam ? 'Un-flag' : 'Flag') + ' issue as spam';
+ riskyOptions.push({
+ text,
+ handler: this._markIssue.bind(this),
+ });
+ }
+
+ if (editOptions.length && riskyOptions.length) {
+ editOptions.push({separator: true});
+ }
+ return editOptions.concat(riskyOptions);
+ }
+
+ /**
+ * Marks an issue as either spam or not spam based on whether the issue
+ * was spam.
+ */
+ _markIssue() {
+ prpcClient.call('monorail.Issues', 'FlagIssues', {
+ issueRefs: [{
+ projectName: this.issue.projectName,
+ localId: this.issue.localId,
+ }],
+ flag: !this.issue.isSpam,
+ }).then(() => {
+ store.dispatch(issueV0.fetch({
+ projectName: this.issue.projectName,
+ localId: this.issue.localId,
+ }));
+ });
+ }
+
+ /**
+ * Deletes an issue.
+ */
+ _deleteIssue() {
+ const ok = confirm(DELETE_ISSUE_CONFIRMATION_NOTICE);
+ if (ok) {
+ const issueRef = issueToIssueRef(this.issue);
+ // TODO(crbug.com/monorail/7374): Delete for the v0 -> v3 migration.
+ prpcClient.call('monorail.Issues', 'DeleteIssue', {
+ issueRef,
+ delete: true,
+ }).then(() => {
+ store.dispatch(issueV0.fetch(issueRef));
+ });
+ }
+ }
+
+ /**
+ * Launches the dialog to edit an issue's description.
+ * @fires CustomEvent#open-dialog
+ * @private
+ */
+ _openEditDescription() {
+ this.dispatchEvent(new CustomEvent('open-dialog', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ dialogId: 'edit-description',
+ fieldName: '',
+ },
+ }));
+ }
+
+ /**
+ * Opens dialog to either move or copy an issue.
+ * @param {"move"|"copy"} action
+ * @fires CustomEvent#open-dialog
+ * @private
+ */
+ _openMoveCopyIssue(action) {
+ this.dispatchEvent(new CustomEvent('open-dialog', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ dialogId: 'move-copy-issue',
+ action,
+ },
+ }));
+ }
+
+ /**
+ * Opens dialog for converting an issue.
+ * @fires CustomEvent#open-dialog
+ * @private
+ */
+ _openConvertIssue() {
+ this.dispatchEvent(new CustomEvent('open-dialog', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ dialogId: 'convert-issue',
+ },
+ }));
+ }
+}
+
+customElements.define('mr-issue-header', MrIssueHeader);