2022-07-14 07:02:35 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-unused-vars*/
|
|
|
|
|
export default {
|
|
|
|
|
getSelectedRow: (props, moment, _) => {
|
|
|
|
|
let index = -1;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* If multiRowSelection is turned on, use the last index to
|
|
|
|
|
* populate the selectedRowIndex
|
|
|
|
|
*/
|
|
|
|
|
if (props.multiRowSelection) {
|
|
|
|
|
if (
|
|
|
|
|
_.isArray(props.selectedRowIndices) &&
|
|
|
|
|
props.selectedRowIndices.length &&
|
|
|
|
|
props.selectedRowIndices.every((i) => _.isNumber(i))
|
|
|
|
|
) {
|
|
|
|
|
index = props.selectedRowIndices[props.selectedRowIndices.length - 1];
|
|
|
|
|
} else if (_.isNumber(props.selectedRowIndices)) {
|
|
|
|
|
index = props.selectedRowIndices;
|
|
|
|
|
}
|
|
|
|
|
} else if (
|
|
|
|
|
!_.isNil(props.selectedRowIndex) &&
|
|
|
|
|
!_.isNaN(parseInt(props.selectedRowIndex))
|
|
|
|
|
) {
|
|
|
|
|
index = parseInt(props.selectedRowIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rows = props.filteredTableData || props.processedTableData || [];
|
2022-09-21 10:58:11 +00:00
|
|
|
|
|
|
|
|
const primaryColumns = props.primaryColumns;
|
|
|
|
|
const nonDataColumnTypes = [
|
|
|
|
|
"editActions",
|
|
|
|
|
"button",
|
|
|
|
|
"iconButton",
|
|
|
|
|
"menuButton",
|
|
|
|
|
];
|
|
|
|
|
const nonDataColumnAliases = primaryColumns
|
|
|
|
|
? Object.values(primaryColumns)
|
|
|
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
|
|
|
.map((column) => column.alias)
|
|
|
|
|
: [];
|
|
|
|
|
|
2022-07-14 07:02:35 +00:00
|
|
|
let selectedRow;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Note(Balaji): Need to include customColumn values in the selectedRow (select, rating)
|
|
|
|
|
* It should have updated values.
|
|
|
|
|
*/
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
selectedRow = { ...rows[index] };
|
|
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* If index is not a valid, selectedRow should have
|
|
|
|
|
* proper row structure with empty string values
|
|
|
|
|
*/
|
|
|
|
|
selectedRow = {};
|
|
|
|
|
Object.keys(rows[0]).forEach((key) => {
|
|
|
|
|
selectedRow[key] = "";
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-21 10:58:11 +00:00
|
|
|
const keysToBeOmitted = [
|
|
|
|
|
"__originalIndex__",
|
|
|
|
|
"__primaryKey__",
|
|
|
|
|
...nonDataColumnAliases,
|
|
|
|
|
];
|
2022-07-14 07:02:35 +00:00
|
|
|
return _.omit(selectedRow, keysToBeOmitted);
|
|
|
|
|
},
|
|
|
|
|
//
|
|
|
|
|
getTriggeredRow: (props, moment, _) => {
|
|
|
|
|
let index = -1;
|
|
|
|
|
const parsedTriggeredRowIndex = parseInt(props.triggeredRowIndex);
|
|
|
|
|
|
|
|
|
|
if (!_.isNaN(parsedTriggeredRowIndex)) {
|
|
|
|
|
index = parsedTriggeredRowIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rows = props.filteredTableData || props.processedTableData || [];
|
2022-09-21 10:58:11 +00:00
|
|
|
const primaryColumns = props.primaryColumns;
|
|
|
|
|
const nonDataColumnTypes = [
|
|
|
|
|
"editActions",
|
|
|
|
|
"button",
|
|
|
|
|
"iconButton",
|
|
|
|
|
"menuButton",
|
|
|
|
|
];
|
|
|
|
|
const nonDataColumnAliases = primaryColumns
|
|
|
|
|
? Object.values(primaryColumns)
|
|
|
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
|
|
|
.map((column) => column.alias)
|
|
|
|
|
: [];
|
2022-07-14 07:02:35 +00:00
|
|
|
let triggeredRow;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Note(Balaji): Need to include customColumn values in the triggeredRow (select, rating)
|
|
|
|
|
* It should have updated values.
|
|
|
|
|
*/
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
const row = rows.find((row) => row.__originalIndex__ === index);
|
|
|
|
|
triggeredRow = { ...row };
|
|
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* If triggeredRowIndex is not a valid index, triggeredRow should
|
|
|
|
|
* have proper row structure with empty string values
|
|
|
|
|
*/
|
|
|
|
|
triggeredRow = {};
|
|
|
|
|
Object.keys(rows[0]).forEach((key) => {
|
|
|
|
|
triggeredRow[key] = "";
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-21 10:58:11 +00:00
|
|
|
const keysToBeOmitted = [
|
|
|
|
|
"__originalIndex__",
|
|
|
|
|
"__primaryKey__",
|
|
|
|
|
...nonDataColumnAliases,
|
|
|
|
|
];
|
2022-07-14 07:02:35 +00:00
|
|
|
return _.omit(triggeredRow, keysToBeOmitted);
|
|
|
|
|
},
|
|
|
|
|
//
|
|
|
|
|
getSelectedRows: (props, moment, _) => {
|
|
|
|
|
if (!props.multiRowSelection) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let indices = [];
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
_.isArray(props.selectedRowIndices) &&
|
|
|
|
|
props.selectedRowIndices.every((i) => _.isNumber(i))
|
|
|
|
|
) {
|
|
|
|
|
indices = props.selectedRowIndices;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rows = props.filteredTableData || props.processedTableData || [];
|
2022-09-21 10:58:11 +00:00
|
|
|
const primaryColumns = props.primaryColumns;
|
|
|
|
|
const nonDataColumnTypes = [
|
|
|
|
|
"editActions",
|
|
|
|
|
"button",
|
|
|
|
|
"iconButton",
|
|
|
|
|
"menuButton",
|
|
|
|
|
];
|
|
|
|
|
const nonDataColumnAliases = primaryColumns
|
|
|
|
|
? Object.values(primaryColumns)
|
|
|
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
|
|
|
.map((column) => column.alias)
|
|
|
|
|
: [];
|
|
|
|
|
const keysToBeOmitted = [
|
|
|
|
|
"__originalIndex__",
|
|
|
|
|
"__primaryKey__",
|
|
|
|
|
...nonDataColumnAliases,
|
|
|
|
|
];
|
2022-07-14 07:02:35 +00:00
|
|
|
return indices.map((index) => _.omit(rows[index], keysToBeOmitted));
|
|
|
|
|
},
|
|
|
|
|
//
|
|
|
|
|
getPageSize: (props, moment, _) => {
|
|
|
|
|
const TABLE_SIZES = {
|
|
|
|
|
DEFAULT: {
|
|
|
|
|
COLUMN_HEADER_HEIGHT: 32,
|
|
|
|
|
TABLE_HEADER_HEIGHT: 38,
|
|
|
|
|
ROW_HEIGHT: 40,
|
|
|
|
|
ROW_FONT_SIZE: 14,
|
|
|
|
|
VERTICAL_PADDING: 6,
|
|
|
|
|
EDIT_ICON_TOP: 10,
|
|
|
|
|
},
|
|
|
|
|
SHORT: {
|
|
|
|
|
COLUMN_HEADER_HEIGHT: 32,
|
|
|
|
|
TABLE_HEADER_HEIGHT: 38,
|
2022-08-23 11:49:59 +00:00
|
|
|
ROW_HEIGHT: 30,
|
2022-07-14 07:02:35 +00:00
|
|
|
ROW_FONT_SIZE: 12,
|
|
|
|
|
VERTICAL_PADDING: 0,
|
|
|
|
|
EDIT_ICON_TOP: 5,
|
|
|
|
|
},
|
|
|
|
|
TALL: {
|
|
|
|
|
COLUMN_HEADER_HEIGHT: 32,
|
|
|
|
|
TABLE_HEADER_HEIGHT: 38,
|
|
|
|
|
ROW_HEIGHT: 60,
|
|
|
|
|
ROW_FONT_SIZE: 18,
|
|
|
|
|
VERTICAL_PADDING: 16,
|
|
|
|
|
EDIT_ICON_TOP: 21,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const compactMode = props.compactMode || "DEFAULT";
|
2023-09-19 05:22:11 +00:00
|
|
|
const componentHeight = props.componentHeight - 10;
|
2022-07-14 07:02:35 +00:00
|
|
|
const tableSizes = TABLE_SIZES[compactMode];
|
2022-08-23 11:49:59 +00:00
|
|
|
|
|
|
|
|
let pageSize =
|
2022-07-14 07:02:35 +00:00
|
|
|
(componentHeight -
|
|
|
|
|
tableSizes.TABLE_HEADER_HEIGHT -
|
|
|
|
|
tableSizes.COLUMN_HEADER_HEIGHT) /
|
2022-08-23 11:49:59 +00:00
|
|
|
tableSizes.ROW_HEIGHT;
|
|
|
|
|
|
|
|
|
|
return pageSize % 1 > 0.3 ? Math.ceil(pageSize) : Math.floor(pageSize);
|
2022-07-14 07:02:35 +00:00
|
|
|
},
|
|
|
|
|
//
|
|
|
|
|
getProcessedTableData: (props, moment, _) => {
|
|
|
|
|
let data;
|
|
|
|
|
|
|
|
|
|
if (_.isArray(props.tableData)) {
|
|
|
|
|
/* Populate meta keys (__originalIndex__, __primaryKey__) and transient values */
|
|
|
|
|
data = props.tableData.map((row, index) => ({
|
|
|
|
|
...row,
|
|
|
|
|
__originalIndex__: index,
|
|
|
|
|
__primaryKey__: props.primaryColumnId
|
|
|
|
|
? row[props.primaryColumnId]
|
|
|
|
|
: undefined,
|
|
|
|
|
...props.transientTableData[index],
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
data = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
},
|
|
|
|
|
//
|
|
|
|
|
getOrderedTableColumns: (props, moment, _) => {
|
|
|
|
|
let columns = [];
|
|
|
|
|
let existingColumns = props.primaryColumns || {};
|
|
|
|
|
/*
|
|
|
|
|
* Assign index based on the columnOrder
|
|
|
|
|
*/
|
|
|
|
|
if (
|
|
|
|
|
_.isArray(props.columnOrder) &&
|
|
|
|
|
props.columnOrder.length > 0 &&
|
|
|
|
|
Object.keys(existingColumns).length > 0
|
|
|
|
|
) {
|
|
|
|
|
const newColumnsInOrder = {};
|
|
|
|
|
let index = 0;
|
|
|
|
|
|
|
|
|
|
_.uniq(props.columnOrder).forEach((columnId) => {
|
|
|
|
|
if (existingColumns[columnId]) {
|
|
|
|
|
newColumnsInOrder[columnId] = Object.assign(
|
|
|
|
|
{},
|
|
|
|
|
existingColumns[columnId],
|
|
|
|
|
{
|
|
|
|
|
index,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
index++;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
existingColumns = newColumnsInOrder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sortByColumn = props.sortOrder && props.sortOrder.column;
|
|
|
|
|
const isAscOrder = props.sortOrder && props.sortOrder.order === "asc";
|
|
|
|
|
/* set sorting flags and convert the existing columns into an array */
|
|
|
|
|
Object.values(existingColumns).forEach((column) => {
|
|
|
|
|
/* guard to not allow columns without id */
|
|
|
|
|
if (column.id) {
|
2023-11-06 05:35:26 +00:00
|
|
|
columns.push({
|
|
|
|
|
...column,
|
|
|
|
|
isAscOrder: column.id === sortByColumn ? isAscOrder : undefined,
|
|
|
|
|
});
|
2022-07-14 07:02:35 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return columns;
|
|
|
|
|
},
|
|
|
|
|
//
|
|
|
|
|
getFilteredTableData: (props, moment, _) => {
|
|
|
|
|
/* Make a shallow copy */
|
|
|
|
|
const primaryColumns = props.primaryColumns || {};
|
|
|
|
|
let processedTableData = [...props.processedTableData];
|
|
|
|
|
const derivedColumns = {};
|
|
|
|
|
|
|
|
|
|
Object.keys(primaryColumns).forEach((id) => {
|
|
|
|
|
if (primaryColumns[id] && primaryColumns[id].isDerived) {
|
|
|
|
|
derivedColumns[id] = primaryColumns[id];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!processedTableData || !processedTableData.length) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* extend processedTableData with values from
|
|
|
|
|
* - computedValues, in case of normal column
|
|
|
|
|
* - empty values, in case of derived column
|
|
|
|
|
*/
|
|
|
|
|
if (primaryColumns && _.isPlainObject(primaryColumns)) {
|
|
|
|
|
Object.entries(primaryColumns).forEach(([id, column]) => {
|
|
|
|
|
let computedValues = [];
|
|
|
|
|
|
|
|
|
|
if (column && column.computedValue) {
|
|
|
|
|
if (_.isString(column.computedValue)) {
|
|
|
|
|
try {
|
|
|
|
|
computedValues = JSON.parse(column.computedValue);
|
|
|
|
|
} catch (e) {
|
2022-10-04 11:38:02 +00:00
|
|
|
/* do nothing */
|
2022-07-14 07:02:35 +00:00
|
|
|
}
|
|
|
|
|
} else if (_.isArray(column.computedValue)) {
|
|
|
|
|
computedValues = column.computedValue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* for derived columns inject empty strings */
|
|
|
|
|
if (
|
|
|
|
|
computedValues.length === 0 &&
|
|
|
|
|
derivedColumns &&
|
|
|
|
|
derivedColumns[id]
|
|
|
|
|
) {
|
|
|
|
|
computedValues = Array(processedTableData.length).fill("");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
computedValues.forEach((computedValue, index) => {
|
|
|
|
|
processedTableData[index] = {
|
|
|
|
|
...processedTableData[index],
|
|
|
|
|
[column.alias]: computedValue,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const columns = props.orderedTableColumns;
|
|
|
|
|
const sortByColumnId = props.sortOrder.column;
|
|
|
|
|
|
|
|
|
|
let sortedTableData;
|
|
|
|
|
|
|
|
|
|
if (sortByColumnId) {
|
|
|
|
|
const sortBycolumn = columns.find(
|
|
|
|
|
(column) => column.id === sortByColumnId,
|
|
|
|
|
);
|
2023-04-20 06:41:38 +00:00
|
|
|
const sortByColumnOriginalId = sortBycolumn.alias;
|
2022-07-14 07:02:35 +00:00
|
|
|
|
|
|
|
|
const columnType =
|
|
|
|
|
sortBycolumn && sortBycolumn.columnType
|
|
|
|
|
? sortBycolumn.columnType
|
|
|
|
|
: "text";
|
|
|
|
|
const inputFormat = sortBycolumn.inputFormat;
|
|
|
|
|
const isEmptyOrNil = (value) => {
|
|
|
|
|
return _.isNil(value) || value === "";
|
|
|
|
|
};
|
|
|
|
|
const isAscOrder = props.sortOrder.order === "asc";
|
|
|
|
|
const sortByOrder = (isAGreaterThanB) => {
|
|
|
|
|
if (isAGreaterThanB) {
|
|
|
|
|
return isAscOrder ? 1 : -1;
|
|
|
|
|
} else {
|
|
|
|
|
return isAscOrder ? -1 : 1;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sortedTableData = processedTableData.sort((a, b) => {
|
|
|
|
|
if (_.isPlainObject(a) && _.isPlainObject(b)) {
|
|
|
|
|
if (
|
|
|
|
|
isEmptyOrNil(a[sortByColumnOriginalId]) ||
|
|
|
|
|
isEmptyOrNil(b[sortByColumnOriginalId])
|
|
|
|
|
) {
|
|
|
|
|
/* push null, undefined and "" values to the bottom. */
|
|
|
|
|
return isEmptyOrNil(a[sortByColumnOriginalId]) ? 1 : -1;
|
|
|
|
|
} else {
|
|
|
|
|
switch (columnType) {
|
|
|
|
|
case "number":
|
2023-11-06 05:35:26 +00:00
|
|
|
case "currency":
|
2022-07-14 07:02:35 +00:00
|
|
|
return sortByOrder(
|
|
|
|
|
Number(a[sortByColumnOriginalId]) >
|
|
|
|
|
Number(b[sortByColumnOriginalId]),
|
|
|
|
|
);
|
|
|
|
|
case "date":
|
|
|
|
|
try {
|
|
|
|
|
return sortByOrder(
|
|
|
|
|
moment(a[sortByColumnOriginalId], inputFormat).isAfter(
|
|
|
|
|
moment(b[sortByColumnOriginalId], inputFormat),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
2023-06-21 12:01:06 +00:00
|
|
|
case "url":
|
|
|
|
|
const column = primaryColumns[sortByColumnOriginalId];
|
|
|
|
|
if (column && column.displayText) {
|
|
|
|
|
if (_.isString(column.displayText)) {
|
|
|
|
|
return sortByOrder(false);
|
|
|
|
|
} else if (_.isArray(column.displayText)) {
|
|
|
|
|
return sortByOrder(
|
|
|
|
|
column.displayText[a.__originalIndex__]
|
|
|
|
|
.toString()
|
|
|
|
|
.toLowerCase() >
|
|
|
|
|
column.displayText[b.__originalIndex__]
|
|
|
|
|
.toString()
|
|
|
|
|
.toLowerCase(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-07-14 07:02:35 +00:00
|
|
|
default:
|
|
|
|
|
return sortByOrder(
|
|
|
|
|
a[sortByColumnOriginalId].toString().toLowerCase() >
|
|
|
|
|
b[sortByColumnOriginalId].toString().toLowerCase(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return isAscOrder ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
sortedTableData = [...processedTableData];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ConditionFunctions = {
|
|
|
|
|
isExactly: (a, b) => {
|
|
|
|
|
return a.toString() === b.toString();
|
|
|
|
|
},
|
|
|
|
|
empty: (a) => {
|
|
|
|
|
return _.isNil(a) || _.isEmpty(a.toString());
|
|
|
|
|
},
|
|
|
|
|
notEmpty: (a) => {
|
|
|
|
|
return !_.isNil(a) && !_.isEmpty(a.toString());
|
|
|
|
|
},
|
|
|
|
|
notEqualTo: (a, b) => {
|
|
|
|
|
return a.toString() !== b.toString();
|
|
|
|
|
},
|
|
|
|
|
/* Note: Duplicate of isExactly */
|
|
|
|
|
isEqualTo: (a, b) => {
|
|
|
|
|
return a.toString() === b.toString();
|
|
|
|
|
},
|
|
|
|
|
lessThan: (a, b) => {
|
|
|
|
|
return Number(a) < Number(b);
|
|
|
|
|
},
|
|
|
|
|
lessThanEqualTo: (a, b) => {
|
|
|
|
|
return Number(a) <= Number(b);
|
|
|
|
|
},
|
|
|
|
|
greaterThan: (a, b) => {
|
|
|
|
|
return Number(a) > Number(b);
|
|
|
|
|
},
|
|
|
|
|
greaterThanEqualTo: (a, b) => {
|
|
|
|
|
return Number(a) >= Number(b);
|
|
|
|
|
},
|
|
|
|
|
contains: (a, b) => {
|
|
|
|
|
try {
|
|
|
|
|
return a
|
|
|
|
|
.toString()
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.includes(b.toString().toLowerCase());
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
doesNotContain: (a, b) => {
|
|
|
|
|
try {
|
|
|
|
|
return !a
|
|
|
|
|
.toString()
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.includes(b.toString().toLowerCase());
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
startsWith: (a, b) => {
|
|
|
|
|
try {
|
|
|
|
|
return (
|
chore: upgrade to prettier v2 + enforce import types (#21013)Co-authored-by: Satish Gandham <hello@satishgandham.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
## Description
This PR upgrades Prettier to v2 + enforces TypeScript’s [`import
type`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export)
syntax where applicable. It’s submitted as a separate PR so we can merge
it easily.
As a part of this PR, we reformat the codebase heavily:
- add `import type` everywhere where it’s required, and
- re-format the code to account for Prettier 2’s breaking changes:
https://prettier.io/blog/2020/03/21/2.0.0.html#breaking-changes
This PR is submitted against `release` to make sure all new code by team
members will adhere to new formatting standards, and we’ll have fewer
conflicts when merging `bundle-optimizations` into `release`. (I’ll
merge `release` back into `bundle-optimizations` once this PR is
merged.)
### Why is this needed?
This PR is needed because, for the Lodash optimization from
https://github.com/appsmithorg/appsmith/commit/7cbb12af886621256224be0c93e6a465dd710ad3,
we need to use `import type`. Otherwise, `babel-plugin-lodash` complains
that `LoDashStatic` is not a lodash function.
However, just using `import type` in the current codebase will give you
this:
<img width="962" alt="Screenshot 2023-03-08 at 17 45 59"
src="https://user-images.githubusercontent.com/2953267/223775744-407afa0c-e8b9-44a1-90f9-b879348da57f.png">
That’s because Prettier 1 can’t parse `import type` at all. To parse it,
we need to upgrade to Prettier 2.
### Why enforce `import type`?
Apart from just enabling `import type` support, this PR enforces
specifying `import type` everywhere it’s needed. (Developers will get
immediate TypeScript and ESLint errors when they forget to do so.)
I’m doing this because I believe `import type` improves DX and makes
refactorings easier.
Let’s say you had a few imports like below. Can you tell which of these
imports will increase the bundle size? (Tip: it’s not all of them!)
```ts
// app/client/src/workers/Linting/utils.ts
import { Position } from "codemirror";
import { LintError as JSHintError, LintOptions } from "jshint";
import { get, isEmpty, isNumber, keys, last, set } from "lodash";
```
It’s pretty hard, right?
What about now?
```ts
// app/client/src/workers/Linting/utils.ts
import type { Position } from "codemirror";
import type { LintError as JSHintError, LintOptions } from "jshint";
import { get, isEmpty, isNumber, keys, last, set } from "lodash";
```
Now, it’s clear that only `lodash` will be bundled.
This helps developers to see which imports are problematic, but it
_also_ helps with refactorings. Now, if you want to see where
`codemirror` is bundled, you can just grep for `import \{.*\} from
"codemirror"` – and you won’t get any type-only imports.
This also helps (some) bundlers. Upon transpiling, TypeScript erases
type-only imports completely. In some environment (not ours), this makes
the bundle smaller, as the bundler doesn’t need to bundle type-only
imports anymore.
## Type of change
- Chore (housekeeping or task changes that don't impact user perception)
## How Has This Been Tested?
This was tested to not break the build.
### Test Plan
> Add Testsmith test cases links that relate to this PR
### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
## Checklist:
### Dev activity
- [x] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag
### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test
---------
Co-authored-by: Satish Gandham <hello@satishgandham.com>
Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2023-03-16 11:41:47 +00:00
|
|
|
a.toString().toLowerCase().indexOf(b.toString().toLowerCase()) === 0
|
2022-07-14 07:02:35 +00:00
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
endsWith: (a, b) => {
|
|
|
|
|
try {
|
|
|
|
|
const _a = a.toString().toLowerCase();
|
|
|
|
|
const _b = b.toString().toLowerCase();
|
2022-10-14 05:39:52 +00:00
|
|
|
return (
|
|
|
|
|
_a.lastIndexOf(_b) >= 0 &&
|
|
|
|
|
_a.length === _a.lastIndexOf(_b) + _b.length
|
|
|
|
|
);
|
2022-07-14 07:02:35 +00:00
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
is: (a, b) => {
|
|
|
|
|
return moment(a).isSame(moment(b), "minute");
|
|
|
|
|
},
|
|
|
|
|
isNot: (a, b) => {
|
|
|
|
|
return !moment(a).isSame(moment(b), "minute");
|
|
|
|
|
},
|
|
|
|
|
isAfter: (a, b) => {
|
|
|
|
|
return moment(a).isAfter(moment(b), "minute");
|
|
|
|
|
},
|
|
|
|
|
isBefore: (a, b) => {
|
|
|
|
|
return moment(a).isBefore(moment(b), "minute");
|
|
|
|
|
},
|
2022-09-08 11:05:59 +00:00
|
|
|
isChecked: (a) => {
|
|
|
|
|
return a === true;
|
|
|
|
|
},
|
|
|
|
|
isUnChecked: (a) => {
|
|
|
|
|
return a === false;
|
|
|
|
|
},
|
2022-07-14 07:02:35 +00:00
|
|
|
};
|
|
|
|
|
let searchKey;
|
|
|
|
|
|
|
|
|
|
/* skipping search when client side search is turned off */
|
|
|
|
|
if (
|
|
|
|
|
props.searchText &&
|
|
|
|
|
(!props.onSearchTextChanged || props.enableClientSideSearch)
|
|
|
|
|
) {
|
|
|
|
|
searchKey = props.searchText.toLowerCase();
|
|
|
|
|
} else {
|
|
|
|
|
searchKey = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* We need to omit hidden column values from being included
|
|
|
|
|
* in the search
|
|
|
|
|
*/
|
|
|
|
|
const hiddenColumns = Object.values(props.primaryColumns)
|
|
|
|
|
.filter((column) => !column.isVisible)
|
|
|
|
|
.map((column) => column.alias);
|
|
|
|
|
|
|
|
|
|
const finalTableData = sortedTableData.filter((row) => {
|
|
|
|
|
let isSearchKeyFound = true;
|
2023-06-21 12:01:06 +00:00
|
|
|
const columnWithDisplayText = Object.values(props.primaryColumns).filter(
|
|
|
|
|
(column) => column.columnType === "url" && column.displayText,
|
|
|
|
|
);
|
|
|
|
|
const displayedRow = {
|
|
|
|
|
...row,
|
|
|
|
|
...columnWithDisplayText.reduce((acc, column) => {
|
|
|
|
|
let displayText;
|
|
|
|
|
if (_.isArray(column.displayText)) {
|
|
|
|
|
displayText = column.displayText[row.__originalIndex__];
|
|
|
|
|
} else {
|
|
|
|
|
displayText = column.displayText;
|
|
|
|
|
}
|
|
|
|
|
acc[column.alias] = displayText;
|
|
|
|
|
return acc;
|
|
|
|
|
}, {}),
|
|
|
|
|
};
|
2022-07-14 07:02:35 +00:00
|
|
|
if (searchKey) {
|
2023-06-21 12:01:06 +00:00
|
|
|
isSearchKeyFound = Object.values(_.omit(displayedRow, hiddenColumns))
|
2022-07-14 07:02:35 +00:00
|
|
|
.join(", ")
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.includes(searchKey);
|
|
|
|
|
}
|
|
|
|
|
if (!isSearchKeyFound) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-22 11:27:02 +00:00
|
|
|
/* when there is no filter defined or when server side filtering is enabled prevent client-side filtering */
|
|
|
|
|
if (
|
|
|
|
|
!props.filters ||
|
|
|
|
|
props.filters.length === 0 ||
|
|
|
|
|
props.enableServerSideFiltering
|
|
|
|
|
) {
|
2022-07-14 07:02:35 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filterOperator =
|
|
|
|
|
props.filters.length >= 2 ? props.filters[1].operator : "OR";
|
|
|
|
|
let isSatisfyingFilters = filterOperator === "AND";
|
|
|
|
|
for (let i = 0; i < props.filters.length; i++) {
|
|
|
|
|
let filterResult = true;
|
|
|
|
|
try {
|
|
|
|
|
const conditionFunction =
|
|
|
|
|
ConditionFunctions[props.filters[i].condition];
|
|
|
|
|
if (conditionFunction) {
|
|
|
|
|
filterResult = conditionFunction(
|
2023-06-21 12:01:06 +00:00
|
|
|
displayedRow[props.filters[i].column],
|
2022-07-14 07:02:35 +00:00
|
|
|
props.filters[i].value,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
filterResult = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* if one filter condition is not satisfied and filter operator is AND, bailout early */
|
|
|
|
|
if (!filterResult && filterOperator === "AND") {
|
|
|
|
|
isSatisfyingFilters = false;
|
|
|
|
|
break;
|
|
|
|
|
} else if (filterResult && filterOperator === "OR") {
|
|
|
|
|
/* if one filter condition is satisfied and filter operator is OR, bailout early */
|
|
|
|
|
isSatisfyingFilters = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSatisfyingFilters =
|
|
|
|
|
filterOperator === "AND"
|
|
|
|
|
? isSatisfyingFilters && filterResult
|
|
|
|
|
: isSatisfyingFilters || filterResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return isSatisfyingFilters;
|
|
|
|
|
});
|
|
|
|
|
return finalTableData;
|
|
|
|
|
},
|
|
|
|
|
//
|
2022-11-25 04:39:59 +00:00
|
|
|
getUpdatedRow: (props, moment, _) => {
|
|
|
|
|
let index = -1;
|
|
|
|
|
const parsedUpdatedRowIndex = parseInt(props.updatedRowIndex);
|
|
|
|
|
|
|
|
|
|
if (!_.isNaN(parsedUpdatedRowIndex)) {
|
|
|
|
|
index = parsedUpdatedRowIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rows = props.filteredTableData || props.processedTableData || [];
|
|
|
|
|
const primaryColumns = props.primaryColumns;
|
|
|
|
|
let updatedRow;
|
|
|
|
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
const row = rows.find((row) => row.__originalIndex__ === index);
|
|
|
|
|
updatedRow = { ...row };
|
|
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* If updatedRowIndex is not a valid index, updatedRow should
|
|
|
|
|
* have proper row structure with empty string values
|
|
|
|
|
*/
|
|
|
|
|
updatedRow = {};
|
|
|
|
|
if (rows && rows[0]) {
|
|
|
|
|
Object.keys(rows[0]).forEach((key) => {
|
|
|
|
|
updatedRow[key] = "";
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nonDataColumnTypes = [
|
|
|
|
|
"editActions",
|
|
|
|
|
"button",
|
|
|
|
|
"iconButton",
|
|
|
|
|
"menuButton",
|
|
|
|
|
];
|
|
|
|
|
const nonDataColumnAliases = primaryColumns
|
|
|
|
|
? Object.values(primaryColumns)
|
|
|
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
|
|
|
.map((column) => column.alias)
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
const keysToBeOmitted = [
|
|
|
|
|
"__originalIndex__",
|
|
|
|
|
"__primaryKey__",
|
|
|
|
|
...nonDataColumnAliases,
|
|
|
|
|
];
|
|
|
|
|
return _.omit(updatedRow, keysToBeOmitted);
|
|
|
|
|
},
|
|
|
|
|
//
|
2022-07-14 07:02:35 +00:00
|
|
|
getUpdatedRows: (props, moment, _) => {
|
2022-09-21 10:58:11 +00:00
|
|
|
const primaryColumns = props.primaryColumns;
|
|
|
|
|
const nonDataColumnTypes = [
|
|
|
|
|
"editActions",
|
|
|
|
|
"button",
|
|
|
|
|
"iconButton",
|
|
|
|
|
"menuButton",
|
|
|
|
|
];
|
|
|
|
|
const nonDataColumnAliases = primaryColumns
|
|
|
|
|
? Object.values(primaryColumns)
|
|
|
|
|
.filter((column) => nonDataColumnTypes.includes(column.columnType))
|
|
|
|
|
.map((column) => column.alias)
|
|
|
|
|
: [];
|
|
|
|
|
const keysToBeOmitted = [
|
|
|
|
|
"__originalIndex__",
|
|
|
|
|
"__primaryKey__",
|
|
|
|
|
...nonDataColumnAliases,
|
|
|
|
|
];
|
2022-07-14 07:02:35 +00:00
|
|
|
/*
|
|
|
|
|
* case 1. If transientTableData is not empty, return aray of updated row.
|
|
|
|
|
* case 2. If transientTableData is empty, return empty array
|
|
|
|
|
*
|
|
|
|
|
* updated row structure
|
|
|
|
|
* {
|
|
|
|
|
* index: {{original index of the row}},
|
|
|
|
|
* {{primary_column}}: {{primary_column_value}} // only if primary has been set
|
|
|
|
|
* updatedFields: {
|
|
|
|
|
* {{updated_column_1}}: {{updated_column_1_value}}
|
|
|
|
|
* },
|
|
|
|
|
* allFields: {
|
|
|
|
|
* {{updated_column_1}}: {{updated_column_1_value}}
|
|
|
|
|
* {{rest of the fields from the row}}
|
|
|
|
|
* }
|
|
|
|
|
* }
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/* case 1 */
|
|
|
|
|
if (
|
|
|
|
|
props.transientTableData &&
|
|
|
|
|
!!Object.keys(props.transientTableData).length
|
|
|
|
|
) {
|
|
|
|
|
const updatedRows = [];
|
|
|
|
|
const tableData = props.processedTableData || props.tableData;
|
|
|
|
|
|
|
|
|
|
/* updatedRows is not sorted by index */
|
|
|
|
|
Object.entries(props.transientTableData)
|
|
|
|
|
.filter((entry) => {
|
|
|
|
|
return (
|
|
|
|
|
!_.isNil(entry[0]) && !!entry[0] && _.isFinite(Number(entry[0]))
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
.forEach((entry) => {
|
|
|
|
|
const key = entry[0];
|
|
|
|
|
const value = entry[1];
|
|
|
|
|
const row = tableData.find(
|
|
|
|
|
(row) => row.__originalIndex__ === Number(key),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
updatedRows.push({
|
|
|
|
|
index: Number(key),
|
|
|
|
|
[props.primaryColumnId]: row[props.primaryColumnId],
|
|
|
|
|
updatedFields: value,
|
|
|
|
|
allFields: _.omit(row, keysToBeOmitted) || {},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return updatedRows;
|
|
|
|
|
} else {
|
|
|
|
|
/* case 2 */
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
//
|
|
|
|
|
getUpdatedRowIndices: (props, moment, _) => {
|
|
|
|
|
/* should return the keys of the transientTableData */
|
|
|
|
|
if (props.transientTableData) {
|
|
|
|
|
return Object.keys(props.transientTableData).map((index) =>
|
|
|
|
|
Number(index),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
//
|
2022-09-01 10:01:02 +00:00
|
|
|
getPageOffset: (props, moment, _) => {
|
|
|
|
|
const pageSize =
|
|
|
|
|
props.serverSidePaginationEnabled && props.tableData
|
|
|
|
|
? props.tableData?.length
|
|
|
|
|
: props.pageSize;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
Number.isFinite(props.pageNo) &&
|
|
|
|
|
Number.isFinite(pageSize) &&
|
|
|
|
|
props.pageNo >= 0 &&
|
|
|
|
|
pageSize >= 0
|
|
|
|
|
) {
|
|
|
|
|
/* Math.max fixes the value of (pageNo - 1) to a minimum of 0 as negative values are not valid */
|
|
|
|
|
return Math.max(props.pageNo - 1, 0) * pageSize;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
},
|
|
|
|
|
//
|
2022-09-13 05:41:59 +00:00
|
|
|
getEditableCellValidity: (props, moment, _) => {
|
2022-11-05 09:54:20 +00:00
|
|
|
if (
|
2023-04-20 13:12:31 +00:00
|
|
|
(!props.editableCell?.column && !props.isAddRowInProgress) ||
|
2022-11-05 09:54:20 +00:00
|
|
|
!props.primaryColumns
|
|
|
|
|
) {
|
|
|
|
|
return {};
|
2022-09-13 05:41:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const createRegex = (regex) => {
|
|
|
|
|
if (!regex) {
|
|
|
|
|
return new RegExp("//");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* break up the regexp pattern into 4 parts: given regex, regex prefix , regex pattern, regex flags
|
|
|
|
|
* Example /test/i will be split into ["/test/gi", "/", "test", "gi"]
|
|
|
|
|
*/
|
|
|
|
|
const regexParts = regex.match(/(\/?)(.+)\\1([a-z]*)/i);
|
|
|
|
|
let parsedRegex;
|
|
|
|
|
|
|
|
|
|
if (!regexParts) {
|
|
|
|
|
parsedRegex = new RegExp(regex);
|
|
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* if we don't have a regex flags (gmisuy), convert provided string into regexp directly
|
|
|
|
|
*/
|
|
|
|
|
if (
|
|
|
|
|
regexParts[3] &&
|
|
|
|
|
!/^(?!.*?(.).*?\\1)[gmisuy]+$/.test(regexParts[3])
|
|
|
|
|
) {
|
|
|
|
|
parsedRegex = RegExp(regex);
|
|
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* if we have a regex flags, use it to form regexp
|
|
|
|
|
*/
|
|
|
|
|
parsedRegex = new RegExp(regexParts[2], regexParts[3]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parsedRegex;
|
|
|
|
|
};
|
|
|
|
|
|
2022-11-05 09:54:20 +00:00
|
|
|
let editableColumns = [];
|
2023-11-06 05:35:26 +00:00
|
|
|
const validatableColumns = ["text", "number", "currency"];
|
2022-11-05 09:54:20 +00:00
|
|
|
|
|
|
|
|
if (props.isAddRowInProgress) {
|
|
|
|
|
Object.values(props.primaryColumns)
|
|
|
|
|
.filter(
|
|
|
|
|
(column) =>
|
|
|
|
|
column.isEditable && validatableColumns.includes(column.columnType),
|
|
|
|
|
)
|
|
|
|
|
.forEach((column) => {
|
|
|
|
|
editableColumns.push([column, props.newRow[column.alias]]);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const editedColumn = Object.values(props.primaryColumns).find(
|
2023-04-20 13:12:31 +00:00
|
|
|
(column) => column.alias === props.editableCell?.column,
|
2022-11-05 09:54:20 +00:00
|
|
|
);
|
2022-09-13 05:41:59 +00:00
|
|
|
|
2022-11-05 09:54:20 +00:00
|
|
|
if (validatableColumns.includes(editedColumn.columnType)) {
|
2023-04-20 13:12:31 +00:00
|
|
|
editableColumns.push([editedColumn, props.editableCell?.value]);
|
2022-09-13 05:41:59 +00:00
|
|
|
}
|
2022-11-05 09:54:20 +00:00
|
|
|
}
|
2022-09-13 05:41:59 +00:00
|
|
|
|
2022-11-05 09:54:20 +00:00
|
|
|
const validationMap = {};
|
2022-09-13 05:41:59 +00:00
|
|
|
|
2022-11-05 09:54:20 +00:00
|
|
|
editableColumns.forEach((validationObj) => {
|
|
|
|
|
const editedColumn = validationObj[0];
|
|
|
|
|
const value = validationObj[1];
|
|
|
|
|
|
|
|
|
|
if (editedColumn && editedColumn.validation) {
|
|
|
|
|
const validation = editedColumn.validation;
|
|
|
|
|
|
|
|
|
|
/* General validations */
|
|
|
|
|
if (
|
|
|
|
|
!validation.isColumnEditableCellRequired &&
|
|
|
|
|
(value === "" || _.isNil(value))
|
|
|
|
|
) {
|
|
|
|
|
validationMap[editedColumn.alias] = true;
|
|
|
|
|
return;
|
|
|
|
|
} else if (
|
|
|
|
|
(!_.isNil(validation.isColumnEditableCellValid) &&
|
|
|
|
|
!validation.isColumnEditableCellValid) ||
|
|
|
|
|
(validation.regex && !createRegex(validation.regex).test(value)) ||
|
|
|
|
|
(validation.isColumnEditableCellRequired &&
|
|
|
|
|
(value === "" || _.isNil(value)))
|
|
|
|
|
) {
|
|
|
|
|
validationMap[editedColumn.alias] = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Column type related validations */
|
|
|
|
|
switch (editedColumn.columnType) {
|
|
|
|
|
case "number":
|
2023-11-06 05:35:26 +00:00
|
|
|
case "currency":
|
2022-11-05 09:54:20 +00:00
|
|
|
if (
|
|
|
|
|
!_.isNil(validation.min) &&
|
|
|
|
|
validation.min !== "" &&
|
|
|
|
|
validation.min > value
|
|
|
|
|
) {
|
|
|
|
|
validationMap[editedColumn.alias] = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!_.isNil(validation.max) &&
|
|
|
|
|
validation.max !== "" &&
|
|
|
|
|
validation.max < value
|
|
|
|
|
) {
|
|
|
|
|
validationMap[editedColumn.alias] = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2022-09-13 05:41:59 +00:00
|
|
|
}
|
|
|
|
|
|
2022-11-05 09:54:20 +00:00
|
|
|
validationMap[editedColumn.alias] = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return validationMap;
|
2022-09-13 05:41:59 +00:00
|
|
|
},
|
|
|
|
|
//
|
2022-11-25 06:12:23 +00:00
|
|
|
getTableHeaders: (props, moment, _) => {
|
|
|
|
|
const columns = props.primaryColumns
|
|
|
|
|
? Object.values(props.primaryColumns)
|
|
|
|
|
: [];
|
2023-06-01 17:26:05 +00:00
|
|
|
|
2022-11-25 06:12:23 +00:00
|
|
|
return columns
|
|
|
|
|
.sort((a, b) => a.index - b.index)
|
|
|
|
|
.map((column) => ({
|
|
|
|
|
id: column?.id,
|
|
|
|
|
label: column?.label,
|
|
|
|
|
isVisible: column?.isVisible,
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
//
|
2022-07-14 07:02:35 +00:00
|
|
|
};
|