case 4302

This commit is contained in:
2022-11-18 23:37:09 +00:00
parent 011677a0e3
commit dac55c92ab
2 changed files with 351 additions and 290 deletions

View File

@@ -151,12 +151,12 @@ Use of the `ayPrepareData` function is optional and most reports will not requir
This is the default ayPrepareData method:
```javascript
async function ayPrepareData(ayData) {
return ayData;
async function ayPrepareData(reportData) {
return reportData;
}
```
`ayData` parameter is an object containing three keys with data required to render the report and it's format is detailed below in the [Report Data Objects](#report-data-structure) section.
`reportData` parameter is an object containing three keys with data required to render the report and it's format is detailed below in the [Report Data Objects](#report-data-structure) section.
Note that the SAMPLE DATA section of the user interface in the report designer shows the raw data _before_ it is passed to this function, not how it would look _after_ this function is run. If you need to see how the data is shaped after a ayPrepareData you can make use of the `ayJSON` helper documented below to view the raw data on the rendered report itself.
@@ -204,7 +204,7 @@ Importing of report templates is done from the administration report templates [
When a report template is processed, business object data is provided to the template along with other data. Some of this data is used internally by the provided helpers and you have full access to these data values in your templates, Handlebar helpers and PrepareData scripts.
In the `ayPrepareData` function on the Prepare Data tab of the report designer the `ayData` parameter contains three properties detailed below you can use in preparing data:
In the `ayPrepareData` function on the Prepare Data tab of the report designer the `reportData` parameter contains three properties detailed below you can use in preparing data:
```json
{
@@ -473,9 +473,9 @@ Note that you need to tell AyaNova which translation keys to pre-fetch before re
For example, let's say you wanted to display the translation for Customer and WorkOrder, you would insert a call to get the translations as follows:
```javascript
async function ayPrepareData(ayData) {
async function ayPrepareData(reportData) {
await ayGetTranslations(["Customer", "WorkOrder"]);
return ayData;
return reportData;
}
```
@@ -507,12 +507,15 @@ The `ayGroupByKey` function is provided to group the report data by a key name i
The following shows using the ayGroupBy function in the ayPrepareData to reformat a Customer Unit List into groups by Customer name which is provided in the CustomerViz property:
```javascript
async function ayPrepareData(ayData) {
async function ayPrepareData(reportData) {
//send the raw report data to the groupByKey function which will return a new array grouped by the key name provided
ayData.ayReportData = ayGroupByKey(ayData.ayReportData, "CustomerViz");
reportData.ayReportData = ayGroupByKey(
reportData.ayReportData,
"CustomerViz"
);
//return the data into the pipeline to send to the report template
return ayData;
return reportData;
}
```
@@ -532,7 +535,7 @@ While it is possible to login via a script using alternate credentials and acces
### API convenience functions
Two functions are provided with the report to assist with API usage from your ayPrepareData custom function:
The following functions are provided to assist with API usage from your ayPrepareData custom function:
_GET_
@@ -549,27 +552,40 @@ The ayPostToAPI function works with POST routes in the API:
The `token` parameter is optional and if not provided will be set by default to the current User's access token.
The `route` parameter will be automatically prepended with the server local api url if you do not provide the starting "http" in the URL. Do not include the initial slash.
_PUT_
The ayPutToAPI function works with PUT routes in the API:
`async function ayPutToAPI(route, data, token) {...`
The `token` parameter is optional and if not provided will be set by default to the current User's access token.
The `route` parameter will be automatically prepended with the server local api url if you do not provide the starting "http" in the URL. Do not include the initial slash.
_Parameters_
The `route` parameter is required and will automatically include the current API server URL prepended if not provided (it looks for "http" at the start). For example you can specifiy the `route` parameter as simply the end portion of the route without the slash: "server-info" or the full route to the server from the server URL meta property. For example the full route to fetch data from the `server-info` API route can be constructed as follows:
`` let route=`${ayData.ayServerMetaData.ayApiUrl}server-info`; ``
The `route` parameter is required and will automatically include the current API server URL prepended if not provided (it looks for "http" at the start).
The `token` parameter is optional and if not provided will be set by default to the current User's access token or you can specify it by using the authorization token provided in the Client data as follows:
`let token=ayData.ayClientMetaData.Authorization;`
For example you can specifiy the `route` parameter as simply the end portion of the route without the slash: "server-info" or the full route to the server from the server URL meta property.
For example the full route to fetch data from the `server-info` API route can be constructed as follows:
`` let route=`${reportData.ayServerMetaData.ayApiUrl}server-info`; ``
The `token` parameter is **optional** and if not provided will be set by default to the current User's access token or you can specify it by using the authorization token provided in the Client data as follows:
`let token=reportData.ayClientMetaData.Authorization;`
The `data` parameter of a POST route differs for each route and is documented in the developer's API documentation.
The following is an example of accessing the API with both a GET and POST action:
```javascript
async function ayPrepareData(ayData) {
async function ayPrepareData(reportData) {
//Example API GET method
//to fetch data from API server
//using the "server-info" route
//Add the data to the main report data object
//so it's available to the template
ayData.myData = {
reportData.myData = {
ServerInfo: await ayGetFromAPI("server-info")
};
@@ -579,8 +595,11 @@ async function ayPrepareData(ayData) {
//construct the POST object
let searchPostData = { phrase: "fish" };
ayData.myData.SearchResults = await ayPostToAPI("search", searchPostData);
return ayData;
reportData.myData.SearchResults = await ayPostToAPI(
"search",
searchPostData
);
return reportData;
}
```

View File

@@ -4,12 +4,12 @@ let PreParedReportDataObject = null;
// Pre render function
//
async function ayPreRender(ayData) {
if (typeof ayPrepareData === "function") {
PreParedReportDataObject = await ayPrepareData(ayData);
} else {
PreParedReportDataObject = ayData;
}
return true;
if (typeof ayPrepareData === "function") {
PreParedReportDataObject = await ayPrepareData(ayData);
} else {
PreParedReportDataObject = ayData;
}
return true;
}
//##################################### HANDLEBARS HELPERS ###################################################
@@ -18,128 +18,128 @@ async function ayPreRender(ayData) {
// Set our stock handlebars helpers
//
function ayRegisterHelpers() {
Handlebars.registerHelper("ayCaps", function (ayValue) {
return ayValue.toUpperCase();
});
Handlebars.registerHelper("ayDateTime", function (ayValue) {
return utcDateToShortDateAndTimeLocalized(ayValue);
});
Handlebars.registerHelper("ayDate", function (ayValue) {
return utcDateToShortDateLocalized(ayValue);
});
Handlebars.registerHelper("ayTime", function (ayValue) {
return utcDateToShortTimeLocalized(ayValue);
});
Handlebars.registerHelper("ayDecimal", function (ayValue) {
return decimalLocalized(ayValue);
});
Handlebars.registerHelper("ayCurrency", function (ayValue) {
return currencyLocalized(ayValue);
});
Handlebars.registerHelper("ayWiki", function (ayValue) {
if (ayValue == null) {
return "";
}
//replace attachment urls with tokenized local urls
let src = ayValue.replace(/\[ATTACH:(.*)\]/g, function (match, p1) {
return attachmentDownloadUrl(p1);
Handlebars.registerHelper("ayCaps", function (ayValue) {
return ayValue.toUpperCase();
});
return new Handlebars.SafeString(
DOMPurify.sanitize(marked.parse(src, { breaks: true }))
);
});
Handlebars.registerHelper("ayJSON", function (obj) {
return JSON.stringify(obj, null, 3);
});
Handlebars.registerHelper("ayDateTime", function (ayValue) {
return utcDateToShortDateAndTimeLocalized(ayValue);
});
Handlebars.registerHelper("ayLink", function (text, url) {
var url = Handlebars.escapeExpression(url),
text = Handlebars.escapeExpression(text);
Handlebars.registerHelper("ayDate", function (ayValue) {
return utcDateToShortDateLocalized(ayValue);
});
return new Handlebars.SafeString("<a href='" + url + "'>" + text + "</a>");
});
Handlebars.registerHelper("ayTime", function (ayValue) {
return utcDateToShortTimeLocalized(ayValue);
});
Handlebars.registerHelper("ayLogo", function (size) {
if (AYMETA.ayServerMetaData) {
switch (size) {
case "small":
if (!AYMETA.ayServerMetaData.HasSmallLogo) {
Handlebars.registerHelper("ayDecimal", function (ayValue) {
return decimalLocalized(ayValue);
});
Handlebars.registerHelper("ayCurrency", function (ayValue) {
return currencyLocalized(ayValue);
});
Handlebars.registerHelper("ayWiki", function (ayValue) {
if (ayValue == null) {
return "";
}
break;
case "medium":
if (!AYMETA.ayServerMetaData.HasMediumLogo) {
return "";
}
break;
case "large":
if (!AYMETA.ayServerMetaData.HasLargeLogo) {
return "";
}
break;
}
}
var url = `${Handlebars.escapeExpression(
AYMETA.ayServerMetaData.ayApiUrl
)}logo/${size}`;
return new Handlebars.SafeString("<img src='" + url + "'/>");
});
}
Handlebars.registerHelper("ayT", function (translationKey) {
if (ayTranslationKeyCache[translationKey] == undefined) {
throw `ayT reporting helper error: the key "${translationKey}" is not present in the translation cache, did you forget to include it in your call to "await ayGetTranslations(['ExampleTranslationKey1','ExampleTranslationKey2','etc']);" in ayPrepareData()\nTranslationKeyCache contains: ${JSON.stringify(
ayTranslationKeyCache,
null,
3
)}?`;
// return translationKey;
}
return ayTranslationKeyCache[translationKey];
});
//replace attachment urls with tokenized local urls
let src = ayValue.replace(/\[ATTACH:(.*)\]/g, function (match, p1) {
return attachmentDownloadUrl(p1);
});
///////////////////////////////////////////
// BarCode helper using
// https://github.com/metafloor/bwip-js#browser-usage
//
Handlebars.registerHelper("ayBC", function (text, options) {
let canvas = document.getElementById("aybarcode");
if (canvas == null) {
canvas = document.createElement("canvas");
canvas.id = "aybarcode";
}
let opt = JSON.parse(options);
if (text == null) {
text = "";
} else {
text = text.toString();
}
opt.text = text;
opt.textxalign = opt.textxalign || "center";
return new Handlebars.SafeString(
DOMPurify.sanitize(marked.parse(src, { breaks: true }))
);
});
bwipjs.toCanvas(canvas, opt);
var url = canvas.toDataURL("image/png");
return new Handlebars.SafeString("<img src='" + url + "'/>");
});
Handlebars.registerHelper("ayJSON", function (obj) {
return JSON.stringify(obj, null, 3);
});
Handlebars.registerHelper("ayLink", function (text, url) {
var url = Handlebars.escapeExpression(url),
text = Handlebars.escapeExpression(text);
return new Handlebars.SafeString(
"<a href='" + url + "'>" + text + "</a>"
);
});
Handlebars.registerHelper("ayLogo", function (size) {
if (AYMETA.ayServerMetaData) {
switch (size) {
case "small":
if (!AYMETA.ayServerMetaData.HasSmallLogo) {
return "";
}
break;
case "medium":
if (!AYMETA.ayServerMetaData.HasMediumLogo) {
return "";
}
break;
case "large":
if (!AYMETA.ayServerMetaData.HasLargeLogo) {
return "";
}
break;
}
}
var url = `${Handlebars.escapeExpression(
AYMETA.ayServerMetaData.ayApiUrl
)}logo/${size}`;
return new Handlebars.SafeString("<img src='" + url + "'/>");
});
Handlebars.registerHelper("ayT", function (translationKey) {
if (ayTranslationKeyCache[translationKey] == undefined) {
throw `ayT reporting helper error: the key "${translationKey}" is not present in the translation cache, did you forget to include it in your call to "await ayGetTranslations(['ExampleTranslationKey1','ExampleTranslationKey2','etc']);" in ayPrepareData()\nTranslationKeyCache contains: ${JSON.stringify(
ayTranslationKeyCache,
null,
3
)}?`;
// return translationKey;
}
return ayTranslationKeyCache[translationKey];
});
///////////////////////////////////////////
// BarCode helper using
// https://github.com/metafloor/bwip-js#browser-usage
//
Handlebars.registerHelper("ayBC", function (text, options) {
let canvas = document.getElementById("aybarcode");
if (canvas == null) {
canvas = document.createElement("canvas");
canvas.id = "aybarcode";
}
let opt = JSON.parse(options);
if (text == null) {
text = "";
} else {
text = text.toString();
}
opt.text = text;
opt.textxalign = opt.textxalign || "center";
bwipjs.toCanvas(canvas, opt);
var url = canvas.toDataURL("image/png");
return new Handlebars.SafeString("<img src='" + url + "'/>");
});
} //eof
///////////////////////////////////////////
// Concat helper using
// https://stackoverflow.com/a/52571635/8939
//
Handlebars.registerHelper('ayConcat', function() {
arguments = [...arguments].slice(0, -1);
return arguments.join('');
Handlebars.registerHelper("ayConcat", function () {
arguments = [...arguments].slice(0, -1);
return arguments.join("");
});
//##################################### LOCALIZATION & TRANSLATION ###################################################
@@ -151,78 +151,81 @@ Handlebars.registerHelper('ayConcat', function() {
//(PRIVATE NOT DOCUMENTED, FOR HELPER USE)
//
function utcDateToShortDateAndTimeLocalized(ayValue) {
if (!ayValue) {
return "";
}
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
let parsedDate = new Date(ayValue);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleString(
AYMETA.ayClientMetaData.LanguageName || "en-US",
{
timeZone: AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg",
dateStyle: "short",
timeStyle: "short",
hour12: AYMETA.ayClientMetaData.Hour12
if (!ayValue) {
return "";
}
);
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
let parsedDate = new Date(ayValue);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleString(
AYMETA.ayClientMetaData.LanguageName || "en-US",
{
timeZone:
AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg",
dateStyle: "short",
timeStyle: "short",
hour12: AYMETA.ayClientMetaData.Hour12
}
);
}
///////////////////////////////////////////
// Turn a utc date into a displayable
// short date
//(PRIVATE NOT DOCUMENTED, FOR HELPER USE)
function utcDateToShortDateLocalized(ayValue) {
if (!ayValue) {
return "";
}
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
let parsedDate = new Date(ayValue);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleDateString(
AYMETA.ayClientMetaData.LanguageName || "en-US",
{
timeZone: AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg",
dateStyle: "short"
if (!ayValue) {
return "";
}
);
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
let parsedDate = new Date(ayValue);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleDateString(
AYMETA.ayClientMetaData.LanguageName || "en-US",
{
timeZone:
AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg",
dateStyle: "short"
}
);
}
///////////////////////////////////////////
// Turn a utc date into a displayable
// short time
//(PRIVATE NOT DOCUMENTED, FOR HELPER USE)
function utcDateToShortTimeLocalized(ayValue) {
if (!ayValue) {
return "";
}
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
let parsedDate = new Date(ayValue);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleTimeString(
AYMETA.ayClientMetaData.LanguageName || "en-US",
{
timeZone: AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg",
timeStyle: "short",
hour12: AYMETA.ayClientMetaData.Hour12
if (!ayValue) {
return "";
}
);
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
let parsedDate = new Date(ayValue);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleTimeString(
AYMETA.ayClientMetaData.LanguageName || "en-US",
{
timeZone:
AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg",
timeStyle: "short",
hour12: AYMETA.ayClientMetaData.Hour12
}
);
}
///////////////////////////////////////////
@@ -230,16 +233,16 @@ function utcDateToShortTimeLocalized(ayValue) {
//(PRIVATE NOT DOCUMENTED, FOR HELPER USE)
//
function currencyLocalized(ayValue) {
if (!ayValue) {
return "";
}
return new Intl.NumberFormat(
AYMETA.ayClientMetaData.LanguageName || "en-US",
{
style: "currency",
currency: AYMETA.ayClientMetaData.CurrencyName || "USD"
if (!ayValue) {
return "";
}
).format(ayValue);
return new Intl.NumberFormat(
AYMETA.ayClientMetaData.LanguageName || "en-US",
{
style: "currency",
currency: AYMETA.ayClientMetaData.CurrencyName || "USD"
}
).format(ayValue);
}
///////////////////////////////////////////
@@ -247,12 +250,12 @@ function currencyLocalized(ayValue) {
//(PRIVATE NOT DOCUMENTED, FOR HELPER USE)
//
function decimalLocalized(ayValue) {
if (!ayValue) {
return "";
}
return new Intl.NumberFormat(
AYMETA.ayClientMetaData.LanguageName || "en-US"
).format(ayValue);
if (!ayValue) {
return "";
}
return new Intl.NumberFormat(
AYMETA.ayClientMetaData.LanguageName || "en-US"
).format(ayValue);
}
//////////////////////////////////
@@ -264,99 +267,133 @@ var ayTranslationKeyCache = {};
// GET TRANSLATIONS FROM API SERVER
//
async function ayGetTranslations(keys) {
if (!keys || keys.length == 0) {
return;
}
try {
let transData = await ayPostToAPI("translation/subset", keys);
transData.data.forEach(function storeFetchedTranslationItemsInCache(item) {
ayTranslationKeyCache[item.key] = item.value;
});
} catch (error) {
//fundamental error, can't proceed with this call
// handleError("GET", error, route);
//todo: deal with this properly
throw error;
}
if (!keys || keys.length == 0) {
return;
}
try {
let transData = await ayPostToAPI("translation/subset", keys);
transData.data.forEach(function storeFetchedTranslationItemsInCache(
item
) {
ayTranslationKeyCache[item.key] = item.value;
});
} catch (error) {
//fundamental error, can't proceed with this call
// handleError("GET", error, route);
//todo: deal with this properly
throw error;
}
}
//##################################### API UTILITIES ###################################################
///////////////////////////////////
// GET DATA FROM API SERVER
//
async function ayGetFromAPI(route, token) {
token = token || AYMETA.ayClientMetaData.Authorization;
if (route && !route.startsWith("http")) {
route = AYMETA.ayServerMetaData.ayApiUrl + route;
}
try {
let r = await fetch(route, {
method: "get",
mode: "cors",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: token
}
});
return await r.json();
} catch (error) {
//fundamental error, can't proceed with this call
// handleError("GET", error, route);
//todo: deal with this properly
throw error;
}
token = token || AYMETA.ayClientMetaData.Authorization;
if (route && !route.startsWith("http")) {
route = AYMETA.ayServerMetaData.ayApiUrl + route;
}
try {
let r = await fetch(route, {
method: "get",
mode: "cors",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: token
}
});
return await r.json();
} catch (error) {
//fundamental error, can't proceed with this call
// handleError("GET", error, route);
//todo: deal with this properly
throw error;
}
}
///////////////////////////////////
// POST DATA TO API SERVER
//
async function ayPostToAPI(route, data, token) {
token = token || AYMETA.ayClientMetaData.Authorization;
if (route && !route.startsWith("http")) {
route = AYMETA.ayServerMetaData.ayApiUrl + route;
}
try {
fetchOptions = {
method: "post",
mode: "cors",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: token
},
body: JSON.stringify(data)
};
let r = await fetch(route, fetchOptions);
return await r.json();
} catch (error) {
//todo: better handle this
throw error;
}
token = token || AYMETA.ayClientMetaData.Authorization;
if (route && !route.startsWith("http")) {
route = AYMETA.ayServerMetaData.ayApiUrl + route;
}
//api expects custom fields to be a string not an object
if (data && data.CustomFields && data.CustomFields.constructor === Object) {
data.CustomFields = JSON.stringify(data.CustomFields);
}
try {
fetchOptions = {
method: "post",
mode: "cors",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: token
},
body: JSON.stringify(data)
};
let r = await fetch(route, fetchOptions);
return await r.json();
} catch (error) {
throw error;
}
}
///////////////////////////////////
// PUT DATA TO API SERVER
//
async function ayPutToAPI(route, data, token) {
token = token || AYMETA.ayClientMetaData.Authorization;
if (route && !route.startsWith("http")) {
route = AYMETA.ayServerMetaData.ayApiUrl + route;
}
//api expects custom fields to be a string not an object
if (data && data.CustomFields && data.CustomFields.constructor === Object) {
data.CustomFields = JSON.stringify(data.CustomFields);
}
try {
fetchOptions = {
method: "put",
mode: "cors",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: token
},
body: JSON.stringify(data)
};
let r = await fetch(route, fetchOptions);
return await r.json();
} catch (error) {
throw error;
}
}
/////////////////////////////
// attachment download URL
// (PRIVATE NOT DOCUMENTED FOR HELPER USE)
function attachmentDownloadUrl(fileId, ctype) {
//http://localhost:7575/api/v8/attachment/download/100?t=sssss
//Ctype is optional and is the MIME content type, used to detect image urls at client for drag and drop ops
//in wiki but ignored by server
//http://localhost:7575/api/v8/attachment/download/100?t=sssss
//Ctype is optional and is the MIME content type, used to detect image urls at client for drag and drop ops
//in wiki but ignored by server
let url =
"attachment/download/" +
fileId +
"?t=" +
AYMETA.ayClientMetaData.DownloadToken;
let url =
"attachment/download/" +
fileId +
"?t=" +
AYMETA.ayClientMetaData.DownloadToken;
if (ctype && ctype.includes("image")) {
url += "&i=1";
}
if (ctype && ctype.includes("image")) {
url += "&i=1";
}
return AYMETA.ayServerMetaData.ayApiUrl + url;
return AYMETA.ayServerMetaData.ayApiUrl + url;
}
//##################################### CODE UTILITIES ###################################################
@@ -370,23 +407,29 @@ function attachmentDownloadUrl(fileId, ctype) {
//
//
function ayGroupByKey(reportDataArray, groupByKeyName) {
//array to hold grouped data
const ret = [];
//iterate through the raw reprot data
for (let i = 0; i < reportDataArray.length; i++) {
//search the ret array for a group with this name and if found return a reference to that group object
let groupObject = ret.find(z => z.group == reportDataArray[i][groupByKeyName]);
if (groupObject != undefined) {
//there is already a matching group in the return array so just push this raw report data record into it
groupObject.items.push(reportDataArray[i]);
//update the count for this group's items
groupObject.count++;
} else {
//No group yet, so start a new one in the ret array and push this raw report data record
ret.push({ group: reportDataArray[i][groupByKeyName], items: [reportDataArray[i]], count: 1 });
}
}
return ret;
//array to hold grouped data
const ret = [];
//iterate through the raw reprot data
for (let i = 0; i < reportDataArray.length; i++) {
//search the ret array for a group with this name and if found return a reference to that group object
let groupObject = ret.find(
(z) => z.group == reportDataArray[i][groupByKeyName]
);
if (groupObject != undefined) {
//there is already a matching group in the return array so just push this raw report data record into it
groupObject.items.push(reportDataArray[i]);
//update the count for this group's items
groupObject.count++;
} else {
//No group yet, so start a new one in the ret array and push this raw report data record
ret.push({
group: reportDataArray[i][groupByKeyName],
items: [reportDataArray[i]],
count: 1
});
}
}
return ret;
}
// //Utils
@@ -395,4 +438,3 @@ function ayGroupByKey(reportDataArray, groupByKeyName) {
// n = n + "";
// return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
// }