@bexis2/bexis2-core-ui
Version:
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
397 lines (360 loc) • 12.5 kB
text/typescript
import dateFormat from 'dateformat';
import { SvelteComponent } from 'svelte';
import type { Writable } from 'svelte/store';
import { Send, Receive } from '$models/Models';
import type { FilterOptionsEnum } from '$models/Enums';
import type { Columns, Filter, OrderBy, ServerColumn, ServerConfig } from '$models/Models';
// Function to determine minWidth for a column to simplify the logic in the HTML
export const minWidth = (id: string, columns: Columns | undefined) => {
if (columns && id in columns) {
return columns[id].minWidth ?? 0;
}
return 0;
};
// Function to determine fixedWidth for a column to simplify the logic in the HTML
export const fixedWidth = (id: string, columns: Columns | undefined) => {
if (columns && id in columns) {
return columns[id].fixedWidth ?? 0;
}
return 0;
};
// Function to create custom styles for the columns to simplify the logic in the HTML
export const cellStyle = (id: string, columns: Columns | undefined) => {
const minW = minWidth(id, columns);
const fixedW = fixedWidth(id, columns);
const styles: string[] = [];
// If minWidth is provided, add to styles
minW && styles.push(`min-width: ${minW}px`);
// If fixedWidth is provided, add to styles
fixedW && styles.push(`width: ${fixedW}px`);
// Create and return styles separated by ';'
return styles.join(';');
};
// Styles for resizing the cells
export const getResizeStyles = (
rowHeights: { [key: number]: { max: number; min: number } },
id: string | number,
index: number
) => {
return `
min-height: ${rowHeights && rowHeights[+id] ? `${rowHeights[+id].min}px` : 'auto'};
max-height: ${index !== 0 && rowHeights && rowHeights[+id]
? `${rowHeights[+id].max}px`
: 'auto'
};
height: ${rowHeights && rowHeights[+id] ? `${rowHeights[+id].min}px` : 'auto'};
`;
}
// Function to normalize the filters for back-end
export const normalizeFilters = (filters: {
[key: string]: { [key in FilterOptionsEnum]?: number | string | Date };
}) => {
let filter: Filter[] = [];
// Add filters to the request
Object.keys(filters).forEach((key) => {
Object.keys(filters[key])
.filter((k) => filters[key][k] !== undefined)
.forEach((k) => {
filter.push({
column: key.replaceAll('%%%', '.'),
filterBy: k as FilterOptionsEnum,
value: filters[key][k]
});
});
});
return filter;
};
// Creates a CSV file and downloads it
export const exportAsCsv = (tableId: string, exportedData: string) => {
// Creating a hidden anchor element to download the CSV file
const anchor = document.createElement('a');
anchor.style.display = 'none';
anchor.href = `data:text/csv;charset=utf-8,${encodeURIComponent(exportedData)}`;
anchor.download = `${tableId}.csv`;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
};
// Function to convert JSON data to CSV format
export const jsonToCsv = (data: string): string => {
const json = JSON.parse(data);
if (json.length === 0) return '';
// Extract headers (keys)
const headers = Object.keys(json[0]);
// Escape and format a single cell
const escapeCsvCell = (value: any): string => {
if (value === null || value === undefined) return '';
let cell = String(value);
// Escape quotes by doubling them, and wrap the value in quotes if it contains special characters
if (/[",\n]/.test(cell)) {
cell = `"${cell.replace(/"/g, '""')}"`;
}
return cell;
};
// Create CSV rows
const rows = [
headers.join(','), // Header row
...json.map((row) =>
headers.map(header => escapeCsvCell(row[header])).join(',')
) // Data rows
];
// Join rows with newlines
return rows.join('\n');
}
// Resetting the resized columns and/or rows
export const resetResize = (
headerRows: any,
pageRows: any,
tableId: string,
columns: Columns | undefined,
resizable: 'none' | 'rows' | 'columns' | 'both'
) => {
// Run only if resizable is not none
if (resizable === 'columns' || resizable === 'both') {
headerRows.forEach((row) => {
row.cells.forEach((cell) => {
const minW = minWidth(cell.id, columns);
const fixedW = fixedWidth(cell.id, columns);
// If a fixedWidth is provided for a column, then reset the width to that value
fixedW &&
document
.getElementById(`th-${tableId}-${cell.id}`)
?.style.setProperty('width', `${fixedW}px`);
// If a minWidth is provided for a column, then reset the width to that value
minW &&
document
.getElementById(`th-${tableId}-${cell.id}`)
?.style.setProperty('width', `${minW}px`);
// If neither minWidth nor fixedWidth provided for a column, then reset the width to auto
!minW &&
!fixedW &&
document.getElementById(`th-${tableId}-${cell.id}`)?.style.setProperty('width', 'auto');
});
});
}
if (resizable === 'rows' || resizable === 'both') {
pageRows.forEach((row) => {
row.cells.forEach((cell) => {
// Reset all row heights to auto
document
.getElementById(`${tableId}-${cell.id}-${row.id}`)
?.style.setProperty('height', 'auto');
});
});
}
};
// Finds the mapping for missing values
export const missingValuesFn = (
key: number | string,
missingValues: { [key: string | number]: string }
) => {
const foundKey =
typeof key === 'number' && key.toString().includes('e')
? Object.keys(missingValues).find((item) => {
return (item as string).toLowerCase() === key.toString().toLowerCase();
})
: typeof key === 'string' && parseInt(key).toString().length !== key.length && new Date(key)
? Object.keys(missingValues).find(
(item) => new Date(item).getTime() === new Date(key).getTime()
)
: key in missingValues
? key
: undefined;
return foundKey ? missingValues[foundKey] : key;
};
// Function to update the server-side table data
export const updateTable = async (
pageSize: number,
pageIndex: number,
server: ServerConfig | undefined,
filters: {
[key: string]: { [key in FilterOptionsEnum]?: number | string | Date }
},
data: Writable<any[]>,
serverItems: Writable<number> | undefined,
columns: Columns | undefined,
dispatch: any,
order: OrderBy[] = [],
) => {
const { baseUrl, entityId, versionId, sendModel = new Send() } = server ?? {};
if (!sendModel) throw new Error('Server-side configuration is missing');
sendModel.limit = pageSize;
sendModel.offset = pageSize * pageIndex;
sendModel.version = versionId || -1;
sendModel.id = entityId || -1;
sendModel.filter = normalizeFilters(filters);
sendModel.order = order;
// remove %%% from the columns object
if (sendModel.order) {
sendModel.order.forEach((order) => {
if (order.column.includes("%%%")) {
const newKey = order.column.replaceAll('%%%', '.');
order.column = newKey;
}
});
}
let fetchData;
try {
fetchData = await fetch(baseUrl || '', {
headers: {
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify(sendModel)
});
} catch (error) {
throw new Error(`Network error: ${(error as Error).message}`);
}
if (!fetchData.ok) {
throw new Error('Failed to fetch data');
}
const response: Receive = await fetchData.json();
// Format server columns to the client columns
if (response.columns !== undefined) {
console.log('Server columns', response.columns);
columns = convertServerColumns(response.columns, columns);
const clientCols = response.columns.reduce((acc, col) => {
console.log(col.key, col.column);
// replace the . with empty string
//const key = col.key.replaceAll('.' ,'');
///console.log(key, col.column);
// set the key to the columns object
col.column = col.column.replaceAll('.', "%%%");
col.key = col.key.replaceAll('.', "%%%");
acc[col.key] = col.column
//acc[col.column] = col.column;
return acc;
}, {});
const tmpArr: any[] = [];
// tmp[clientCols["['" + key.replaceAll('.', ",") +"']"]] = row[key];
response.data.forEach((row, index) => {
const tmp: { [key: string]: any } = {};
Object.keys(row).forEach((key) => {
tmp[clientCols[key.replaceAll('.', "%%%") ]] = row[key]
});
tmpArr.push(tmp);
});
dispatch('fetch', columns);
data.set(tmpArr);
console.log('Server data', tmpArr);
return data;
}
serverItems?.set(response.count);
console.log('Server data updated');
// log the columns object
console.log(response);
return response;
};
// Function to convert server data to client data
export const convertServerColumns = (
serverColumns: ServerColumn[],
columns: Columns | undefined
) => {
const columnsConfig: Columns = {};
serverColumns.forEach((col) => {
let instructions = {};
if (col.instructions?.displayPattern) {
let dp = col.instructions.displayPattern;
// Swap 'm' and 'M' to match the backend date format
for (let i = 0; i < col.instructions.displayPattern.length; i++) {
if (col.instructions.displayPattern[i] === 'm') {
dp = `${dp.slice(0, i)}M${dp.slice(i + 1)}`;
} else if (col.instructions.displayPattern[i] === 'M') {
dp = `${dp.slice(0, i)}m${dp.slice(i + 1)}`;
}
}
instructions = {
toStringFn: (date: string) => {
if (col.instructions?.missingValues) {
const missingValue = missingValuesFn(date, col.instructions?.missingValues || {});
if (missingValue === date) {
return dateFormat(new Date(date), dp);
}
return missingValue;
} else {
return dateFormat(new Date(date), dp);
}
},
toSortableValueFn: (date: string) => new Date(date).getTime(),
toFilterableValueFn: (date: string) => new Date(date)
};
} else if (col.instructions?.missingValues) {
instructions = {
...instructions,
toStringFn: (key) => missingValuesFn(key, col.instructions?.missingValues || {})
};
}
if (columns && col.column in columns) {
columnsConfig[col.column.replaceAll('.', "%%%")] = {
...columns[col.column.replaceAll('.', "%%%")],
instructions
};
} else {
columnsConfig[col.column.replaceAll('.', "%%%") ] = {
instructions
};
}
});
console.log('Columns config', columnsConfig);
return columnsConfig;
};
// Calculates the maximum height of the cells in each row
export const getMaxCellHeightInRow = (
tableRef: HTMLTableElement,
resizable: 'columns' | 'rows' | 'none' | 'both',
optionsComponent: typeof SvelteComponent | undefined,
rowHeights: Writable<{ [key: number]: { max: number; min: number } }>,
tableId: string,
rowHeight: number | null
) => {
if (!tableRef || resizable === 'columns' || resizable === 'none') return;
tableRef.querySelectorAll('tbody tr').forEach((row, index) => {
const cells = row.querySelectorAll('td');
let maxHeight = optionsComponent ? 56 : 44;
let minHeight = optionsComponent ? 56 : 44;
cells.forEach((cell) => {
const cellHeight = cell.getBoundingClientRect().height;
// + 2 pixels for rendering borders correctly
if (cellHeight > maxHeight) {
maxHeight = cellHeight + 2;
}
if (cellHeight < minHeight) {
minHeight = cellHeight + 2;
}
});
rowHeights.update((rh) => {
const id = +row.id.split(`${tableId}-row-`)[1];
return {
...rh,
[id]: {
max: maxHeight - 24,
min: Math.max(minHeight - 24, rowHeight ?? 20)
}
};
});
});
};
// Calculates the minimum width of the cells in each column
export const getMinCellWidthInColumn = (
tableRef: HTMLTableElement,
colWidths: Writable<number[]>,
headerRowsLength: number,
resizable: 'columns' | 'rows' | 'none' | 'both'
) => {
if (!tableRef || resizable === 'rows' || resizable === 'none') return;
// Initialize the column widths if they are not already initialized
colWidths.update((cw) => {
if (cw.length === 0) {
return Array.from({ length: headerRowsLength }, () => 100);
}
return cw;
});
colWidths.update((cw) => {
tableRef?.querySelectorAll('thead tr th span').forEach((cell, index) => {
// + 12 pixels for padding and + 32 pixels for filter icon
// If the column width is 100, which means it has not been initialized, then calculate the width
cw[index] = cw[index] === 100 ? cell.getBoundingClientRect().width + 12 + 32 : cw[index];
});
return cw;
});
};