Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-detail/dialogs/mr-edit-description/mr-edit-description.js b/static_src/elements/issue-detail/dialogs/mr-edit-description/mr-edit-description.js
new file mode 100644
index 0000000..2a34b8f
--- /dev/null
+++ b/static_src/elements/issue-detail/dialogs/mr-edit-description/mr-edit-description.js
@@ -0,0 +1,340 @@
+// 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/framework/mr-upload/mr-upload.js';
+import 'elements/framework/mr-error/mr-error.js';
+import {fieldTypes} from 'shared/issue-fields.js';
+import {store, connectStore} from 'reducers/base.js';
+import * as issueV0 from 'reducers/issueV0.js';
+import * as projectV0 from 'reducers/projectV0.js';
+import * as userV0 from 'reducers/userV0.js';
+import 'elements/chops/chops-checkbox/chops-checkbox.js';
+import 'elements/chops/chops-dialog/chops-dialog.js';
+import {SHARED_STYLES, MD_PREVIEW_STYLES, MD_STYLES} from 'shared/shared-styles.js';
+import {commentListToDescriptionList} from 'shared/convertersV0.js';
+import {renderMarkdown, shouldRenderMarkdown} from 'shared/md-helper.js';
+import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
+
+
+/**
+ * `<mr-edit-description>`
+ *
+ * A dialog to edit descriptions.
+ *
+ */
+export class MrEditDescription extends connectStore(LitElement) {
+ /** @override */
+ constructor() {
+ super();
+ this._editedDescription = '';
+ }
+
+ /** @override */
+ static get styles() {
+ return [
+ SHARED_STYLES,
+ MD_PREVIEW_STYLES,
+ MD_STYLES,
+ css`
+ chops-dialog {
+ --chops-dialog-width: 800px;
+ }
+ textarea {
+ font-family: var(--mr-toggled-font-family);
+ min-height: 300px;
+ max-height: 500px;
+ border: var(--chops-accessible-border);
+ padding: 0.5em 4px;
+ margin: 0.5em 0;
+ }
+ .attachments {
+ margin: 0.5em 0;
+ }
+ .content {
+ padding: 0.5em 0.5em;
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .edit-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ `,
+ ];
+ }
+
+ /** @override */
+ render() {
+ return html`
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
+ rel="stylesheet">
+ <chops-dialog aria-labelledby="editDialogTitle">
+ <h3 id="editDialogTitle" class="medium-heading">
+ Edit ${this._title}
+ </h3>
+ <textarea
+ id="description"
+ class="content"
+ @keyup=${this._setEditedDescription}
+ @change=${this._setEditedDescription}
+ .value=${this._editedDescription}
+ ></textarea>
+ ${this._renderMarkdown ? html`
+ <div class="markdown-preview preview-height-description">
+ <div class="markdown">
+ ${unsafeHTML(renderMarkdown(this._editedDescription))}
+ </div>
+ </div>`: ''}
+ <h3 class="medium-heading">
+ Add attachments
+ </h3>
+ <div class="attachments">
+ ${this._attachments && this._attachments.map((attachment) => html`
+ <label>
+ <chops-checkbox
+ type="checkbox"
+ checked="true"
+ class="kept-attachment"
+ data-attachment-id=${attachment.attachmentId}
+ @checked-change=${this._keptAttachmentIdsChanged}
+ />
+ <a href=${attachment.viewUrl} target="_blank">
+ ${attachment.filename}
+ </a>
+ </label>
+ <br>
+ `)}
+ <mr-upload></mr-upload>
+ </div>
+ <mr-error
+ ?hidden=${!this._attachmentError}
+ >${this._attachmentError}</mr-error>
+ <div class="edit-controls">
+ <chops-checkbox
+ id="sendEmail"
+ ?checked=${this._sendEmail}
+ @checked-change=${this._setSendEmail}
+ >Send email</chops-checkbox>
+ <div>
+ <chops-button id="discard" @click=${this.cancel} class="de-emphasized">
+ Discard
+ </chops-button>
+ <chops-button id="save" @click=${this.save} class="emphasized">
+ Save changes
+ </chops-button>
+ </div>
+ </div>
+ </chops-dialog>
+ `;
+ }
+
+ /** @override */
+ static get properties() {
+ return {
+ commentsByApproval: {type: Array},
+ issueRef: {type: Object},
+ fieldName: {type: String},
+ projectName: {type: String},
+ _attachmentError: {type: String},
+ _attachments: {type: Array},
+ _boldLines: {type: Array},
+ _editedDescription: {type: String},
+ _title: {type: String},
+ _keptAttachmentIds: {type: Object},
+ _sendEmail: {type: Boolean},
+ _prefs: {type: Object},
+ };
+ }
+
+ /** @override */
+ stateChanged(state) {
+ this.commentsByApproval = issueV0.commentsByApprovalName(state);
+ this.issueRef = issueV0.viewedIssueRef(state);
+ this.projectName = projectV0.viewedProjectName(state);
+ this._prefs = userV0.prefs(state);
+ }
+
+ /**
+ * Public function to open the issue description editing dialog.
+ * @param {Event} e
+ */
+ async open(e) {
+ await this.updateComplete;
+ this.shadowRoot.querySelector('chops-dialog').open();
+ this.fieldName = e.detail.fieldName;
+ this.reset();
+ }
+
+ /**
+ * Resets edit form.
+ */
+ async reset() {
+ await this.updateComplete;
+ this._attachmentError = '';
+ this._attachments = [];
+ this._boldLines = [];
+ this._keptAttachmentIds = new Set();
+
+ const uploader = this.shadowRoot.querySelector('mr-upload');
+ if (uploader) {
+ uploader.reset();
+ }
+
+ // Sets _editedDescription and _title.
+ this._initializeView(this.commentsByApproval, this.fieldName);
+
+ this.shadowRoot.querySelectorAll('.kept-attachment').forEach((checkbox) => {
+ checkbox.checked = true;
+ });
+ this.shadowRoot.querySelector('#sendEmail').checked = true;
+
+ this._sendEmail = true;
+ }
+
+ /**
+ * Cancels in-flight edit data.
+ */
+ async cancel() {
+ await this.updateComplete;
+ this.shadowRoot.querySelector('chops-dialog').close();
+ }
+
+ /**
+ * Sends the user's edit to Monorail's backend to be saved.
+ */
+ async save() {
+ const commentContent = this._markupNewContent();
+ const sendEmail = this._sendEmail;
+ const keptAttachments = Array.from(this._keptAttachmentIds);
+ const message = {
+ issueRef: this.issueRef,
+ isDescription: true,
+ commentContent,
+ keptAttachments,
+ sendEmail,
+ };
+
+ try {
+ const uploader = this.shadowRoot.querySelector('mr-upload');
+ const uploads = await uploader.loadFiles();
+ if (uploads && uploads.length) {
+ message.uploads = uploads;
+ }
+
+ if (!this.fieldName) {
+ store.dispatch(issueV0.update(message));
+ } else {
+ // This is editing an approval if there is no field name.
+ message.fieldRef = {
+ type: fieldTypes.APPROVAL_TYPE,
+ fieldName: this.fieldName,
+ };
+ store.dispatch(issueV0.updateApproval(message));
+ }
+ this.shadowRoot.querySelector('chops-dialog').close();
+ } catch (e) {
+ this._attachmentError = `Error while loading file for attachment: ${
+ e.message}`;
+ }
+ }
+
+ /**
+ * Getter for checking if the user has Markdown enabled.
+ * @return {boolean} Whether Markdown preview should be rendered or not.
+ */
+ get _renderMarkdown() {
+ const enabled = this._prefs.get('render_markdown');
+ return shouldRenderMarkdown({project: this.projectName, enabled});
+ }
+
+ /**
+ * Event handler for keeping <mr-edit-description>'s copy of
+ * _editedDescription in sync.
+ * @param {Event} e
+ */
+ _setEditedDescription(e) {
+ const target = e.target;
+ this._editedDescription = target.value;
+ }
+
+ /**
+ * Event handler for keeping attachment state in sync.
+ * @param {Event} e
+ */
+ _keptAttachmentIdsChanged(e) {
+ e.target.checked = e.detail.checked;
+ const attachmentId = Number.parseInt(e.target.dataset.attachmentId);
+ if (e.target.checked) {
+ this._keptAttachmentIds.add(attachmentId);
+ } else {
+ this._keptAttachmentIds.delete(attachmentId);
+ }
+ }
+
+ _initializeView(commentsByApproval, fieldName) {
+ this._title = fieldName ? `${fieldName} Survey` : 'Description';
+ const key = fieldName || '';
+ if (!commentsByApproval || !commentsByApproval.has(key)) return;
+ const comments = commentListToDescriptionList(commentsByApproval.get(key));
+
+ const comment = comments[comments.length - 1];
+
+ if (comment.attachments) {
+ this._keptAttachmentIds = new Set(comment.attachments.map(
+ (attachment) => Number.parseInt(attachment.attachmentId)));
+ this._attachments = comment.attachments;
+ }
+
+ this._processRawContent(comment.content);
+ }
+
+ _processRawContent(content) {
+ const chunks = content.trim().split(/(<b>[^<\n]+<\/b>)/m);
+ const boldLines = [];
+ let cleanContent = '';
+ chunks.forEach((chunk) => {
+ if (chunk.startsWith('<b>') && chunk.endsWith('</b>')) {
+ const cleanChunk = chunk.slice(3, -4).trim();
+ cleanContent += cleanChunk;
+ // Don't add whitespace to boldLines.
+ if (/\S/.test(cleanChunk)) {
+ boldLines.push(cleanChunk);
+ }
+ } else {
+ cleanContent += chunk;
+ }
+ });
+
+ this._boldLines = boldLines;
+ this._editedDescription = cleanContent;
+ }
+
+ _markupNewContent() {
+ const lines = this._editedDescription.trim().split('\n');
+ const markedLines = lines.map((line) => {
+ let markedLine = line;
+ const matchingBoldLine = this._boldLines.find(
+ (boldLine) => (line.startsWith(boldLine)));
+ if (matchingBoldLine) {
+ markedLine =
+ `<b>${matchingBoldLine}</b>${line.slice(matchingBoldLine.length)}`;
+ }
+ return markedLine;
+ });
+ return markedLines.join('\n');
+ }
+
+ /**
+ * Event handler for keeping email state in sync.
+ * @param {Event} e
+ */
+ _setSendEmail(e) {
+ this._sendEmail = e.detail.checked;
+ }
+}
+
+customElements.define('mr-edit-description', MrEditDescription);