Implement Transform modal (WIP)

This commit is contained in:
Jos de Jong 2020-09-02 10:25:29 +02:00
parent ad35e6fc16
commit 00d0099355
5 changed files with 183 additions and 27 deletions

View File

@ -1,2 +1,37 @@
@import '../../styles.scss';
@import './Modal.scss';
.jsoneditor-modal.transform {
.description {
padding-bottom: $padding;
}
label {
font-weight: bold;
padding: $padding/2 0;
display: block;
}
textarea.query,
textarea.preview {
border: 1px solid $border-gray;
border-radius: $border-radius;
outline: none;
width: 100%;
height: 100px;
resize: vertical; // prevent resizing horizontally
box-sizing: border-box;
padding: $padding / 2;
font-family: $font-family-mono;
font-size: $font-size-mono;
}
textarea.preview {
height: 200px;
&.error {
color: $red;
}
}
}

View File

@ -2,15 +2,94 @@
<script>
import { getContext } from 'svelte'
import { compileJSONPointer } from '../../utils/jsonPointer';
import Header from './Header.svelte'
export let id
export let json
export let rootPath
export let onTransform
const {close} = getContext('simple-modal')
let query = 'function query (data) {\n return data\n}'
let previewHasError = false
let preview = ''
function evalTransform(json, query) {
// TODO: replace unsafe eval with a JS based query language
const queryFn = eval(`(${query})`)
return queryFn(json)
}
function previewTransform(json, query) {
try {
const jsonTransformed = evalTransform(json, query)
preview = JSON.stringify(jsonTransformed, null, 2) // TODO: limit preview length
previewHasError = false
} catch (err) {
preview = err.toString()
previewHasError = true
}
}
$: {
previewTransform(json, query)
}
function handleTransform () {
try {
const jsonTransformed = evalTransform(json, query)
onTransform([
{
op: 'replace',
path: compileJSONPointer(rootPath),
value: jsonTransformed
}
])
close()
} catch (err) {
// this should never occur since we can only press the Transform
// button when creating a preview was succesful
console.error(err)
preview = err.toString()
previewHasError = true
}
}
</script>
<div class="jsoneditor-modal transform">
<Header title='Transform' />
<div class="contents">
TODO...
<div class='description'>
Enter a JavaScript function to filter, sort, or transform the data.
</div>
<label>Query</label>
<textarea class="query" bind:value={query} />
<label>Preview</label>
<textarea
class="preview"
class:error={previewHasError}
bind:value={preview}
readonly
/>
<div class="actions">
<button
class="primary"
on:click={handleTransform}
disabled={previewHasError}
>
Transform
</button>
</div>
</div>
</div>

View File

@ -19,7 +19,8 @@
import {
createPathsMap,
createSelectionFromOperations,
expandSelection
expandSelection,
findRootPath
} from '../../logic/selection.js'
import { isContentEditableDiv } from '../../utils/domUtils.js'
import {
@ -43,6 +44,7 @@
const { open } = getContext('simple-modal')
const sortModalId = uniqueId()
const transformModalId = uniqueId()
let divContents
let domHiddenInput
@ -264,11 +266,7 @@
}
function handleSort () {
const rootPath = selection && selection.paths
? selection.paths.length > 1
? initial(first(selection.paths)) // the parent path of the paths
: first(selection.paths) // the first and only path
: []
const rootPath = findRootPath(selection)
open(SortModal, {
id: sortModalId,
@ -288,7 +286,26 @@
}
function handleTransform () {
open(TransformModal, {}, SIMPLE_MODAL_OPTIONS)
const rootPath = findRootPath(selection)
open(TransformModal, {
id: transformModalId,
json: getIn(doc, rootPath),
rootPath,
onTransform: async (operations) => {
console.log('onTransform', rootPath, operations)
patch(operations, selection)
// FIXME: replaced node should keep it's expanded state (if expanded)
}
}, {
...SIMPLE_MODAL_OPTIONS,
styleWindow: {
...SIMPLE_MODAL_OPTIONS.styleWindow,
width: '600px'
}
})
}
async function handleSearchText (text) {

View File

@ -17,14 +17,14 @@ import { isObject } from '../utils/typeUtils.js'
export function expandSelection (doc, state, anchorPath, focusPath) {
if (isEqual(anchorPath, focusPath)) {
// just a single node
return [ anchorPath ]
return [anchorPath]
} else {
// multiple nodes
let sharedPath = findSharedPath(anchorPath, focusPath)
const sharedPath = findSharedPath(anchorPath, focusPath)
if (anchorPath.length === sharedPath.length || focusPath.length === sharedPath.length) {
// a parent and a child, like ['arr', 1] and ['arr']
return [ sharedPath ]
return [sharedPath]
}
const anchorKey = anchorPath[sharedPath.length]
@ -66,8 +66,7 @@ export function expandSelection (doc, state, anchorPath, focusPath) {
}
/**
*
* @param {Selection} selection
* @param {Selection} selection
* @return {Path} Returns parent path
*/
export function getParentPath (selection) {
@ -94,7 +93,7 @@ export function createSelectionFromOperations (operations) {
.filter(operation => operation.op === 'add' || operation.op === 'copy')
.map(operation => parseJSONPointer(operation.path))
return {
return {
paths,
pathsMap: createPathsMap(paths)
}
@ -123,10 +122,21 @@ export function createPathsMap (paths) {
*/
// TODO: write unit tests for findSharedPath
export function findSharedPath (path1, path2) {
let i = 0;
let i = 0
while (i < path1.length && path1[i] === path2[i]) {
i++;
i++
}
return path1.slice(0, i)
}
/**
* @param {Selection} selection
*/
export function findRootPath (selection) {
return selection && selection.paths
? selection.paths.length > 1
? initial(first(selection.paths)) // the parent path of the paths
: first(selection.paths) // the first and only path
: []
}

View File

@ -1,15 +1,15 @@
import assert from 'assert'
import { expandSelection, getParentPath } from './selection.js'
import { expandSelection, getParentPath, findRootPath } from './selection.js'
import { syncState } from './documentState.js'
describe ('selection', () => {
describe('selection', () => {
const doc = {
"obj": {
"arr": [1,2, {"first":3,"last":4}]
obj: {
arr: [1, 2, { first: 3, last: 4 }]
},
"str": "hello world",
"nill": null,
"bool": false
str: 'hello world',
nill: null,
bool: false
}
const state = syncState(doc, undefined, [], () => true)
@ -67,12 +67,27 @@ describe ('selection', () => {
})
it('should get parent path from a selection', () => {
assert.deepStrictEqual(getParentPath({ beforePath: ['a', 'b']}), ['a'])
assert.deepStrictEqual(getParentPath({ appendPath: ['a', 'b']}), ['a', 'b'])
assert.deepStrictEqual(getParentPath({ paths:[
assert.deepStrictEqual(getParentPath({ beforePath: ['a', 'b'] }), ['a'])
assert.deepStrictEqual(getParentPath({ appendPath: ['a', 'b'] }), ['a', 'b'])
assert.deepStrictEqual(getParentPath({
paths: [
['a', 'b'],
['a', 'c'],
['a', 'd']
]}), ['a'])
]
}), ['a'])
})
it('should find the root path from a selection', () => {
assert.deepStrictEqual(findRootPath({
paths: [
['a', 'b'],
['a', 'c'],
['a', 'd']
]
}), ['a'])
assert.deepStrictEqual(findRootPath({ beforePath: ['a', 'b'] }), [])
assert.deepStrictEqual(findRootPath({ appendPath: ['a', 'b'] }), [])
assert.deepStrictEqual(findRootPath(), [])
})
})