blob: 5c49d63fc986da4c1f751c80c695e09c72c4bcb8 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001// Copyright 2019 The Chromium Authors
Copybara854996b2021-09-07 19:36:02 +00002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import './mr-grid-tile.js';
6
7import {css, html, LitElement} from 'lit-element';
8import qs from 'qs';
9import {connectStore} from 'reducers/base.js';
10import * as projectV0 from 'reducers/projectV0.js';
11import {issueRefToUrl} from 'shared/convertersV0.js';
12import {setHasAny} from 'shared/helpers.js';
13import {EMPTY_FIELD_VALUE} from 'shared/issue-fields.js';
14import {SHARED_STYLES} from 'shared/shared-styles.js';
15
16import {extractGridData, makeGridCellKey} from './extract-grid-data.js';
17
18const PROPERTIES_TRIGGERING_GROUPING = Object.freeze([
19 'xField',
20 'yField',
21 'issues',
22 '_extractFieldValuesFromIssue',
23 '_extractTypeForFieldName',
24 '_statusDefs',
25]);
26
27/**
28 * <mr-grid>
29 *
30 * A grid of issues grouped optionally horizontally and vertically.
31 *
32 * Throughout the file 'x' corresponds to column headers and 'y' corresponds to
33 * row headers.
34 *
35 * @extends {LitElement}
36 */
37export class MrGrid extends connectStore(LitElement) {
38 /** @override */
39 render() {
40 return html`
41 <table>
42 <tr>
43 <th>&nbsp</th>
44 ${this._xHeadings.map((heading) => html`
45 <th>${heading}</th>`)}
46 </tr>
47 ${this._yHeadings.map((yHeading) => html`
48 <tr>
49 <th>${yHeading}</th>
50 ${this._xHeadings.map((xHeading) => html`
51 ${this._renderCell(xHeading, yHeading)}`)}
52 </tr>
53 `)}
54 </table>
55 `;
56 }
57 /**
58 *
59 * @param {string} xHeading
60 * @param {string} yHeading
61 * @return {TemplateResult}
62 */
63 _renderCell(xHeading, yHeading) {
64 const cell = this._groupedIssues.get(makeGridCellKey(xHeading, yHeading));
65 if (!cell) {
66 return html`<td></td>`;
67 }
68
69 const cellMode = this.cellMode.toLowerCase();
70 let content;
71 if (cellMode === 'ids') {
72 content = html`
73 ${cell.map((issue) => html`
74 <mr-issue-link
75 .projectName=${this.projectName}
76 .issue=${issue}
77 .text=${issue.localId}
78 .queryParams=${this.queryParams}
79 ></mr-issue-link>
80 `)}
81 `;
82 } else if (cellMode === 'counts') {
83 const itemCount = cell.length;
84 if (itemCount === 1) {
85 const issue = cell[0];
86 content = html`
87 <a href=${issueRefToUrl(issue, this.queryParams)} class="counts">
88 1 item
89 </a>
90 `;
91 } else {
92 content = html`
93 <a href=${this._formatListUrl(xHeading, yHeading)} class="counts">
94 ${itemCount} items
95 </a>
96 `;
97 }
98 } else {
99 // Default to tiles.
100 content = html`
101 ${cell.map((issue) => html`
102 <mr-grid-tile
103 .issue=${issue}
104 .queryParams=${this.queryParams}
105 ></mr-grid-tile>
106 `)}
107 `;
108 }
109 return html`<td>${content}</td>`;
110 }
111
112 /**
113 * Creates a URL to the list view for the group of issues corresponding to
114 * the given headings.
115 *
116 * @param {string} xHeading
117 * @param {string} yHeading
118 * @return {string}
119 */
120 _formatListUrl(xHeading, yHeading) {
121 let url = 'list?';
122 const params = Object.assign({}, this.queryParams);
123 params.mode = '';
124
125 params.q = this._addHeadingToQuery(params.q, xHeading, this.xField);
126 params.q = this._addHeadingToQuery(params.q, yHeading, this.yField);
127
128 url += qs.stringify(params);
129
130 return url;
131 }
132
133 /**
134 * @param {string} query
135 * @param {string} heading The value of field for the current group.
136 * @param {string} field Field on which we're grouping the issue.
137 * @return {string} The query with an additional clause if needed.
138 */
139 _addHeadingToQuery(query, heading, field) {
140 if (field && field !== 'None') {
141 if (heading === EMPTY_FIELD_VALUE) {
142 query += ' -has:' + field;
143 // The following two cases are to handle grouping issues by Blocked
144 } else if (heading === 'No') {
145 query += ' -is:' + field;
146 } else if (heading === 'Yes') {
147 query += ' is:' + field;
148 } else {
149 query += ' ' + field + '=' + heading;
150 }
151 }
152 return query;
153 }
154
155 /** @override */
156 static get properties() {
157 return {
158 xField: {type: String},
159 yField: {type: String},
160 issues: {type: Array},
161 cellMode: {type: String},
162 queryParams: {type: Object},
163 projectName: {type: String},
164 _extractFieldValuesFromIssue: {type: Object},
165 _extractTypeForFieldName: {type: Object},
166 _statusDefs: {type: Array},
167 _labelPrefixValueMap: {type: Map},
168 };
169 }
170
171 /** @override */
172 static get styles() {
173 return [
174 SHARED_STYLES,
175 css`
176 table {
177 table-layout: auto;
178 border-collapse: collapse;
179 width: 98%;
180 margin: 0.5em 1%;
181 text-align: left;
182 }
183 th {
184 border: 1px solid white;
185 padding: 5px;
186 background-color: var(--chops-table-header-bg);
187 white-space: nowrap;
188 }
189 td {
190 border: var(--chops-table-divider);
191 padding-left: 0.3em;
192 background-color: var(--chops-white);
193 vertical-align: top;
194 }
195 mr-issue-link {
196 display: inline-block;
197 margin-right: 8px;
198 }
199 `,
200 ];
201 }
202
203 /** @override */
204 constructor() {
205 super();
206 /** @type {string} */
207 this.cellMode = 'tiles';
208 /** @type {Array<Issue>} */
209 this.issues = [];
210 /** @type {string} */
211 this.projectName;
212 this.queryParams = {};
213
214 /** @type {string} The issue field on which to group columns. */
215 this.xField;
216
217 /** @type {string} The issue field on which to group rows. */
218 this.yField;
219
220 /**
221 * Grid cell key mapped to issues associated with that cell.
222 * @type {Map.<string, Array<Issue>>}
223 */
224 this._groupedIssues = new Map();
225
226 /** @type {Array<string>} */
227 this._xHeadings = [];
228
229 /** @type {Array<string>} */
230 this._yHeadings = [];
231
232 /**
233 * Method for extracting values from an issue for a given
234 * project config.
235 * @type {function(Issue, string): Array<string>}
236 */
237 this._extractFieldValuesFromIssue = undefined;
238
239 /**
240 * Method for finding the types of fields based on their names.
241 * @type {function(string): string}
242 */
243 this._extractTypeForFieldName = undefined;
244
245 /**
246 * Note: no default assigned here: it can be undefined in stateChanged.
247 * @type {Array<StatusDef>}
248 */
249 this._statusDefs;
250
251 /**
252 * Mapping predefined label prefix to set of values
253 * @type {Map}
254 */
255 this._labelPrefixValueMap = new Map();
256 }
257
258 /** @override */
259 stateChanged(state) {
260 this._extractFieldValuesFromIssue =
261 projectV0.extractFieldValuesFromIssue(state);
262 this._extractTypeForFieldName = projectV0.extractTypeForFieldName(state);
263 this._statusDefs = projectV0.viewedConfig(state).statusDefs;
264 this._labelPrefixValueMap = projectV0.labelPrefixValueMap(state);
265 }
266
267 /** @override */
268 update(changedProperties) {
269 if (setHasAny(changedProperties, PROPERTIES_TRIGGERING_GROUPING)) {
270 if (this._extractFieldValuesFromIssue) {
271 const gridData = extractGridData({
272 issues: this.issues,
273 extractFieldValuesFromIssue: this._extractFieldValuesFromIssue,
274 }, {
275 xFieldName: this.xField,
276 yFieldName: this.yField,
277 extractTypeForFieldName: this._extractTypeForFieldName,
278 statusDefs: this._statusDefs,
279 labelPrefixValueMap: this._labelPrefixValueMap,
280 });
281
282 this._xHeadings = gridData.xHeadings;
283 this._yHeadings = gridData.yHeadings;
284 this._groupedIssues = gridData.groupedIssues;
285 }
286 }
287
288 super.update(changedProperties);
289 }
290};
291customElements.define('mr-grid', MrGrid);