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); }