blob: f0f859b267bd1a5d9f28de4706968edbd5b2e7f0 [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 {EMPTY_FIELD_VALUE, fieldTypes} from 'shared/issue-fields.js';
6import 'shared/typedef.js';
7
8
9const DEFAULT_HEADER_VALUE = 'All';
10
11// Sort headings functions
12// TODO(zhangtiff): Find some way to restructure this code to allow
13// sorting functions to sort with raw types instead of stringified values.
14
15/**
16 * Used as an optional 'compareFunction' for Array.sort().
17 * @param {string} strA
18 * @param {string} strB
19 * @return {number}
20 */
21function intStrComparator(strA, strB) {
22 return parseInt(strA) - parseInt(strB);
23}
24
25/**
26 * Used as an optional 'compareFunction' for Array.sort()
27 * @param {string} issueRefStrA
28 * @param {string} issueRefStrB
29 * @return {number}
30 */
31function issueRefComparator(issueRefStrA, issueRefStrB) {
32 const issueRefA = issueRefStrA.split(':');
33 const issueRefB = issueRefStrB.split(':');
34 if (issueRefA[0] != issueRefB[0]) {
35 return issueRefStrA.localeCompare(issueRefStrB);
36 } else {
37 return parseInt(issueRefA[1]) - parseInt(issueRefB[1]);
38 }
39}
40
41/**
42 * Returns a comparator for strings representing statuses using the ordering
43 * provided in statusDefs.
44 * Any status not found in statusDefs will be sorted to the end.
45 * @param {!Array<StatusDef>=} statusDefs
46 * @return {function(string, string): number}
47 */
48function getStatusDefComparator(statusDefs = []) {
49 return (statusStrA, statusStrB) => {
50 // Traverse statusDefs to determine which status is first.
51 for (const statusDef of statusDefs) {
52 if (statusDef.status == statusStrA) {
53 return -1;
54 } else if (statusDef.status == statusStrB) {
55 return 1;
56 }
57 }
58 return 0;
59 };
60}
61
62/**
63 * @param {!Set<string>} headingSet The headers found for the field.
64 * @param {string} fieldName The field on which we're sorting.
65 * @param {function(string): string=} extractTypeForFieldName
66 * @param {!Array<StatusDef>=} statusDefs
67 * @return {!Array<string>}
68 */
69function sortHeadings(headingSet, fieldName, extractTypeForFieldName,
70 statusDefs = []) {
71 let sorter;
72 if (extractTypeForFieldName) {
73 const type = extractTypeForFieldName(fieldName);
74 if (type === fieldTypes.ISSUE_TYPE) {
75 sorter = issueRefComparator;
76 } else if (type === fieldTypes.INT_TYPE) {
77 sorter = intStrComparator;
78 } else if (type === fieldTypes.STATUS_TYPE) {
79 sorter = getStatusDefComparator(statusDefs);
80 }
81 }
82
83 // Track whether EMPTY_FIELD_VALUE is present, and ensure that
84 // it is sorted to the first position of custom fields.
85 // TODO(jessan): although convenient, it is bad practice to mutate parameters.
86 const hasEmptyFieldValue = headingSet.delete(EMPTY_FIELD_VALUE);
87 const headingsList = [...headingSet];
88
89 headingsList.sort(sorter);
90
91 if (hasEmptyFieldValue) {
92 headingsList.unshift(EMPTY_FIELD_VALUE);
93 }
94 return headingsList;
95}
96
97/**
98 * @param {string} x Header value.
99 * @param {string} y Header value.
100 * @return {string} The key for the groupedIssue map.
101 * TODO(jessan): Make a GridData class, which avoids exposing this logic.
102 */
103export function makeGridCellKey(x, y) {
104 // Note: Some possible x and y values contain ':', '-', and other
105 // non-word characters making delimiter options limited.
106 return x + ' + ' + y;
107}
108
109/**
110 * @param {Issue} issue The issue for which we're preparing grid headings.
111 * @param {string} fieldName The field on which we're grouping.
112 * @param {function(Issue, string): Array<string>} extractFieldValuesFromIssue
113 * @return {!Array<string>} The headings the issue should be grouped into.
114 */
115function prepareHeadings(
116 issue, fieldName, extractFieldValuesFromIssue) {
117 const values = extractFieldValuesFromIssue(issue, fieldName);
118
119 return values.length == 0 ?
120 [EMPTY_FIELD_VALUE] :
121 values;
122}
123
124/**
125 * Groups issues by their values for the given fields.
126 * @param {Array<Issue>} required.issues The issues we are grouping
127 * @param {function(Issue, string): Array<string>}
128 * required.extractFieldValuesFromIssue
129 * @param {string=} options.xFieldName name of the field for grouping columns
130 * @param {string=} options.yFieldName name of the field for grouping rows
131 * @param {function(string): string=} options.extractTypeForFieldName
132 * @param {Array=} options.statusDefs
133 * @param {Map=} options.labelPrefixValueMap
134 * @return {!Object} Grid data
135 * - groupedIssues: A map of issues grouped by thir xField and yField values.
136 * - xHeadings: sorted headings for columns.
137 * - yHeadings: sorted headings for rows.
138 */
139export function extractGridData({issues, extractFieldValuesFromIssue}, {
140 xFieldName = '',
141 yFieldName = '',
142 extractTypeForFieldName = undefined,
143 statusDefs = [],
144 labelPrefixValueMap = new Map(),
145} = {}) {
146 const xHeadingsPredefinedSet = new Set();
147 const xHeadingsAdHocSet = new Set();
148 const yHeadingsSet = new Set();
149 const groupedIssues = new Map();
150 for (const issue of issues) {
151 const xHeadings = !xFieldName ?
152 [DEFAULT_HEADER_VALUE] :
153 prepareHeadings(
154 issue, xFieldName, extractFieldValuesFromIssue);
155 const yHeadings = !yFieldName ?
156 [DEFAULT_HEADER_VALUE] :
157 prepareHeadings(
158 issue, yFieldName, extractFieldValuesFromIssue);
159
160 // Find every combo of 'xValue yValue' that the issue belongs to
161 // and add it into that cell. Also record each header used.
162 for (const xHeading of xHeadings) {
163 if (labelPrefixValueMap.has(xFieldName) &&
164 labelPrefixValueMap.get(xFieldName).has(xHeading)) {
165 xHeadingsPredefinedSet.add(xHeading);
166 } else {
167 xHeadingsAdHocSet.add(xHeading);
168 }
169 for (const yHeading of yHeadings) {
170 yHeadingsSet.add(yHeading);
171 const cellKey = makeGridCellKey(xHeading, yHeading);
172 if (groupedIssues.has(cellKey)) {
173 groupedIssues.get(cellKey).push(issue);
174 } else {
175 groupedIssues.set(cellKey, [issue]);
176 }
177 }
178 }
179 }
180
181 // Predefined labels to be ordered in front of ad hoc labels
182 const xHeadings = [
183 ...sortHeadings(
184 xHeadingsPredefinedSet,
185 xFieldName,
186 extractTypeForFieldName,
187 statusDefs,
188 ),
189 ...sortHeadings(
190 xHeadingsAdHocSet,
191 xFieldName,
192 extractTypeForFieldName,
193 statusDefs,
194 ),
195 ];
196
197 return {
198 groupedIssues,
199 xHeadings,
200 yHeadings: sortHeadings(yHeadingsSet, yFieldName, extractTypeForFieldName,
201 statusDefs),
202 };
203}