Implement SortModal and TransformModal (WIP)

This commit is contained in:
Jos de Jong 2020-08-16 21:43:21 +02:00
parent 8256bd637c
commit 37fcdf85e2
20 changed files with 538 additions and 32 deletions

152
package-lock.json generated
View File

@ -735,6 +735,12 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
"dev": true
},
"is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@ -745,9 +751,9 @@
}
},
"is-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",
"integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
@ -802,6 +808,11 @@
"iterate-iterator": "^1.0.1"
}
},
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k="
},
"jest-worker": {
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz",
@ -889,12 +900,6 @@
"p-locate": "^4.1.0"
}
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"lodash-es": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
@ -952,9 +957,9 @@
"dev": true
},
"mocha": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.0.1.tgz",
"integrity": "sha512-vefaXfdYI8+Yo8nPZQQi0QO2o+5q9UIMX1jZ1XMmK3+4+CQjc7+B0hPdUeglXiTlr8IHMVRo63IhO9Mzt6fxOg==",
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.1.tgz",
"integrity": "sha512-p7FuGlYH8t7gaiodlFreseLxEmxTgvyG9RgPHODFPySNhwUehu8NIb0vdSt3WFckSneswZ0Un5typYcWElk7HQ==",
"dev": true,
"requires": {
"ansi-colors": "4.1.1",
@ -973,7 +978,7 @@
"ms": "2.1.2",
"object.assign": "4.1.0",
"promise.allsettled": "1.0.2",
"serialize-javascript": "3.0.0",
"serialize-javascript": "4.0.0",
"strip-json-comments": "3.0.1",
"supports-color": "7.1.0",
"which": "2.0.2",
@ -981,7 +986,7 @@
"workerpool": "6.0.0",
"yargs": "13.3.2",
"yargs-parser": "13.1.2",
"yargs-unparser": "1.6.0"
"yargs-unparser": "1.6.1"
},
"dependencies": {
"chokidar": {
@ -1010,10 +1015,13 @@
}
},
"serialize-javascript": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz",
"integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==",
"dev": true
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
}
}
},
@ -1447,6 +1455,11 @@
"strip-indent": "^3.0.0"
}
},
"svelte-select": {
"version": "3.11.1",
"resolved": "https://registry.npmjs.org/svelte-select/-/svelte-select-3.11.1.tgz",
"integrity": "sha512-fva5VRmZT/MnqTMXOlwkEnX+ultdTKdMyguTOngzt77NsXjvPxA7+/M8cUlxyQkAJEqI1RdxCq2RRAAxaJxNBg=="
},
"svelte-simple-modal": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/svelte-simple-modal/-/svelte-simple-modal-0.6.0.tgz",
@ -1673,14 +1686,107 @@
}
},
"yargs-unparser": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
"integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz",
"integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==",
"dev": true,
"requires": {
"camelcase": "^5.3.1",
"decamelize": "^1.2.0",
"flat": "^4.1.0",
"lodash": "^4.17.15",
"yargs": "^13.3.0"
"is-plain-obj": "^1.1.0",
"yargs": "^14.2.3"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"yargs": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
"integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"decamelize": "^1.2.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^15.0.1"
}
},
"yargs-parser": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
"integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
}
}

View File

