free table editor

This commit is contained in:
Jan Prochazka
2021-03-14 10:40:38 +01:00
parent 159ba72129
commit 7cd26c4fe4
12 changed files with 753 additions and 10 deletions

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
export let column;
export let onEdit;
export let onRemove;
export let onUp;
export let onDown;
</script>
<div class="row">
<div class="name">{column.columnName}</div>
<div class="nowrap">
<span class="icon" on:click={onEdit}>
<FontIcon icon="icon edit" />
</span>
<span class="icon" on:click={onRemove}>
<FontIcon icon="icon delete" />
</span>
<span class="icon" on:click={onUp}>
<FontIcon icon="icon arrow-up" />
</span>
<span class="icon" on:click={onDown}>
<FontIcon icon="icon arrow-down" />
</span>
</div>
</div>
<style>
.row {
display: flex;
justify-content: space-between;
cursor: pointer;
}
.row:hover {
background-color: var(--theme-bg-selected);
}
.name {
white-space: nowrap;
margin: 5px;
}
.icon {
position: relative;
top: 5px;
padding: 5px;
}
.icon:hover {
background-color: var(--theme-bg-3);
}
</style>

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { onMount } from 'svelte';
import keycodes from '../utility/keycodes';
export let onEnter;
export let onBlur = undefined;
export let focusOnCreate = false;
export let blurOnEnter = false;
export let existingNames;
export let defaultValue = '';
let domEditor;
let value = defaultValue || '';
$: isError = value && existingNames && existingNames.includes(value);
const handleKeyDown = event => {
if (value && event.keyCode == keycodes.enter && !isError) {
onEnter(value);
value = '';
if (blurOnEnter) domEditor.blur();
}
if (event.keyCode == keycodes.escape) {
value = '';
domEditor.blur();
}
};
const handleBlur = () => {
if (value && !isError) {
onEnter(value);
value = '';
}
if (onBlur) onBlur();
};
if (focusOnCreate) onMount(() => domEditor.focus());
</script>
<input
type="text"
{...$$restProps}
bind:value
bind:this={domEditor}
on:keydown={handleKeyDown}
on:blur={handleBlur}
class:isError
/>
<style>
input {
width: calc(100% - 10px);
}
input.isError {
background: var(--theme-bg-red);
}
</style>

View File

@@ -0,0 +1,82 @@
<script context="module">
function dispatchChangeColumns(props, func, rowFunc = null) {
const { modelState, dispatchModel } = props;
const model = modelState.value;
dispatchModel({
type: 'set',
value: {
rows: rowFunc ? model.rows.map(rowFunc) : model.rows,
structure: {
...model.structure,
columns: func(model.structure.columns),
},
},
});
}
function exchange(array, i1, i2) {
const i1r = (i1 + array.length) % array.length;
const i2r = (i2 + array.length) % array.length;
const res = [...array];
[res[i1r], res[i2r]] = [res[i2r], res[i1r]];
return res;
}
</script>
<script>
import _ from 'lodash';
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
import ColumnManagerRow from './ColumnManagerRow.svelte';
import ColumnNameEditor from './ColumnNameEditor.svelte';
export let modelState;
export let dispatchModel;
export let managerSize;
let editingColumn = null;
$: structure = modelState.value.structure;
</script>
<ManagerInnerContainer width={managerSize}>
{#each structure.columns as column, index}
{#if index == editingColumn}
<ColumnNameEditor
defaultValue={column.columnName}
onEnter={columnName => {
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}
<ColumnManagerRow
{column}
onEdit={() => (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}
<ColumnNameEditor
onEnter={columnName => {
dispatchChangeColumns($$props, cols => [...cols, { columnName }]);
}}
placeholder="New column"
existingNames={structure.columns.map(x => x.columnName)}
/>
</ManagerInnerContainer>

View File

@@ -0,0 +1,72 @@
<script lang="ts" context="module">
function extractMacroValuesForMacro(macroValues, macro) {
if (!macro) return {};
return {
..._.fromPairs((macro.args || []).filter(x => x.default != null).map(x => [x.name, x.default])),
..._.mapKeys(macroValues, (v, k) => k.replace(/^.*#/, '')),
};
}
</script>
<script lang="ts">
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
import ColumnManager from '../datagrid/ColumnManager.svelte';
import ReferenceManager from '../datagrid/ReferenceManager.svelte';
import FreeTableGridCore from './FreeTableGridCore.svelte';
import FreeTableColumnEditor from './FreeTableColumnEditor.svelte';
let managerSize;
let selectedMacro;
</script>
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
<div class="left" slot="1">
<WidgetColumnBar>
<WidgetColumnBarItem title="Columns" name="columns" height="40%">
<FreeTableColumnEditor {...$$props} {managerSize} />
</WidgetColumnBarItem>
<!-- <WidgetColumnBarItem title="Macros" name="macros">
<MacroManager {...$$props} {managerSize} />
</WidgetColumnBarItem> -->
</WidgetColumnBar>
</div>
<div class="grid" slot="2">
<VerticalSplitter initialValue="70%">
<svelte:fragment slot="1">
<FreeTableGridCore {...$$props} />
</svelte:fragment>
<!-- macroPreview={selectedMacro}
macroValues={extractMacroValuesForMacro(macroValues, selectedMacro)}
onSelectionChanged={setSelectedCells}
{setSelectedMacro} -->
<!-- {#if selectedMacro}
<MacroDetail
{selectedMacro}
{setSelectedMacro}
onChangeValues={setMacroValues}
{macroValues}
onExecute={handleExecuteMacro}
/>
{/if} -->
</VerticalSplitter>
</div>
</HorizontalSplitter>
<style>
.left {
display: flex;
flex: 1;
background-color: var(--theme-bg-0);
}
.grid {
position: relative;
flex-grow: 1;
}
</style>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import { createGridCache, FreeTableGridDisplay } from 'dbgate-datalib';
import { writable } from 'svelte/store';
import uuidv1 from 'uuid/v1';
import DataGridCore from '../datagrid/DataGridCore.svelte';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import axiosInstance from '../utility/axiosInstance';
import FreeTableGrider from './FreeTableGrider';
import MacroPreviewGrider from './MacroPreviewGrider';
export let macroPreview;
export let modelState;
export let dispatchModel;
export let macroValues;
export let config;
export let setConfig;
let selectedCells = [];
const cache = writable(createGridCache());
$: grider = macroPreview
? new MacroPreviewGrider(modelState.value, macroPreview, macroValues, selectedCells)
: new FreeTableGrider(modelState, dispatchModel);
$: display = new FreeTableGridDisplay(grider.model || modelState.value, config, setConfig, $cache, cache.update);
async function exportGrid() {
const jslid = uuidv1();
await axiosInstance.post('jsldata/save-free-table', { jslid, data: modelState.value });
const initialValues: any = {};
initialValues.sourceStorageType = 'jsldata';
initialValues.sourceJslId = jslid;
initialValues.sourceList = ['editor-data'];
showModal(ImportExportModal, { initialValues: initialValues });
}
</script>
<DataGridCore {...$$props} {grider} {display} frameSelection={!!macroPreview} {exportGrid} onExportGrid={exportGrid} />

View File

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

View File

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

View File

@@ -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;