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`,
},
];