Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-detail/mr-flipper/mr-flipper.js b/static_src/elements/issue-detail/mr-flipper/mr-flipper.js
new file mode 100644
index 0000000..8159e01
--- /dev/null
+++ b/static_src/elements/issue-detail/mr-flipper/mr-flipper.js
@@ -0,0 +1,151 @@
+// 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 qs from 'qs';
+import {connectStore} from 'reducers/base.js';
+import * as sitewide from 'reducers/sitewide.js';
+import {SHARED_STYLES} from 'shared/shared-styles.js';
+
+/**
+ * Class for displaying a single flipper.
+ * @extends {LitElement}
+ */
+export default class MrFlipper extends connectStore(LitElement) {
+ /** @override */
+ static get properties() {
+ return {
+ currentIndex: {type: Number},
+ totalCount: {type: Number},
+ prevUrl: {type: String},
+ nextUrl: {type: String},
+ listUrl: {type: String},
+ queryParams: {type: Object},
+ };
+ }
+
+ /** @override */
+ constructor() {
+ super();
+ this.currentIndex = null;
+ this.totalCount = null;
+ this.prevUrl = null;
+ this.nextUrl = null;
+ this.listUrl = null;
+
+ this.queryParams = {};
+ }
+
+ /** @override */
+ stateChanged(state) {
+ this.queryParams = sitewide.queryParams(state);
+ }
+
+ /** @override */
+ updated(changedProperties) {
+ if (changedProperties.has('queryParams')) {
+ this.fetchFlipperData(qs.stringify(this.queryParams));
+ }
+ }
+
+ // Eventually this should be replaced with pRPC.
+ fetchFlipperData(query) {
+ const options = {
+ credentials: 'include',
+ method: 'GET',
+ };
+ fetch(`detail/flipper?${query}`, options).then(
+ (response) => response.text(),
+ ).then(
+ (responseBody) => {
+ let responseData;
+ try {
+ // Strip XSSI prefix from response.
+ responseData = JSON.parse(responseBody.substr(5));
+ } catch (e) {
+ console.error(`Error parsing JSON response for flipper: ${e}`);
+ return;
+ }
+ this._populateResponseData(responseData);
+ },
+ );
+ }
+
+ _populateResponseData(data) {
+ this.totalCount = data.total_count;
+ this.currentIndex = data.cur_index;
+ this.prevUrl = data.prev_url;
+ this.nextUrl = data.next_url;
+ this.listUrl = data.list_url;
+ }
+
+ /** @override */
+ static get styles() {
+ return [
+ SHARED_STYLES,
+ css`
+ :host {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ }
+ /* Use visibility instead of display:hidden for hiding in order to
+ * avoid popping when elements are made visible. */
+ .row a[hidden], .counts[hidden] {
+ visibility: hidden;
+ }
+ .counts[hidden] {
+ display: block;
+ }
+ .row a {
+ display: block;
+ padding: 0.25em 0;
+ }
+ .row a, .row div {
+ flex: 1;
+ white-space: nowrap;
+ padding: 0 2px;
+ }
+ .row .counts {
+ padding: 0 16px;
+ }
+ .row {
+ display: flex;
+ align-items: baseline;
+ text-align: center;
+ flex-direction: row;
+ }
+ @media (max-width: 960px) {
+ :host {
+ display: inline-block;
+ }
+ }
+ `,
+ ];
+ }
+
+ /** @override */
+ render() {
+ return html`
+ <div class="row">
+ <a href="${this.prevUrl}" ?hidden="${!this.prevUrl}" title="Prev" class="prev-url">
+ ‹ Prev
+ </a>
+ <div class="counts" ?hidden=${!this.totalCount}>
+ ${this.currentIndex + 1} of ${this.totalCount}
+ </div>
+ <a href="${this.nextUrl}" ?hidden="${!this.nextUrl}" title="Next" class="next-url">
+ Next ›
+ </a>
+ </div>
+ <div class="row">
+ <a href="${this.listUrl}" ?hidden="${!this.listUrl}" title="Back to list" class="list-url">
+ Back to list
+ </a>
+ </div>
+ `;
+ }
+}
+
+window.customElements.define('mr-flipper', MrFlipper);
diff --git a/static_src/elements/issue-detail/mr-flipper/mr-flipper.test.js b/static_src/elements/issue-detail/mr-flipper/mr-flipper.test.js
new file mode 100644
index 0000000..183a8d5
--- /dev/null
+++ b/static_src/elements/issue-detail/mr-flipper/mr-flipper.test.js
@@ -0,0 +1,75 @@
+// 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 {assert} from 'chai';
+import MrFlipper from './mr-flipper.js';
+import sinon from 'sinon';
+
+const xssiPrefix = ')]}\'';
+
+let element;
+
+describe('mr-flipper', () => {
+ beforeEach(() => {
+ element = document.createElement('mr-flipper');
+ document.body.appendChild(element);
+
+ sinon.stub(window, 'fetch');
+
+ const response = new window.Response(`${xssiPrefix}{"message": "Ok"}`, {
+ status: 201,
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+ window.fetch.returns(Promise.resolve(response));
+ });
+
+ afterEach(() => {
+ document.body.removeChild(element);
+
+ window.fetch.restore();
+ });
+
+ it('initializes', () => {
+ assert.instanceOf(element, MrFlipper);
+ });
+
+ it('renders links', async () => {
+ // Test DOM after properties are updated.
+ element._populateResponseData({
+ cur_index: 4,
+ total_count: 13,
+ prev_url: 'http://prevurl/',
+ next_url: 'http://nexturl/',
+ list_url: 'http://listurl/',
+ });
+
+ await element.updateComplete;
+
+ const prevUrlEl = element.shadowRoot.querySelector('a.prev-url');
+ const nextUrlEl = element.shadowRoot.querySelector('a.next-url');
+ const listUrlEl = element.shadowRoot.querySelector('a.list-url');
+ const countsEl = element.shadowRoot.querySelector('div.counts');
+
+ assert.equal(prevUrlEl.href, 'http://prevurl/');
+ assert.equal(nextUrlEl.href, 'http://nexturl/');
+ assert.equal(listUrlEl.href, 'http://listurl/');
+ assert.include(countsEl.innerText, '5 of 13');
+ });
+
+ it('fetches flipper data when queryParams change', async () => {
+ await element.updateComplete;
+
+ sinon.stub(element, 'fetchFlipperData');
+
+ element.queryParams = {id: 21, q: 'owner:me'};
+
+ sinon.assert.notCalled(element.fetchFlipperData);
+
+ await element.updateComplete;
+
+ sinon.assert.calledWith(element.fetchFlipperData, 'id=21&q=owner%3Ame');
+ });
+});