frontend/apps/quantgrid/src/app/hooks/ManualEditDSL/useTotalManualEditDSL.ts (365 lines of code) (raw):
import { useCallback, useContext } from 'react';
import {
dynamicFieldName,
findFieldNameInExpression,
naExpression,
newLine,
ParsedTable,
ParsedTotal,
SheetReader,
ShortDSLPlacement,
totalKeyword,
TotalType,
} from '@frontend/parser';
import { ProjectContext } from '../../context';
import { autoFixSingleExpression } from '../../services';
import { useDSLUtils } from './useDSLUtils';
export function useTotalManualEditDSL() {
const { projectName, functions, parsedSheets } = useContext(ProjectContext);
const { findContext, findNewTotalSectionOffset, updateDSL } = useDSLUtils();
const removeTotalByField = useCallback(
(tableName: string, fieldName: string, sheetContent: string): string => {
try {
const parsedSheet = SheetReader.parseSheet(sheetContent);
const table = parsedSheet.tables.find((t) => t.tableName === tableName);
const itemsToRemove = getTotalsToRemove(fieldName, table);
return removeTotalItems(sheetContent, itemsToRemove);
} catch (e) {
return sheetContent;
}
},
[]
);
const removeTotalByType = useCallback(
(tableName: string, fieldName: string, type: TotalType) => {
const context = findContext(tableName, fieldName);
if (!context || !projectName) return;
const itemsToRemove = getTotalsToRemove(fieldName, context.table, type);
if (!itemsToRemove.length) return;
const updatedSheetContent = removeTotalItems(
context.sheetContent,
itemsToRemove
);
const historyTitle = `Remove total ${type.toUpperCase()} from ${tableName}[${fieldName}]`;
updateDSL(updatedSheetContent, historyTitle);
},
[findContext, projectName, updateDSL]
);
const removeTotalByIndex = useCallback(
(tableName: string, fieldName: string, index: number) => {
const context = findContext(tableName, fieldName);
if (!context || !projectName) return;
const { table, sheetContent } = context;
const { total } = table;
if (!total || total?.size === 0) return;
const fieldTotals = total.getFieldTotal(fieldName);
if (!fieldTotals || !fieldTotals[index]) return;
const {
fieldNameDslPlacement,
expressionDslPlacement,
totalDslPlacement,
} = fieldTotals[index];
if (
!fieldNameDslPlacement ||
!expressionDslPlacement ||
!totalDslPlacement
)
return;
const totalRow = total.getTotalByIndex(index);
const startOffset =
totalRow.length === 1
? totalDslPlacement.start
: fieldNameDslPlacement.start;
const endOffset =
totalRow.length === 1
? totalDslPlacement.end + 1
: expressionDslPlacement.end + 1;
const dsl =
sheetContent.substring(0, startOffset).trimEnd() +
newLine +
sheetContent.substring(endOffset).trimStart();
const historyTitle = `Remove total ${index} from ${tableName}[${fieldName}]`;
updateDSL(dsl, historyTitle);
},
[findContext, projectName, updateDSL]
);
const addTotalExpression = useCallback(
(
tableName: string,
fieldName: string,
index: number,
expression: string
) => {
const context = findContext(tableName, fieldName);
if (!context || !projectName) return;
const { table, sheetContent } = context;
const { total } = table;
if (!total || total?.size === 0) return;
const fieldTotals = total.getTotalByIndex(index);
if (fieldTotals.length === 0) return;
const firstTotalItem = fieldTotals[0];
if (!firstTotalItem.totalDslPlacement) return;
const { totalDslPlacement } = firstTotalItem;
const fixedExpression = autoFixSingleExpression(
expression,
functions,
parsedSheets,
tableName
);
const dsl =
sheetContent.substring(0, totalDslPlacement.end).trimEnd() +
newLine +
`[${fieldName}] = ${fixedExpression
.trimStart()
.replace('=', '')
.trimStart()}` +
newLine +
sheetContent.substring(totalDslPlacement.end + 1).trimStart();
const historyTitle = `Add total formula for the ${tableName}[${fieldName}]`;
updateDSL(dsl, historyTitle);
},
[findContext, functions, parsedSheets, projectName, updateDSL]
);
const editTotalExpression = useCallback(
(
tableName: string,
fieldName: string,
index: number,
expression: string
) => {
const context = findContext(tableName, fieldName);
if (!context || !projectName) return;
const { table, sheetContent } = context;
const { total } = table;
if (!total || total?.size === 0) return;
const fieldTotals = total.getFieldTotal(fieldName);
if (!fieldTotals || !fieldTotals[index]) return;
const { expressionDslPlacement } = fieldTotals[index];
if (!expressionDslPlacement) return;
const fixedExpression = autoFixSingleExpression(
expression,
functions,
parsedSheets,
tableName
);
const dsl =
sheetContent.substring(0, expressionDslPlacement.start) +
fixedExpression.trimStart().replace('=', '').trimStart() +
sheetContent.substring(expressionDslPlacement.end + 1);
const historyTitle = `Edit total formula for the ${tableName}[${fieldName}]`;
updateDSL(dsl, historyTitle);
},
[findContext, functions, parsedSheets, projectName, updateDSL]
);
const renameTotalField = useCallback(
(
tableName: string,
fieldName: string,
newFieldName: string,
sheetContent: string
): string => {
try {
const parsedSheet = SheetReader.parseSheet(sheetContent);
const table = parsedSheet.tables.find((t) => t.tableName === tableName);
if (!table) return sheetContent;
const { total } = table;
if (!total || total?.size === 0) return sheetContent;
const fieldTotal = total.getFieldTotal(fieldName);
if (!fieldTotal) return sheetContent;
let updatedSheetContent = sheetContent;
const reversedTotalIndexes = Object.keys(fieldTotal).sort(
(a, b) => parseInt(b) - parseInt(a)
);
for (const index of reversedTotalIndexes) {
const rowIndex = parseInt(index);
const totalItem = fieldTotal[rowIndex];
const { fieldNameDslPlacement, expressionDslPlacement, expression } =
totalItem;
if (!fieldNameDslPlacement || !expressionDslPlacement) continue;
try {
const parsedExpression = SheetReader.parseFormula(expression);
const expressionFields = findFieldNameInExpression(parsedExpression)
.filter(
(i) =>
i.tableName === tableName && i.fieldName === `[${fieldName}]`
)
.sort((a, b) => b.start - a.start);
let updatedExpression = expression;
for (const field of expressionFields) {
const { start, end } = field;
updatedExpression =
updatedExpression.substring(0, start) +
`[${newFieldName}]` +
updatedExpression.substring(end + 1);
}
updatedSheetContent =
updatedSheetContent.substring(0, expressionDslPlacement.start) +
updatedExpression +
updatedSheetContent.substring(expressionDslPlacement.end + 1);
} catch (e) {
// empty block
}
updatedSheetContent =
updatedSheetContent.substring(0, fieldNameDslPlacement.start) +
`[${newFieldName}]` +
updatedSheetContent.substring(fieldNameDslPlacement.end);
}
return updatedSheetContent;
} catch (e) {
return sheetContent;
}
},
[]
);
const toggleTotalByType = useCallback(
(tableName: string, fieldName: string, type: TotalType) => {
const context = findContext(tableName, fieldName);
if (!context || !projectName) return;
const { table, sheetContent, field } = context;
const { total } = table;
const targetFieldName =
!field || field?.isDynamic ? dynamicFieldName : fieldName;
const totalSectionExpression = geTotalSectionExpression(
tableName,
targetFieldName,
type
);
const expression = getTotalExpression(tableName, targetFieldName, type);
const historyTitle = `Add total ${type.toUpperCase()} to ${tableName}[${targetFieldName}]`;
if (!total || total?.size === 0) {
const offset = findNewTotalSectionOffset(table);
if (!offset) return;
const dsl = insertTotal(sheetContent, offset, totalSectionExpression);
updateDSL(dsl, historyTitle);
return;
}
const fieldTotal = total.getFieldTotalTypes(targetFieldName);
if (!fieldTotal.includes(type) || type === 'custom') {
const emptyTotalSection = findTargetTotalPlacement(
total,
targetFieldName
);
if (!emptyTotalSection) return;
const { offset, newTotalRequired } = emptyTotalSection;
const exp = newTotalRequired ? totalSectionExpression : expression;
const dsl = insertTotal(sheetContent, offset, exp);
updateDSL(dsl, historyTitle);
return;
}
return removeTotalByType(tableName, targetFieldName, type);
},
[
findContext,
findNewTotalSectionOffset,
projectName,
removeTotalByType,
updateDSL,
]
);
return {
removeTotalByField,
removeTotalByType,
removeTotalByIndex,
toggleTotalByType,
addTotalExpression,
editTotalExpression,
renameTotalField,
};
}
function findTargetTotalPlacement(
total: ParsedTotal,
fieldName: string
): { offset: number; newTotalRequired: boolean } | null {
const totalSize = total.size;
const fieldTotals = total.getFieldTotal(fieldName);
// No totals for source field yet => get existing first total row
if (!fieldTotals) {
const firstTotalRow = total.getTotalByIndex(1);
if (firstTotalRow.length === 0) return null;
const offset = firstTotalRow[0].totalDslPlacement?.end;
return offset ? { offset, newTotalRequired: false } : null;
}
const fieldTotalKeys = Object.keys(fieldTotals);
const existingIndexes = fieldTotalKeys.sort(
(a, b) => parseInt(a) - parseInt(b)
);
// Search for empty placement in the existing rows
for (let i = 1; i <= totalSize; i++) {
if (!existingIndexes.includes(i.toString())) {
const targetTotalRow = total.getTotalByIndex(i);
if (targetTotalRow.length === 0) return null;
const offset = targetTotalRow[0].totalDslPlacement?.end;
return offset ? { offset, newTotalRequired: false } : null;
}
}
// No placement found => create new total row
const offset = total.dslPlacement?.stopOffset;
return offset ? { offset, newTotalRequired: true } : null;
}
function insertTotal(
sheetContent: string,
offset: number,
expression: string
): string {
return (
sheetContent.substring(0, offset + 1).trimEnd() +
`${newLine}${expression}` +
sheetContent.substring(offset + 1).trimStart()
);
}
function geTotalSectionExpression(
tableName: string,
fieldName: string,
type: TotalType
): string {
return `${totalKeyword}${newLine}${getTotalExpression(
tableName,
fieldName,
type
)}`;
}
function getTotalExpression(
tableName: string,
fieldName: string,
type: TotalType
): string {
if (type === 'custom') return `[${fieldName}] = ${naExpression}${newLine}`;
return `[${fieldName}] = ${type.toUpperCase()}(${tableName}[${fieldName}])${newLine}`;
}
function getTotalsToRemove(
fieldName: string,
table?: ParsedTable,
filterType?: TotalType
): ShortDSLPlacement[] {
if (!table || !table.total || table.getTotalSize() === 0) return [];
const fieldTotals = table.total.getFieldTotal(fieldName);
if (!fieldTotals) return [];
const itemsToRemove: ShortDSLPlacement[] = [];
for (const index of Object.keys(fieldTotals)) {
const rowIndex = parseInt(index);
const totalItem = fieldTotals[rowIndex];
const { fieldNameDslPlacement, expressionDslPlacement } = totalItem;
if (filterType && totalItem.type !== filterType) continue;
const totalRow = table.total.getTotalByIndex(rowIndex);
if (totalRow.length === 1 && totalRow[0].totalDslPlacement) {
itemsToRemove.push(totalRow[0].totalDslPlacement);
} else if (fieldNameDslPlacement && expressionDslPlacement) {
itemsToRemove.push({
start: fieldNameDslPlacement.start,
end: expressionDslPlacement.end,
});
}
}
return itemsToRemove;
}
function removeTotalItems(
sheetContent: string,
items: ShortDSLPlacement[]
): string {
if (items.length === 0) return sheetContent;
items.sort((a, b) => b.start - a.start);
let updatedSheetContent = sheetContent;
items.forEach((i) => {
updatedSheetContent =
updatedSheetContent.substring(0, i.start).trimEnd() +
newLine +
updatedSheetContent.substring(i.end + 1).trimStart();
});
return updatedSheetContent;
}