diff --git a/packages/datalib/src/MacroDefinition.ts b/packages/datalib/src/MacroDefinition.ts new file mode 100644 index 000000000..0ef83d0cf --- /dev/null +++ b/packages/datalib/src/MacroDefinition.ts @@ -0,0 +1,22 @@ +import _ from 'lodash'; + +export interface MacroArgument { + type: 'text' | 'select'; + label: string; + name: string; +} + +export interface MacroDefinition { + title: string; + name: string; + group: string; + description?: string; + type: 'transformValue'; + code: string; + args?: MacroArgument[]; +} + +export interface MacroSelectedCell { + column: string; + row: number; +} diff --git a/packages/datalib/src/index.ts b/packages/datalib/src/index.ts index 3a293d13e..5185f22f5 100644 --- a/packages/datalib/src/index.ts +++ b/packages/datalib/src/index.ts @@ -7,3 +7,5 @@ export * from "./ChangeSet"; export * from "./filterName"; export * from "./FreeTableGridDisplay"; export * from "./FreeTableModel"; +export * from "./MacroDefinition"; +export * from "./runMacro"; diff --git a/packages/datalib/src/runMacro.ts b/packages/datalib/src/runMacro.ts new file mode 100644 index 000000000..8185c4a4d --- /dev/null +++ b/packages/datalib/src/runMacro.ts @@ -0,0 +1,59 @@ +import { FreeTableModel } from './FreeTableModel'; +import _ from 'lodash'; +import { MacroDefinition, MacroSelectedCell } from './MacroDefinition'; + +const getMacroFunction = { + transformValue: (code) => ` +(value, args, modules, rowIndex, row, columnName) => { + ${code} +} +`, +}; + +const modules = { + lodash: _, +}; + +export function runMacro( + macro: MacroDefinition, + macroArgs: {}, + data: FreeTableModel, + preview: boolean, + selectedCells: MacroSelectedCell[] +): FreeTableModel { + const func = eval(getMacroFunction[macro.type](macro.code)); + if (macro.type == 'transformValue') { + const selectedRows = _.groupBy(selectedCells, 'row'); + const rows = data.rows.map((row, rowIndex) => { + const selectedRow = selectedRows[rowIndex]; + if (selectedRow) { + const columnSet = new Set(selectedRow.map((item) => item.column)); + const changedValues = []; + const res = _.mapValues(row, (value, key) => { + if (columnSet.has(key)) { + const newValue = func(value, macroArgs, modules, rowIndex, row, key); + if (preview && newValue != value) changedValues.push(key); + return newValue; + } else { + return value; + } + }); + if (changedValues.length > 0) { + return { + ...res, + __changedValues: new Set(changedValues), + }; + } + return res; + } else { + return row; + } + }); + + return { + structure: data.structure, + rows, + }; + } + return data; +} diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index ad876a346..34860865c 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -109,6 +109,7 @@ export default function DataGridCore(props) { onSave, isLoading, grider, + onSelectionChanged, } = props; // console.log('RENDER GRID', display.baseTable.pureName); const columns = React.useMemo(() => display.allColumns, [display]); @@ -205,6 +206,12 @@ export default function DataGridCore(props) { } }, [tabVisible, focusFieldRef.current]); + React.useEffect(() => { + if (onSelectionChanged) { + onSelectionChanged(getSelectedMacroCells()); + } + }, [onSelectionChanged, selectedCells]); + const maxScrollColumn = React.useMemo(() => { let newColumn = columnSizes.scrollInView(0, columns.length - 1 - columnSizes.frozenCount, gridScrollAreaWidth); return newColumn; @@ -530,6 +537,32 @@ export default function DataGridCore(props) { return _.uniq((selectedCells || []).map((x) => x[1])).filter((x) => _.isNumber(x)); } + function getSelectedRegularCells() { + if (selectedCells.find((x) => x[0] == 'header' && x[1] == 'header')) { + const row = _.range(0, realColumnUniqueNames.length); + return _.range(0, grider.rowCount).map((rowIndex) => row.map((colIndex) => [rowIndex, colIndex])); + } + const res = []; + for (const cell of selectedCells) { + if (isRegularCell(cell)) res.push(cell); + else if (cell[0] == 'header' && _.isNumber(cell[1])) { + res.push(..._.range(0, grider.rowCount).map((rowIndex) => [rowIndex, cell[1]])); + } else if (cell[1] == 'header' && _.isNumber(cell[0])) { + res.push(..._.range(0, realColumnUniqueNames.length).map((colIndex) => [cell[0], colIndex])); + } + } + return res; + } + + function getSelectedMacroCells() { + const regular = getSelectedRegularCells(); + // @ts-ignore + return regular.map((cell) => ({ + row: cell[0], + column: realColumnUniqueNames[cell[1]], + })); + } + function getSelectedRowData() { return _.compact(getSelectedRowIndexes().map((index) => grider.getRowData(index))); } diff --git a/packages/web/src/freetable/FreeTableGridCore.js b/packages/web/src/freetable/FreeTableGridCore.js index f2c867bae..f203894ca 100644 --- a/packages/web/src/freetable/FreeTableGridCore.js +++ b/packages/web/src/freetable/FreeTableGridCore.js @@ -2,16 +2,36 @@ import { createGridCache, FreeTableGridDisplay } from '@dbgate/datalib'; import React from 'react'; import DataGridCore from '../datagrid/DataGridCore'; import FreeTableGrider from './FreeTableGrider'; +import MacroPreviewGrider from './MacroPreviewGrider'; export default function FreeTableGridCore(props) { const { modelState, dispatchModel, config, setConfig, macroPreview, macroValues } = props; - const grider = React.useMemo(() => FreeTableGrider.factory(props), FreeTableGrider.factoryDeps(props)); const [cache, setCache] = React.useState(createGridCache()); + const [selectedCells, setSelectedCells] = React.useState([]); + const grider = React.useMemo( + () => + macroPreview + ? new MacroPreviewGrider(modelState.value, macroPreview, macroValues, selectedCells) + : FreeTableGrider.factory(props), + [ + ...FreeTableGrider.factoryDeps(props), + macroPreview, + macroPreview ? macroValues : null, + macroPreview ? selectedCells : null, + ] + ); const display = React.useMemo(() => new FreeTableGridDisplay(modelState.value, config, setConfig, cache, setCache), [ modelState.value, config, cache, ]); - return ; + return ( + + ); } diff --git a/packages/web/src/freetable/MacroParameters.js b/packages/web/src/freetable/MacroParameters.js index 1dceab963..55ce3ff2c 100644 --- a/packages/web/src/freetable/MacroParameters.js +++ b/packages/web/src/freetable/MacroParameters.js @@ -1,11 +1,28 @@ import React from 'react'; -import { FormTextField, FormSubmit, FormArchiveFolderSelect, FormRow, FormLabel } from '../utility/forms'; +import _ from 'lodash'; +import { + FormTextField, + FormSubmit, + FormArchiveFolderSelect, + FormRow, + FormLabel, + FormSelectField, +} from '../utility/forms'; import { Formik, Form, useFormikContext } from 'formik'; function MacroArgument({ arg }) { if (arg.type == 'text') { return ; } + if (arg.type == 'select') { + return ( + + {arg.options.map((opt) => + _.isString(opt) ? : + )} + + ); + } return null; } diff --git a/packages/web/src/freetable/MacroPreviewGrider.ts b/packages/web/src/freetable/MacroPreviewGrider.ts new file mode 100644 index 000000000..9d0cb35fb --- /dev/null +++ b/packages/web/src/freetable/MacroPreviewGrider.ts @@ -0,0 +1,17 @@ +import { FreeTableModel, MacroDefinition, MacroSelectedCell, runMacro } from '@dbgate/datalib'; +import Grider from '../datagrid/Grider'; + +export default class MacroPreviewGrider extends Grider { + model: FreeTableModel; + constructor(model: FreeTableModel, macro: MacroDefinition, macroArgs: {}, selectedCells: MacroSelectedCell[]) { + super(); + this.model = runMacro(macro, macroArgs, model, true, selectedCells); + } + + getRowData(index: any) { + return this.model.rows[index]; + } + get rowCount() { + return this.model.rows.length; + } +} diff --git a/packages/web/src/freetable/macros.js b/packages/web/src/freetable/macros.js index 2e6301612..68ee5474e 100644 --- a/packages/web/src/freetable/macros.js +++ b/packages/web/src/freetable/macros.js @@ -2,15 +2,15 @@ const macros = [ { title: 'Remove diacritics', name: 'removeDiacritics', - group: 'text', + group: 'Text', description: 'Removes diacritics from selected cells', type: 'transformValue', - code: `value => modules.diacritics.remove(value)`, + code: `return modules.lodash.deburr(value)`, }, { title: 'Search & replace text', name: 'stringReplace', - group: 'text', + group: 'Text', description: 'Search & replace text or regular expression', type: 'transformValue', args: [ @@ -25,7 +25,31 @@ const macros = [ name: 'replace', }, ], - code: `value => value ? value.toString().replace(args.find, args.replace) : value`, + code: `return value ? value.toString().replace(args.find, args.replace) : value`, + }, + { + title: 'Change text case', + name: 'changeTextCase', + group: 'Text', + description: 'Uppercase, lowercase and other case functions', + type: 'transformValue', + args: [ + { + type: 'select', + options: ['toUpper', 'toLower', 'lowerCase', 'upperCase', 'kebabCase', 'snakeCase', 'camelCase', 'startCase'], + label: 'Type', + name: 'caseTransform', + }, + ], + code: `return modules.lodash[args.caseTransform || 'toUpper'](value)`, + }, + { + title: 'Row index', + name: 'rowIndex', + group: 'Tools', + description: 'index of row from 1 (autoincrement)', + type: 'transformValue', + code: `return rowIndex + 1`, }, ];