Implement Transform modal (WIP)
This commit is contained in:
parent
ad35e6fc16
commit
00d0099355
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
|
@ -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(), [])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue