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 '../../styles.scss';
@import './Modal.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> <script>
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { compileJSONPointer } from '../../utils/jsonPointer';
import Header from './Header.svelte' import Header from './Header.svelte'
export let id
export let json
export let rootPath
export let onTransform
const {close} = getContext('simple-modal') 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> </script>
<div class="jsoneditor-modal transform"> <div class="jsoneditor-modal transform">
<Header title='Transform' /> <Header title='Transform' />
<div class="contents"> <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>
</div> </div>

View File

@ -19,7 +19,8 @@
import { import {
createPathsMap, createPathsMap,
createSelectionFromOperations, createSelectionFromOperations,
expandSelection expandSelection,
findRootPath
} from '../../logic/selection.js' } from '../../logic/selection.js'
import { isContentEditableDiv } from '../../utils/domUtils.js' import { isContentEditableDiv } from '../../utils/domUtils.js'
import { import {
@ -43,6 +44,7 @@
const { open } = getContext('simple-modal') const { open } = getContext('simple-modal')
const sortModalId = uniqueId() const sortModalId = uniqueId()
const transformModalId = uniqueId()
let divContents let divContents
let domHiddenInput let domHiddenInput
@ -264,11 +266,7 @@
} }
function handleSort () { function handleSort () {
const rootPath = selection && selection.paths const rootPath = findRootPath(selection)
? selection.paths.length > 1
? initial(first(selection.paths)) // the parent path of the paths
: first(selection.paths) // the first and only path
: []
open(SortModal, { open(SortModal, {
id: sortModalId, id: sortModalId,
@ -288,7 +286,26 @@
} }
function handleTransform () { 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) { async function handleSearchText (text) {

View File

@ -17,14 +17,14 @@ import { isObject } from '../utils/typeUtils.js'
export function expandSelection (doc, state, anchorPath, focusPath) { export function expandSelection (doc, state, anchorPath, focusPath) {
if (isEqual(anchorPath, focusPath)) { if (isEqual(anchorPath, focusPath)) {
// just a single node // just a single node
return [ anchorPath ] return [anchorPath]
} else { } else {
// multiple nodes // multiple nodes
let sharedPath = findSharedPath(anchorPath, focusPath) const sharedPath = findSharedPath(anchorPath, focusPath)
if (anchorPath.length === sharedPath.length || focusPath.length === sharedPath.length) { if (anchorPath.length === sharedPath.length || focusPath.length === sharedPath.length) {
// a parent and a child, like ['arr', 1] and ['arr'] // a parent and a child, like ['arr', 1] and ['arr']
return [ sharedPath ] return [sharedPath]
} }
const anchorKey = anchorPath[sharedPath.length] const anchorKey = anchorPath[sharedPath.length]
@ -66,7 +66,6 @@ export function expandSelection (doc, state, anchorPath, focusPath) {
} }
/** /**
*
* @param {Selection} selection * @param {Selection} selection
* @return {Path} Returns parent path * @return {Path} Returns parent path
*/ */
@ -94,7 +93,7 @@ export function createSelectionFromOperations (operations) {
.filter(operation => operation.op === 'add' || operation.op === 'copy') .filter(operation => operation.op === 'add' || operation.op === 'copy')
.map(operation => parseJSONPointer(operation.path)) .map(operation => parseJSONPointer(operation.path))
return { return {
paths, paths,
pathsMap: createPathsMap(paths) pathsMap: createPathsMap(paths)
} }
@ -123,10 +122,21 @@ export function createPathsMap (paths) {
*/ */
// TODO: write unit tests for findSharedPath // TODO: write unit tests for findSharedPath
export function findSharedPath (path1, path2) { export function findSharedPath (path1, path2) {
let i = 0; let i = 0
while (i < path1.length && path1[i] === path2[i]) { while (i < path1.length && path1[i] === path2[i]) {
i++; i++
} }
return path1.slice(0, 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 assert from 'assert'
import { expandSelection, getParentPath } from './selection.js' import { expandSelection, getParentPath, findRootPath } from './selection.js'
import { syncState } from './documentState.js' import { syncState } from './documentState.js'
describe ('selection', () => { describe('selection', () => {
const doc = { const doc = {
"obj": { obj: {
"arr": [1,2, {"first":3,"last":4}] arr: [1, 2, { first: 3, last: 4 }]
}, },
"str": "hello world", str: 'hello world',
"nill": null, nill: null,
"bool": false bool: false
} }
const state = syncState(doc, undefined, [], () => true) const state = syncState(doc, undefined, [], () => true)
@ -67,12 +67,27 @@ describe ('selection', () => {
}) })
it('should get parent path from a selection', () => { it('should get parent path from a selection', () => {
assert.deepStrictEqual(getParentPath({ beforePath: ['a', 'b']}), ['a']) assert.deepStrictEqual(getParentPath({ beforePath: ['a', 'b'] }), ['a'])
assert.deepStrictEqual(getParentPath({ appendPath: ['a', 'b']}), ['a', 'b']) assert.deepStrictEqual(getParentPath({ appendPath: ['a', 'b'] }), ['a', 'b'])
assert.deepStrictEqual(getParentPath({ paths:[ assert.deepStrictEqual(getParentPath({
paths: [
['a', 'b'], ['a', 'b'],
['a', 'c'], ['a', 'c'],
['a', 'd'] ['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(), [])
}) })
}) })