Implement transform wizard (WIP)

This commit is contained in:
Jos de Jong 2020-09-02 20:24:11 +02:00
parent d67fffc7d5
commit 22f429e01b
6 changed files with 235 additions and 3 deletions

View File

@ -10,6 +10,7 @@
font-family: $font-family; font-family: $font-family;
font-size: $font-size; font-size: $font-size;
color: $black;
.contents { .contents {
padding: 20px; padding: 20px;
@ -27,5 +28,5 @@
// custom styling for the modal. // custom styling for the modal.
// FIXME: not neat to override global styles! // FIXME: not neat to override global styles!
:global(.bg .window-wrap) { :global(.bg .window-wrap) {
margin-top: 10rem; margin-top: 8rem;
} }

View File

@ -4,7 +4,7 @@
.jsoneditor-modal.transform { .jsoneditor-modal.transform {
.description { .description {
padding-bottom: $padding; color: $dark-gray;
code { code {
background: $background-gray; background: $background-gray;
@ -15,7 +15,8 @@
label { label {
font-weight: bold; font-weight: bold;
padding: $padding/2 0; padding-top: $padding * 2;
padding-bottom: $padding / 2;
display: block; display: block;
} }
@ -31,6 +32,7 @@
padding: $padding / 2; padding: $padding / 2;
font-family: $font-family-mono; font-family: $font-family-mono;
font-size: $font-size-mono; font-size: $font-size-mono;
color: $black;
} }
textarea.preview { textarea.preview {

View File

@ -8,6 +8,7 @@
import { transformModalState } from './transformModalState.js' import { transformModalState } from './transformModalState.js'
import { DEBOUNCE_DELAY, MAX_PREVIEW_CHARACTERS } from '../../constants.js' import { DEBOUNCE_DELAY, MAX_PREVIEW_CHARACTERS } from '../../constants.js'
import { truncate } from '../../utils/stringUtils.js' import { truncate } from '../../utils/stringUtils.js'
import TransformWizard from './TransformWizard.svelte'
import * as _ from 'lodash-es' import * as _ from 'lodash-es'
import { getIn } from '../../utils/immutabilityHelpers.js' import { getIn } from '../../utils/immutabilityHelpers.js'
@ -34,6 +35,11 @@
return queryFn(json) return queryFn(json)
} }
function updateQuery (newQuery) {
console.log('updated query by wizard', newQuery)
query = newQuery
}
function previewTransform(json, query) { function previewTransform(json, query) {
try { try {
const jsonTransformed = evalTransform(json, query) const jsonTransformed = evalTransform(json, query)
@ -96,6 +102,13 @@
<code>_.pick</code>, <code>_.uniq</code>, <code>_.get</code>, etcetera. <code>_.pick</code>, <code>_.uniq</code>, <code>_.get</code>, etcetera.
</div> </div>
<label>Wizard</label>
{#if Array.isArray(json)}
<TransformWizard json={json} onQuery={updateQuery} />
{:else}
(Only available for arrays, not for objects)
{/if}
<label>Query</label> <label>Query</label>
<textarea class="query" bind:value={query} /> <textarea class="query" bind:value={query} />

View File

@ -0,0 +1,36 @@
@import '../../styles.scss';
table.transform-wizard {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
tr {
th {
font-weight: normal;
text-align: left;
}
td {
.horizontal {
width: 100%;
display: flex;
flex-direction: row;
.selectContainer.filter-field {
flex: 4;
margin-right: $padding;
}
.selectContainer.filter-relation {
flex: 1;
margin-right: $padding;
}
.filter-value {
flex: 4;
}
}
}
}
}

View File

@ -0,0 +1,133 @@
<svelte:options immutable={true} />
<script>
import Select from 'svelte-select'
import { getNestedPaths } from '../../utils/arrayUtils.js'
import { stringifyPath } from '../../utils/pathUtils.js'
import { createQuery } from '../../logic/jsCreateQuery.js'
import { isEqual } from 'lodash-es'
export let json
export let onQuery
// fields
let filterField = undefined
let filterRelation = undefined
let filterValue = undefined
let sortField = undefined
let sortDirection = undefined
let pickFields = undefined
// options
$: jsonIsArray = Array.isArray(json)
$: paths = jsonIsArray ? getNestedPaths(json) : undefined
$: fieldOptions = paths ? paths.map(pathToOption) : undefined
const filterRelationOptions = ['==', '!=', '<', '<=', '>', '>='].map(relation => ({
value: relation,
label: relation
}))
const sortDirectionOptions = [
{ value: 'asc', label: 'ascending' },
{ value: 'desc', label: 'descending' },
]
function pathToOption (path) {
return {
value: path,
label: stringifyPath(path)
}
}
let queryOptions = {}
$: {
const newQueryOptions = {}
if (filterField && filterRelation && filterValue) {
newQueryOptions.filter = {
field: filterField.value,
relation: filterRelation.value,
value: filterValue
}
}
if (sortField && sortDirection) {
newQueryOptions.sort = {
field: sortField.value,
direction: sortDirection.value
}
}
if (pickFields) {
newQueryOptions.projection = {
fields: pickFields.map(item => item.value)
}
}
if (!isEqual(newQueryOptions, queryOptions)) {
queryOptions = newQueryOptions
const query = createQuery(json, queryOptions)
console.log('query updated', query, queryOptions)
onQuery(query)
}
}
</script>
<table class="transform-wizard">
<tr>
<th>Filter</th>
<td>
<div class='horizontal'>
<Select
containerClasses='filter-field'
items={fieldOptions}
bind:selectedValue={filterField}
/>
<Select
containerClasses='filter-relation'
items={filterRelationOptions}
bind:selectedValue={filterRelation}
/>
<input
class='filter-value'
bind:value={filterValue}
/>
</div>
</td>
</tr>
<tr>
<th>Sort</th>
<td>
<div class='horizontal'>
<Select
containerClasses='sort-field'
items={fieldOptions}
bind:selectedValue={sortField}
/>
<Select
containerClasses='sort-direction'
items={sortDirectionOptions}
bind:selectedValue={sortDirection}
/>
</div>
</td>
</tr>
<tr>
<th>Pick</th>
<td>
<div class='horizontal'>
<Select
containerClasses='pick-fields'
items={fieldOptions}
isMulti
bind:selectedValue={pickFields}
/>
</div>
</td>
</tr>
</table>
<style src="./TransformWizard.scss"></style>

View File

@ -0,0 +1,47 @@
import { last } from 'lodash-es'
export function createQuery (json, queryOptions) {
console.log('createQuery', queryOptions)
const { filter, sort, projection } = queryOptions
const queryParts = []
if (filter) {
// Note that the comparisons embrace type coercion,
// so a filter value like '5' (text) will match numbers like 5 too.
const getActualValue = filter.field.length > 0
? `item => _.get(item, ${JSON.stringify(filter.field)})`
: 'item => item'
queryParts.push(` data = data.filter(${getActualValue} ${filter.relation} '${filter.value}')\n`)
}
if (sort) {
// The '@' field name is a special case,
// which means that the field itself is selected.
// For example when we have an array containing numbers.
if (sort.field !== '@') {
queryParts.push(` data = _.orderBy(data, '${sort.field}', '${sort.direction}')\n`)
} else {
queryParts.push(` data = _.sortBy(data, '${sort.direction}')\n`)
}
}
if (projection) {
// It is possible to make a util function "pickFlat"
// and use that when building the query to make it more readable.
if (projection.fields.length > 1) {
const fields = projection.fields.map(field => {
const name = last(field)
return ` ${JSON.stringify(name)}: _.get(item, ${JSON.stringify(field)})`
})
queryParts.push(` data = data.map(item => ({\n${fields.join(',\n')}})\n )\n`)
} else {
const field = projection.fields[0]
queryParts.push(` data = data.map(item => _.get(item, ${JSON.stringify(field)}))\n`)
}
}
queryParts.push(' return data\n')
return `function query (data) {\n${queryParts.join('')}}`
}