github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/components/product-builder.js (about) 1 /** 2 * Copyright 2018 The WPT Dashboard Project. All rights reserved. 3 * Use of this source code is governed by a BSD-style license that can be 4 * found in the LICENSE file. 5 */ 6 7 import '../node_modules/@polymer/paper-card/paper-card.js'; 8 import '../node_modules/@polymer/paper-dropdown-menu/paper-dropdown-menu.js'; 9 import '../node_modules/@polymer/paper-icon-button/paper-icon-button.js'; 10 import '../node_modules/@polymer/paper-input/paper-input.js'; 11 import '../node_modules/@polymer/paper-item/paper-icon-item.js'; 12 import '../node_modules/@polymer/paper-item/paper-item.js'; 13 import '../node_modules/@polymer/paper-listbox/paper-listbox.js'; 14 import '../node_modules/@polymer/polymer/lib/elements/dom-if.js'; 15 import '../node_modules/@polymer/polymer/lib/elements/dom-repeat.js'; 16 import { html } from '../node_modules/@polymer/polymer/polymer-element.js'; 17 import { PolymerElement } from '../node_modules/@polymer/polymer/polymer-element.js'; 18 import './display-logo.js'; 19 import './browser-picker.js'; 20 import { Channels, DefaultBrowserNames, ProductInfo, SemanticLabels, Sources } from './product-info.js'; 21 22 class ProductBuilder extends ProductInfo(PolymerElement) { 23 static get template() { 24 return html` 25 <style> 26 paper-icon-button { 27 float: right; 28 } 29 display-logo[small] { 30 margin-top: 16px; 31 } 32 .source { 33 height: 24px; 34 width: 24px; 35 } 36 </style> 37 <paper-card> 38 <div class="card-content"> 39 <paper-icon-button icon="delete" onclick="[[deleteProduct]]"></paper-icon-button> 40 41 <display-logo product="[[_product]]"></display-logo> 42 <template is="dom-if" if="[[debug]]"> 43 [[spec]] 44 </template> 45 46 <br> 47 <browser-picker browser="{{browserName}}" products="[[allProducts]]"></browser-picker> 48 49 <br> 50 <paper-dropdown-menu label="Channel" no-animations> 51 <paper-listbox slot="dropdown-content" selected="{{ _channel }}" attr-for-selected="value"> 52 <paper-item value="any">Any</paper-item> 53 <template is="dom-repeat" items="[[channels]]" as="channel"> 54 <paper-icon-item value="[[channel]]"> 55 <display-logo slot="item-icon" product="[[productWithChannel(_product, channel)]]" small></display-logo> 56 [[displayName(channel)]] 57 </paper-icon-item> 58 </template> 59 </paper-listbox> 60 </paper-dropdown-menu> 61 62 <br> 63 <paper-dropdown-menu label="Source" no-animations> 64 <paper-listbox slot="dropdown-content" selected="{{ _source }}" attr-for-selected="value"> 65 <paper-item value="any">Any</paper-item> 66 <template is="dom-repeat" items="[[sources]]" as="source"> 67 <paper-icon-item value="[[source]]"> 68 <img slot="item-icon" class="source" src="/static/[[source]].svg"> 69 [[displayName(source)]] 70 </paper-icon-item> 71 </template> 72 </paper-listbox> 73 </paper-dropdown-menu> 74 75 <br> 76 <paper-input-container always-float-label> 77 <label slot="label">Version</label> 78 <input slot="input" placeholder="(Any version)" list="versions-datalist" value="{{ browserVersion::input }}"> 79 <datalist id="versions-datalist"></datalist> 80 </paper-input-container> 81 </div></paper-card> 82 `; 83 } 84 85 static get is() { 86 return 'product-builder'; 87 } 88 89 static get properties() { 90 return { 91 browserName: { 92 type: String, 93 value: DefaultBrowserNames[0], 94 notify: true, 95 }, 96 browserVersion: { 97 type: String, 98 value: '', 99 notify: true, 100 }, 101 labels: { 102 type: Array, 103 value: [], 104 notify: true, 105 observer: 'labelsChanged', 106 }, 107 /* 108 _product is a local re-aggregation of the fields, used for 109 display-logo, and notifying parents of changes. 110 */ 111 _product: { 112 type: Object, 113 computed: 'computeProduct(browserName, browserVersion, labels)', 114 notify: true, 115 }, 116 _channel: { 117 type: String, 118 value: 'any', 119 observer: 'semanticLabelChanged', 120 }, 121 _source: { 122 type: String, 123 value: 'any', 124 observer: 'semanticLabelChanged', 125 }, 126 spec: { 127 type: String, 128 computed: 'computeSpec(_product)', 129 }, 130 debug: { 131 type: Boolean, 132 value: false, 133 }, 134 onDelete: Function, 135 onProductChanged: Function, 136 channels: { 137 type: Array, 138 value: Array.from(Channels), 139 }, 140 sources: { 141 type: Array, 142 value: Array.from(Sources), 143 }, 144 versionsURL: { 145 type: String, 146 computed: 'computeVersionsURL(_product)', 147 observer: 'versionsURLUpdated', 148 }, 149 versions: { 150 type: Array, 151 }, 152 versionsAutocomplete: { 153 type: Array, 154 observer: 'versionsAutocompleteUpdated' 155 }, 156 }; 157 } 158 159 constructor() { 160 super(); 161 this.deleteProduct = () => { 162 this.onDelete && this.onDelete(this.product); 163 }; 164 this._createMethodObserver('versionsUpdated(browserVersion, versions)'); 165 } 166 167 computeProduct(browserName, browserVersion, labels) { 168 const product = { 169 browser_name: browserName, 170 browser_version: browserVersion, 171 labels: labels, 172 }; 173 this.onProductChanged && this.onProductChanged(product); 174 return product; 175 } 176 177 computeSpec(product) { 178 return this.getSpec(product); 179 } 180 181 labelsChanged(labels) { 182 // Configure the channel from the labels. 183 labels = new Set(labels || []); 184 for (const semantic of SemanticLabels) { 185 const value = Array.from(semantic.values).find(c => labels.has(c)) || 'any'; 186 if (this[semantic.property] !== value) { 187 this[semantic.property] = value; 188 } 189 } 190 } 191 192 semanticLabelChanged(newValue, oldValue) { 193 // Configure the labels from the semantic label's value. 194 const isAny = !newValue || newValue === 'any'; 195 let labels = Array.from(this.labels || []); 196 if (oldValue) { 197 labels = labels.filter(l => l !== oldValue); 198 } 199 if (!isAny && !labels.includes(newValue)) { 200 labels.push(newValue); 201 } else if (!oldValue) { 202 return; 203 } 204 this.labels = labels; 205 } 206 207 productWithChannel(product, channel) { 208 return Object.assign({}, product, { 209 labels: (product.labels || []).filter(l => !Channels.has(l)).concat(channel) 210 }); 211 } 212 213 // Respond to product spec changing by computing a new versions URL. 214 computeVersionsURL(product) { 215 product = Object.assign({}, product); 216 delete product.browser_version; 217 const url = new URL('/api/versions', window.location); 218 url.searchParams.set('product', this.getSpec(product)); 219 return url; 220 } 221 222 // Respond to version URL changing by fetching the versions 223 versionsURLUpdated(url, urlBefore) { 224 if (!url || urlBefore === url) { 225 return; 226 } 227 fetch(url).then(r => r.json()).then(v => { 228 this.versions = v; 229 }); 230 } 231 232 // Respond to newly fetched versions, or user input, by filtering the autocomplete list. 233 versionsUpdated(version, versions) { 234 if (!versions || !versions.length) { 235 this.versionsAutocomplete = []; 236 return; 237 } 238 if (version) { 239 versions = versions.filter(s => s.startsWith(version)); 240 } 241 versions = versions.slice(0, 10); 242 // Check actually different from current. 243 const current = new Set(this.versionsAutocomplete || []); 244 if (current.size === versions.length && !versions.find(v => !current.has(v))) { 245 return; 246 } 247 this.versionsAutocomplete = versions; 248 } 249 250 versionsAutocompleteUpdated(versionsAutocomplete) { 251 const datalist = this.shadowRoot.querySelector('datalist'); 252 datalist.innerHTML = ''; 253 for (const sha of versionsAutocomplete) { 254 const option = document.createElement('option'); 255 option.setAttribute('value', sha); 256 datalist.appendChild(option); 257 } 258 } 259 } 260 261 window.customElements.define(ProductBuilder.is, ProductBuilder);