diff --git a/packages/web/src/freetable/ColumnManagerRow.svelte b/packages/web/src/freetable/ColumnManagerRow.svelte
new file mode 100644
index 000000000..c255bd588
--- /dev/null
+++ b/packages/web/src/freetable/ColumnManagerRow.svelte
@@ -0,0 +1,50 @@
+
+
+
+
{column.columnName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/web/src/freetable/ColumnNameEditor.svelte b/packages/web/src/freetable/ColumnNameEditor.svelte
new file mode 100644
index 000000000..0cb5975f7
--- /dev/null
+++ b/packages/web/src/freetable/ColumnNameEditor.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/packages/web/src/freetable/FreeTableColumnEditor.svelte b/packages/web/src/freetable/FreeTableColumnEditor.svelte
new file mode 100644
index 000000000..79d8b462e
--- /dev/null
+++ b/packages/web/src/freetable/FreeTableColumnEditor.svelte
@@ -0,0 +1,82 @@
+
+
+
+
+
+ {#each structure.columns as column, index}
+ {#if index == editingColumn}
+ {
+ dispatchChangeColumns(
+ $$props,
+ cols => cols.map((col, i) => (index == i ? { columnName } : col)),
+ row => _.mapKeys(row, (v, k) => (k == column.columnName ? columnName : k))
+ );
+ }}
+ onBlur={() => (editingColumn = null)}
+ focusOnCreate
+ blurOnEnter
+ existingNames={structure.columns.map(x => x.columnName)}
+ />
+ {:else}
+ (editingColumn = index)}
+ onRemove={() => {
+ dispatchChangeColumns($$props, cols => cols.filter((c, i) => i != index));
+ }}
+ onUp={() => {
+ dispatchChangeColumns($$props, cols => exchange(cols, index, index - 1));
+ }}
+ onDown={() => {
+ dispatchChangeColumns($$props, cols => exchange(cols, index, index + 1));
+ }}
+ />
+ {/if}
+ {/each}
+ {
+ dispatchChangeColumns($$props, cols => [...cols, { columnName }]);
+ }}
+ placeholder="New column"
+ existingNames={structure.columns.map(x => x.columnName)}
+ />
+
diff --git a/packages/web/src/freetable/FreeTableGrid.svelte b/packages/web/src/freetable/FreeTableGrid.svelte
new file mode 100644
index 000000000..68a8c4b3b
--- /dev/null
+++ b/packages/web/src/freetable/FreeTableGrid.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/web/src/freetable/FreeTableGridCore.svelte b/packages/web/src/freetable/FreeTableGridCore.svelte
new file mode 100644
index 000000000..e719f3e25
--- /dev/null
+++ b/packages/web/src/freetable/FreeTableGridCore.svelte
@@ -0,0 +1,39 @@
+
+
+
diff --git a/packages/web/src/freetable/FreeTableGrider.ts b/packages/web/src/freetable/FreeTableGrider.ts
new file mode 100644
index 000000000..008112327
--- /dev/null
+++ b/packages/web/src/freetable/FreeTableGrider.ts
@@ -0,0 +1,85 @@
+import { FreeTableModel } from 'dbgate-datalib';
+import Grider, { GriderRowStatus } from '../datagrid/Grider';
+
+export default class FreeTableGrider extends Grider {
+ public model: FreeTableModel;
+ private batchModel: FreeTableModel;
+
+ constructor(public modelState, public dispatchModel) {
+ super();
+ this.model = modelState && modelState.value;
+ }
+ getRowData(index: any) {
+ return this.model.rows[index];
+ }
+ get rowCount() {
+ return this.model.rows.length;
+ }
+ get currentModel(): FreeTableModel {
+ return this.batchModel || this.model;
+ }
+ set currentModel(value) {
+ if (this.batchModel) this.batchModel = value;
+ else this.dispatchModel({ type: 'set', value });
+ }
+ setCellValue(index: number, uniqueName: string, value: any) {
+ const model = this.currentModel;
+ if (model.rows[index])
+ this.currentModel = {
+ ...model,
+ rows: model.rows.map((row, i) => (index == i ? { ...row, [uniqueName]: value } : row)),
+ };
+ }
+ get editable() {
+ return true;
+ }
+ get canInsert() {
+ return true;
+ }
+ get allowSave() {
+ return true;
+ }
+ insertRow(): number {
+ const model = this.currentModel;
+ this.currentModel = {
+ ...model,
+ rows: [...model.rows, {}],
+ };
+ return this.currentModel.rows.length - 1;
+ }
+ deleteRow(index: number) {
+ const model = this.currentModel;
+ this.currentModel = {
+ ...model,
+ rows: model.rows.filter((row, i) => index != i),
+ };
+ }
+ beginUpdate() {
+ this.batchModel = this.model;
+ }
+ endUpdate() {
+ if (this.model != this.batchModel) {
+ this.dispatchModel({ type: 'set', value: this.batchModel });
+ this.batchModel = null;
+ }
+ }
+
+ // static factory({ modelState, dispatchModel }): FreeTableGrider {
+ // return new FreeTableGrider(modelState, dispatchModel);
+ // }
+ // static factoryDeps({ modelState, dispatchModel }) {
+ // return [modelState, dispatchModel];
+ // }
+ undo() {
+ this.dispatchModel({ type: 'undo' });
+ }
+ redo() {
+ this.dispatchModel({ type: 'redo' });
+ }
+ get canUndo() {
+ return this.modelState.canUndo;
+ }
+ get canRedo() {
+ return this.modelState.canRedo;
+ }
+}
diff --git a/packages/web/src/freetable/MacroPreviewGrider.ts b/packages/web/src/freetable/MacroPreviewGrider.ts
new file mode 100644
index 000000000..6503dc09f
--- /dev/null
+++ b/packages/web/src/freetable/MacroPreviewGrider.ts
@@ -0,0 +1,40 @@
+import { FreeTableModel, MacroDefinition, MacroSelectedCell, runMacro } from 'dbgate-datalib';
+import Grider, { GriderRowStatus } from '../datagrid/Grider';
+import _ from 'lodash';
+
+function convertToSet(row, field) {
+ if (!row) return null;
+ if (!row[field]) return null;
+ if (_.isSet(row[field])) return row[field];
+ return new Set(row[field]);
+}
+
+export default class MacroPreviewGrider extends Grider {
+ model: FreeTableModel;
+ _errors: string[] = [];
+ constructor(model: FreeTableModel, macro: MacroDefinition, macroArgs: {}, selectedCells: MacroSelectedCell[]) {
+ super();
+ this.model = runMacro(macro, macroArgs, model, true, selectedCells, this._errors);
+ }
+
+ get errors() {
+ return this._errors;
+ }
+
+ getRowStatus(index): GriderRowStatus {
+ const row = this.model.rows[index];
+ return {
+ status: (row && row.__rowStatus) || 'regular',
+ modifiedFields: convertToSet(row, '__modifiedFields'),
+ insertedFields: convertToSet(row, '__insertedFields'),
+ deletedFields: convertToSet(row, '__deletedFields'),
+ };
+ }
+
+ 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
new file mode 100644
index 000000000..f614dba4b
--- /dev/null
+++ b/packages/web/src/freetable/macros.js
@@ -0,0 +1,273 @@
+const macros = [
+ {
+ title: 'Remove diacritics',
+ name: 'removeDiacritics',
+ group: 'Text',
+ description: 'Removes diacritics from selected cells',
+ type: 'transformValue',
+ code: `return modules.lodash.deburr(value)`,
+ },
+ {
+ title: 'Search & replace text',
+ name: 'stringReplace',
+ group: 'Text',
+ description: 'Search & replace text or regular expression',
+ type: 'transformValue',
+ args: [
+ {
+ type: 'text',
+ label: 'Find',
+ name: 'find',
+ },
+ {
+ type: 'text',
+ label: 'Replace with',
+ name: 'replace',
+ },
+ {
+ type: 'checkbox',
+ label: 'Case sensitive',
+ name: 'caseSensitive',
+ },
+ {
+ type: 'checkbox',
+ label: 'Regular expression',
+ name: 'isRegex',
+ },
+ ],
+ code: `
+const rtext = args.isRegex ? args.find : modules.lodash.escapeRegExp(args.find);
+const rflags = args.caseSensitive ? 'g' : 'ig';
+return value ? value.toString().replace(new RegExp(rtext, rflags), 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: 'type',
+ default: 'toUpper',
+ },
+ ],
+ code: `return modules.lodash[args.type](value)`,
+ },
+ {
+ title: 'Row index',
+ name: 'rowIndex',
+ group: 'Tools',
+ description: 'Index of row from 1 (autoincrement)',
+ type: 'transformValue',
+ code: `return rowIndex + 1`,
+ },
+ {
+ title: 'Generate UUID',
+ name: 'uuidv1',
+ group: 'Tools',
+ description: 'Generate unique identifier',
+ type: 'transformValue',
+ args: [
+ {
+ type: 'select',
+ options: [
+ { value: 'uuidv1', name: 'V1 - from timestamp' },
+ { value: 'uuidv4', name: 'V4 - random generated' },
+ ],
+ label: 'Version',
+ name: 'version',
+ default: 'uuidv1',
+ },
+ ],
+ code: `return modules[args.version]()`,
+ },
+ {
+ title: 'Current date',
+ name: 'currentDate',
+ group: 'Tools',
+ description: 'Gets current date',
+ type: 'transformValue',
+ args: [
+ {
+ type: 'text',
+ label: 'Format',
+ name: 'format',
+ default: 'YYYY-MM-DD HH:mm:ss',
+ },
+ ],
+ code: `return modules.moment().format(args.format)`,
+ },
+ {
+ title: 'Duplicate rows',
+ name: 'duplicateRows',
+ group: 'Tools',
+ description: 'Duplicate selected rows',
+ type: 'transformRows',
+ code: `
+const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
+const selectedRows = modules.lodash.groupBy(selectedCells, 'row');
+const maxIndex = modules.lodash.max(selectedRowIndexes);
+return [
+ ...rows.slice(0, maxIndex + 1),
+ ...selectedRowIndexes.map(index => ({
+ ...modules.lodash.pick(rows[index], selectedRows[index].map(x => x.column)),
+ __rowStatus: 'inserted',
+ })),
+ ...rows.slice(maxIndex + 1),
+]
+ `,
+ },
+ {
+ title: 'Delete empty rows',
+ name: 'deleteEmptyRows',
+ group: 'Tools',
+ description: 'Delete empty rows - rows with all values null or empty string',
+ type: 'transformRows',
+ code: `
+return rows.map(row => {
+ if (cols.find(col => row[col])) return row;
+ return {
+ ...row,
+ __rowStatus: 'deleted',
+ };
+})
+`,
+ },
+ {
+ title: 'Duplicate columns',
+ name: 'duplicateColumns',
+ group: 'Tools',
+ description: 'Duplicate selected columns',
+ type: 'transformData',
+ code: `
+const selectedColumnNames = modules.lodash.uniq(selectedCells.map(x => x.column));
+const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
+const addedColumnNames = selectedColumnNames.map(col => (args.prefix || '') + col + (args.postfix || ''));
+const resultRows = rows.map((row, rowIndex) => ({
+ ...row,
+ ...(selectedRowIndexes.includes(rowIndex) ? modules.lodash.fromPairs(selectedColumnNames.map(col => [(args.prefix || '') + col + (args.postfix || ''), row[col]])) : {}),
+ __insertedFields: addedColumnNames,
+}));
+const resultCols = [
+ ...cols,
+ ...addedColumnNames,
+];
+return {
+ rows: resultRows,
+ cols: resultCols,
+}
+ `,
+ args: [
+ {
+ type: 'text',
+ label: 'Prefix',
+ name: 'prefix',
+ },
+ {
+ type: 'text',
+ label: 'Postfix',
+ name: 'postfix',
+ default: '_copy',
+ },
+ ],
+ },
+ {
+ title: 'Extract date fields',
+ name: 'extractDateFields',
+ group: 'Tools',
+ description: 'Extract yaear, month, day and other date/time fields from selection and adds it as new columns',
+ type: 'transformData',
+ code: `
+const selectedColumnNames = modules.lodash.uniq(selectedCells.map(x => x.column));
+const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
+const addedColumnNames = modules.lodash.compact([args.year, args.month, args.day, args.hour, args.minute, args.second]);
+const selectedRows = modules.lodash.groupBy(selectedCells, 'row');
+const resultRows = rows.map((row, rowIndex) => {
+ if (!selectedRowIndexes.includes(rowIndex)) return {
+ ...row,
+ __insertedFields: addedColumnNames,
+ };
+ let mom = null;
+ for(const cell of selectedRows[rowIndex]) {
+ const m = modules.moment(row[cell.column]);
+ if (m.isValid()) {
+ mom = m;
+ break;
+ }
+ }
+ if (!mom) return {
+ ...row,
+ __insertedFields: addedColumnNames,
+ };
+
+ const fields = {
+ [args.year]: mom.year(),
+ [args.month]: mom.month() + 1,
+ [args.day]: mom.day(),
+ [args.hour]: mom.hour(),
+ [args.minute]: mom.minute(),
+ [args.second]: mom.second(),
+ };
+
+ return {
+ ...row,
+ ...modules.lodash.pick(fields, addedColumnNames),
+ __insertedFields: addedColumnNames,
+ }
+});
+const resultCols = [
+ ...cols,
+ ...addedColumnNames,
+];
+return {
+ rows: resultRows,
+ cols: resultCols,
+}
+ `,
+ args: [
+ {
+ type: 'text',
+ label: 'Year name',
+ name: 'year',
+ default: 'year',
+ },
+ {
+ type: 'text',
+ label: 'Month name',
+ name: 'month',
+ default: 'month',
+ },
+ {
+ type: 'text',
+ label: 'Day name',
+ name: 'day',
+ default: 'day',
+ },
+ {
+ type: 'text',
+ label: 'Hour name',
+ name: 'hour',
+ default: 'hour',
+ },
+ {
+ type: 'text',
+ label: 'Minute name',
+ name: 'minute',
+ default: 'minute',
+ },
+ {
+ type: 'text',
+ label: 'Second name',
+ name: 'second',
+ default: 'second',
+ },
+ ],
+ },
+];
+
+export default macros;
diff --git a/packages/web/src/query/useEditorData.ts b/packages/web/src/query/useEditorData.ts
index 63a3fa224..b786abd65 100644
--- a/packages/web/src/query/useEditorData.ts
+++ b/packages/web/src/query/useEditorData.ts
@@ -19,7 +19,7 @@ function getParsedLocalStorage(key) {
return null;
}
-export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null }) {
+export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null, onInitialData = null }) {
const localStorageKey = `tabdata_editor_${tabid}`;
let changeCounter = 0;
let savedCounter = 0;
@@ -32,7 +32,6 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
const editorValue = derived(editorState, $state => $state.value);
- let initialData = null;
let value = null;
// const valueRef = React.useRef(null);
@@ -49,8 +48,8 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
...x,
value: init,
}));
+ if (onInitialData) onInitialData(init);
value = init;
- initialData = init;
// mark as not saved
changeCounter += 1;
} catch (err) {
@@ -68,11 +67,11 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
...x,
value: initFallback,
}));
+ if (onInitialData) onInitialData(initFallback);
value = initFallback;
// move to local forage
await localforage.setItem(localStorageKey, initFallback);
localStorage.removeItem(localStorageKey);
- initialData = initFallback;
} else {
const init = await localforage.getItem(localStorageKey);
if (init) {
@@ -80,8 +79,8 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
...x,
value: init,
}));
+ if (onInitialData) onInitialData(init);
value = init;
- initialData = init;
}
}
}
@@ -140,7 +139,6 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
editorState,
editorValue,
setEditorData,
- initialData,
saveToStorage,
saveToStorageSync,
initialLoad,
diff --git a/packages/web/src/tabs/FreeTableTab.svelte b/packages/web/src/tabs/FreeTableTab.svelte
new file mode 100644
index 000000000..445fcd7b3
--- /dev/null
+++ b/packages/web/src/tabs/FreeTableTab.svelte
@@ -0,0 +1,49 @@
+
+
+{#if isLoading}
+
+{:else if errorMessage}
+
+{:else}
+
+{/if}
diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js
index 6127d6bb3..4cbe667bc 100644
--- a/packages/web/src/tabs/index.js
+++ b/packages/web/src/tabs/index.js
@@ -5,7 +5,7 @@ import * as QueryTab from './QueryTab.svelte';
import * as ShellTab from './ShellTab.svelte';
// import InfoPageTab from './InfoPageTab';
import * as ArchiveFileTab from './ArchiveFileTab.svelte';
-// import FreeTableTab from './FreeTableTab';
+import * as FreeTableTab from './FreeTableTab.svelte';
// import PluginTab from './PluginTab';
// import ChartTab from './ChartTab';
import * as MarkdownEditorTab from './MarkdownEditorTab.svelte';
@@ -22,7 +22,7 @@ export default {
// InfoPageTab,
ShellTab,
ArchiveFileTab,
- // FreeTableTab,
+ FreeTableTab,
// PluginTab,
// ChartTab,
MarkdownEditorTab,
diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts
index 57d16c144..24924b4e9 100644
--- a/packages/web/src/utility/metadataLoaders.ts
+++ b/packages/web/src/utility/metadataLoaders.ts
@@ -348,10 +348,10 @@ export function useArchiveFiles(args) {
return useCore(archiveFilesLoader, args);
}
-export function getArchiveFolders(args={}) {
+export function getArchiveFolders(args = {}) {
return getCore(archiveFoldersLoader, args);
}
-export function useArchiveFolders(args={}) {
+export function useArchiveFolders(args = {}) {
return useCore(archiveFoldersLoader, args);
}