@ -21,8 +21,10 @@
"ace-builds": "1.4.12",
"ajv": "6.12.3",
"classnames": "2.2.6",
"javascript-natural-sort": "0.7.1",
"lodash-es": "4.17.15",
"svelte-awesome": "2.3.0",
"svelte-select": "3.11.1",
"svelte-simple-modal": "0.6.0"
},
"devDependencies": {
@ -31,7 +33,7 @@
"@rollup/plugin-node-resolve": "8.4.0",
"btoa": "1.2.1",
"mkdirp": "1.0.4",
"mocha": "8.0.1",
"mocha": "8.1.1",
"rollup": "2.23.0",
"rollup-plugin-livereload": "1.3.0",
"rollup-plugin-svelte": "5.2.3",

View File

@ -1,3 +1,5 @@
<svelte:options immutable={true} />
<script>
import Modal from 'svelte-simple-modal'
import TreeMode from './treemode/TreeMode.svelte'
@ -6,6 +8,43 @@
export let config = {}
let ref
export function set (json) {
// TODO: check if the method exists for this mode, if not, throw a clear error
ref.set(json)
}
export function get () {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.get()
}
export function expand (callback) {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.expand(callback)
}
export function collapse (callback) {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.collapse(callback)
}
export function setValidator (newValidate) {
// TODO: check if the method exists for this mode, if not, throw a clear error
ref.setValidator(newValidate)
}
export function getValidator () {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.getValidator()
}
export function patch(operations, newSelection) {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.patch(operations, newSelection)
}
function getRestConfig (config) {
let { mode, ...restConfig } = config
return restConfig
@ -13,5 +52,10 @@
</script>
<Modal>
<svelte:component this={config.mode || DefaultMode} {...getRestConfig(config)} />
<!-- TODO: pass the config options explicitly here? -->
<svelte:component
this={config.mode || DefaultMode}
bind:this={ref}
{...getRestConfig(config)}
/>
</Modal>

View File

@ -0,0 +1,21 @@
@import '../../styles.scss';
.header {
display: flex;
background: $theme-color;
color: $white;
.title {
flex: 1;
padding: $input-padding;
vertical-align: middle;
}
button.close {
min-width: 32px;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
}

View File

@ -0,0 +1,20 @@
<script>
import { getContext } from 'svelte'
import Icon from 'svelte-awesome'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
export let title = 'Modal'
const {close} = getContext('simple-modal')
</script>
<div class="header">
<div class="title">
{title}
</div>
<button class="close" on:click={close}>
<Icon data={faTimes} />
</button>
</div>
<style src="./Header.scss"></style>

View File

@ -0,0 +1,13 @@
@import '../../styles.scss';
.contents {
padding: 20px;
.actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding-top: $padding;
}
}

View File

@ -1 +1,17 @@
@import '../../styles.scss';
@import './Modal.scss';
.sort-modal {
table {
width: 100%;
border-collapse: collapse;
border-spacing: none;
th, td {
text-align: left;
vertical-align: middle;
font-weight: normal;
padding-bottom: $padding;
}
}
}

View File

@ -1,10 +1,138 @@
<svelte:options immutable={true} />
<script>
import { getContext } from 'svelte'
import naturalSort from 'javascript-natural-sort'
import Select from 'svelte-select'
import Header from './Header.svelte'
import { getNestedPaths } from '../../utils/arrayUtils.js'
import { getIn, setIn } from '../../utils/immutabilityHelpers.js'
export let json
export let path
export let onSort
const {close} = getContext('simple-modal')
$: root = getIn(json, path)
$: isArray = Array.isArray(root)
$: paths = isArray ? getNestedPaths(root) : undefined
$: properties = paths?.map(pathToOption)
const asc = {
value: 1,
label: 'asc'
}
const desc = {
value: -1,
label: 'desc'
}
const directions = [ asc, desc ]
let selectedProperty = undefined
let selectedDirection = asc
function pathToOption (path) {
return {
value: path,
label: path.join('.')
}
}
function handleSort () {
if (!selectedProperty || !selectedDirection) {
return
}
// TODO: create a sortBy which returns a JSONPatch document
// TODO: sort object keys when root is an object
const property = selectedProperty.value
const direction = selectedDirection.value
function comparator (a, b) {
const valueA = getIn(a, property)
const valueB = getIn(b, property)
if (valueA === undefined) {
return direction
}
if (valueB === undefined) {
return -direction
}
if (typeof valueA !== 'string' && typeof valueB !== 'string') {
// both values are a number, boolean, or null -> use simple, fast sorting
return valueA > valueB
? direction
: valueA < valueB
? -direction
: 0
}
return direction * naturalSort(valueA, valueB)
}
// TODO: use lodash orderBy, split comparator and direction?
const sorted = root.slice()
sorted.sort(comparator)
onSort(setIn(json, path, sorted))
close()
}
</script>
<div>
Sort Modal...
<div class="sort-modal">
<Header title={isArray ? 'Sort array items' : 'Sort object keys'} />
<div class="contents">
<table>
<colgroup>
<col width="25%">
<col width="75%">
</colgroup>
<tbody>
{#if path.length > 0}
<tr>
<th>Path</th>
<td>{path.join('.')}</td>
</tr>
{/if}
{#if isArray}
<tr>
<th>Property</th>
<td>
<Select
items={properties}
bind:selectedValue={selectedProperty}
/>
</td>
</tr>
{/if}
<tr>
<th>Direction</th>
<td>
<Select
items={directions}
bind:selectedValue={selectedDirection}
isClearable={false}
/>
</td>
</tr>
</tbody>
</table>
<div class="actions">
<button
class="primary"
on:click={handleSort}
disabled={isArray ? !selectedProperty : false}
>
Sort
</button>
</div>
</div>
</div>
<style src="./SortModal.scss"></style>

View File

@ -1 +1,2 @@
@import '../../styles.scss';
@import './Modal.scss';

View File

@ -1,10 +1,17 @@
<svelte:options immutable={true} />
<script>
import { getContext } from 'svelte'
import Header from './Header.svelte'
const {close} = getContext('simple-modal')
</script>
<div>
Transform Modal...
<div class="transform-modal">
<Header title='Transform' />
<div class="contents">
TODO...
</div>
</div>
<style src="./TransformModal.scss"></style>

View File

@ -1,3 +1,5 @@
<svelte:options immutable={true} />
<script>
import { debounce, isEqual } from 'lodash-es'
import { rename } from '../../logic/operations.js'

View File

@ -9,6 +9,7 @@
align-items: center;
position: relative;
// FIXME: should utilize the generic styling in styles.scss
.button {
width: $menu-button-size;
height: $menu-button-size;

View File

@ -1,3 +1,5 @@
<svelte:options immutable={true} />
<script>
import Icon from 'svelte-awesome'
import { faCut, faClone, faCopy, faPaste, faSearch, faUndo, faRedo, faPlus, faTimes, faFilter, faSortAmountDownAlt } from '@fortawesome/free-solid-svg-icons'

View File

@ -1,3 +1,5 @@
<svelte:options immutable={true} />
<script>
import { debounce } from 'lodash-es'
import Icon from 'svelte-awesome'

View File

@ -1,3 +1,5 @@
<svelte:options immutable={true} />
<script>
import { getContext, tick } from 'svelte'
import {
@ -260,7 +262,20 @@
}
function handleSort () {
open(SortModal, {}, SIMPLE_MODAL_OPTIONS)
open(SortModal, {
json: doc,
path: [], // FIXME: based on selection
onSort: sortedDoc => {
console.log('onSort', sortedDoc)
doc = sortedDoc
}
}, {
...SIMPLE_MODAL_OPTIONS,
styleWindow: {
...SIMPLE_MODAL_OPTIONS.styleWindow,
width: '400px'
}
})
}
function handleTransform () {

View File

@ -21,5 +21,9 @@ export const SIMPLE_MODAL_OPTIONS = {
},
styleWindow: {
borderRadius: '2px'
},
styleContent: {
padding: '0px',
overflow: 'visible'
}
}

View File

@ -12,6 +12,8 @@ export default function jsoneditor (config) {
})
}
// modes
export const TreeMode = _TreeMode
// plugins
export { createAjvValidator } from './plugins/createAjvValidator.mjs'

View File

@ -35,6 +35,7 @@ $input-padding: 5px;
$search-box-offset: 10px;
$border-radius: 3px;
$padding: 10px;
$menu-padding: 5px;
$bottom-height: 5px;
@ -49,3 +50,18 @@ button {
font-size: $font-size;
padding: $menu-padding;
}
button.primary {
background: $theme-color;
color: $white;
padding: $padding 2 * $padding;
border-radius: $border-radius;
&:hover {
background: lighten($theme-color, 7%);
}
&:disabled {
background: $gray;
}
}

View File

@ -1,3 +1,9 @@
import { isObject } from './typeUtils.js'
import { compileJSONPointer, parseJSONPointer } from './jsonPointer.js'
const MAX_ITEM_PATHS_COLLECTION = 10000
const EMPTY_ARRAY = []
/**
* Comparator to sort an array in ascending order
*
@ -60,3 +66,41 @@ export function compareArrays(a, b) {
return a.length - b.length
}
/**
* Get the paths of all nested properties in the items of an array
* @param {JSON} json
* @param {boolean} [includeObjects=false] If true, object and array paths are returned as well
* @return {Path[]}
*/
export function getNestedPaths (array, includeObjects = false) {
const pathsMap = {}
if (!Array.isArray(array)) {
throw new TypeError('Array expected')
}
function recurseNestedPaths (obj, path) {
const isValue = !Array.isArray(obj) && !isObject(obj)
if (isValue || includeObjects) {
pathsMap[compileJSONPointer(path)] = true
}
if (isObject(obj)) {
Object.keys(obj).forEach(key => {
recurseNestedPaths(obj[key], path.concat(key))
})
}
}
const max = Math.min(array.length, MAX_ITEM_PATHS_COLLECTION)
for (let i = 0; i < max; i++) {
const item = array[i]
recurseNestedPaths(item, EMPTY_ARRAY)
}
const pathsArray = Object.keys(pathsMap).sort()
return pathsArray.map(parseJSONPointer)
}

View File

@ -1,5 +1,5 @@
import assert from "assert"
import { compareArrays } from './arrayUtils.js'
import { compareArrays, getNestedPaths } from './arrayUtils.js'
describe('arrayUtils', () => {
it('compareArrays', () => {
@ -27,4 +27,64 @@ describe('arrayUtils', () => {
['b', 'c']
])
})
describe('getNestedPaths', () => {
it('should extract all nested paths of an array containing objects', () => {
const json = [
{ name: 'A', location: { latitude: 1, longitude: 2 } },
{ name: 'B', location: { latitude: 1, longitude: 2 } },
{ name: 'C', timestamp: 0 }
]
assert.deepStrictEqual(getNestedPaths(json), [
['location', 'latitude'],
['location', 'longitude'],
['name'],
['timestamp']
])
})
it('should extract a path containing an empty key', () => {
const json = [
{ '': 'empty' }
]
assert.deepStrictEqual(getNestedPaths(json), [
['']
])
})
it('should extract all nested paths of an array containing objects, including objects', () => {
const json = [
{ name: 'A', location: { latitude: 1, longitude: 2 } },
{ name: 'B', location: { latitude: 1, longitude: 2 } },
{ name: 'C', timestamp: 0 }
]
console.log('paths', getNestedPaths(json, true))
assert.deepStrictEqual(getNestedPaths(json, true), [
[],
['location'],
['location', 'latitude'],
['location', 'longitude'],
['name'],
['timestamp']
])
})
it('should extract all nested paths of an array containing values', () => {
const json = [1, 2, 3]
assert.deepStrictEqual(getNestedPaths(json), [
[]
])
})
it('should throw an error when not passing an array', () => {
assert.throws(() => getNestedPaths({ a: 2, b: { c: 3 } }), /TypeError: Array expected/)
assert.throws(() => getNestedPaths('foo'), /TypeError: Array expected/)
assert.throws(() => getNestedPaths(123), /TypeError: Array expected/)
})
})
})