Skip to content

Commit

Permalink
feat: implement validation errors overview and validation error annot…
Browse files Browse the repository at this point in the history
…ations in code mode (#6)

* fix: validation errors on an object/array not visible when expanded

* feat: implement validation errors overview in TreeMode (WIP)

* feat: add validation errors overview to CodeMode (WIP)

* feat: show annotation with JSON schema errors in code mode

* feat: make validation error overview expandable/collapsable

* fix: positioning of floating context menu button and validation error icons

* fix: editor not getting focus after clicking on a validation error

* fix: validation errors not being updated when changed via public API

* fix: do not show validation error summary when there is only one error, and remember collapsed state
  • Loading branch information
josdejong authored May 13, 2021
1 parent 4a3da2c commit b206f10
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 23 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"debug": "^4.3.1",
"diff-sequences": "^26.6.2",
"immutable-json-patch": "^1.1.1",
"json-source-map": "^0.6.1",
"jsonrepair": "^2.2.0",
"lodash-es": "^4.17.21",
"natural-compare-lite": "^1.4.0",
Expand Down
56 changes: 56 additions & 0 deletions src/components/controls/ValidationErrorsOverview.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@import '../../styles.scss';

.validation-errors-overview {
font-family: $font-family-mono;
font-size: $font-size-mono;
background: $warning-color;
color: $white;
overflow: auto;
max-height: $errors-overview-max-height;

table {
border-collapse: collapse;
width: 100%;

tr {
cursor: pointer;

&:hover {
background-color: rgba(255, 255, 255, 0.1);
}

td {
padding: 4px $padding;
vertical-align:middle;

&.validation-error-icon {
width: 36px;
box-sizing: border-box;
}

&.validation-error-action {
width: 36px;
box-sizing: border-box;
padding: 0;

button.validation-errors-collapse {
width: 36px;
height: 26px;
cursor: pointer;

&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
}
}

div.validation-errors-expand {
display: inline-block;
position: relative;
top: 3px;
// TODO: position this icon in a better way
}
}
}
}
}
93 changes: 93 additions & 0 deletions src/components/controls/ValidationErrorsOverview.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<svelte:options immutable={true} />

<script>
import {
faAngleDown,
faAngleRight,
faExclamationTriangle
} from '@fortawesome/free-solid-svg-icons'
import { isEmpty } from 'lodash-es'
import Icon from 'svelte-awesome'
import { stringifyPath } from '../../utils/pathUtils.js'
/**
* @type {ValidationError[]}
**/
export let validationErrorsList
/**
* @type {function(error: ValidationError)}
*/
export let selectError
let expanded = true
function collapse () {
expanded = false
}
function expand () {
expanded = true
}
$: filteredValidationErrors = validationErrorsList.filter(error => !error.isChildError)
</script>

{#if !isEmpty(validationErrorsList)}
<div class="validation-errors-overview">
{#if expanded || validationErrorsList.length === 1}
<table>
<tbody>
{#each validationErrorsList as validationError, index}
<tr
class="validation-error"
on:click={() => {
// trigger on the next tick to prevent the editor not getting focus
setTimeout(() => selectError(validationError))
}}
>
<td class="validation-error-icon">
<Icon data={faExclamationTriangle} />
</td>
<td>
{stringifyPath(validationError.path)}
</td>
<td>
{validationError.message}
</td>
<td class="validation-error-action">
{#if index === 0 && validationErrorsList.length > 1}
<button
class="validation-errors-collapse"
on:click|stopPropagation={collapse}
title="Collapse validation errors"
>
<Icon data={faAngleDown} />
</button>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<table>
<tbody>
<tr class="validation-error" on:click={expand}>
<td class="validation-error-icon">
<Icon data={faExclamationTriangle} />
</td>
<td>
{validationErrorsList.length} validation errors
<div class="validation-errors-expand">
<Icon data={faAngleRight} />
</div>
</td>
</tr>
</tbody>
</table>
{/if}
</div>
{/if}

<style src="./ValidationErrorsOverview.scss"></style>
100 changes: 87 additions & 13 deletions src/components/modes/codemode/CodeMode.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script>
import {
faExclamationTriangle,
faInfoCircle,
faWrench
} from '@fortawesome/free-solid-svg-icons'
import createDebug from 'debug'
Expand All @@ -14,16 +13,19 @@
JSON_STATUS_INVALID,
JSON_STATUS_REPAIRABLE,
JSON_STATUS_VALID,
MAX_DOCUMENT_SIZE_CODE_MODE,
MAX_AUTO_REPAIRABLE_SIZE,
MAX_DOCUMENT_SIZE_CODE_MODE,
SORT_MODAL_OPTIONS,
TRANSFORM_MODAL_OPTIONS
} from '../../../constants.js'
import { activeElementIsChildOf, getWindow } from '../../../utils/domUtils.js'
import { keyComboFromEvent } from '../../../utils/keyBindings.js'
import { formatSize } from '../../../utils/fileUtils.js'
import { findTextLocation } from '../../../utils/jsonUtils.js'
import { keyComboFromEvent } from '../../../utils/keyBindings.js'
import { createFocusTracker } from '../../controls/createFocusTracker.js'
import Message from '../../controls/Message.svelte'
import ValidationErrorsOverview
from '../../controls/ValidationErrorsOverview.svelte'
import SortModal from '../../modals/SortModal.svelte'
import TransformModal from '../../modals/TransformModal.svelte'
import ace from './ace/index.js'
Expand All @@ -34,9 +36,10 @@
export let text = ''
export let indentation = 2 // TODO: make indentation configurable
export let aceTheme = 'ace/theme/jsoneditor' // TODO: make aceTheme configurable
export let validator
export let validator = null
export let onChange = null
export let onSwitchToTreeMode = () => {}
export let onSwitchToTreeMode = () => {
}
export let onError
export let onFocus = () => {
}
Expand All @@ -55,6 +58,8 @@
let onChangeDisabled = false
let acceptTooLarge = false
let validationErrorsList = []
$: isNewDocument = text.length === 0
$: tooLarge = text && text.length > MAX_DOCUMENT_SIZE_CODE_MODE
$: aceEditorDisabled = tooLarge && !acceptTooLarge
Expand Down Expand Up @@ -281,6 +286,30 @@
setAceEditorValue(text, true)
}
/**
* @param {ValidationError} error
**/
function handleSelectValidationError (error) {
debug('select validation error', error)
const annotation = validationErrorToAnnotation(error)
const location = {
row: annotation.row,
column: annotation.column
}
setSelection(location, location)
focus()
}
/**
* @param {Point} start
* @param {Point} end
**/
function setSelection (start, end) {
aceEditor.selection.setRange({ start, end })
aceEditor.scrollToLine(start.row, true)
}
function createAceEditor ({ target, ace, readOnly, indentation, onChange }) {
debug('create Ace editor')
Expand All @@ -306,12 +335,53 @@
aceEditor.commands.bindKey('Ctrl-Shift-\\', null)
aceEditor.commands.bindKey('Command-Shift-\\', null)
// replace ace setAnnotations with custom function that also covers jsoneditor annotations
const originalSetAnnotations = aceSession.setAnnotations
aceSession.setAnnotations = function (annotations) {
const newAnnotations = annotations && annotations.length
? annotations
: validationErrorsList.map(validationErrorToAnnotation)
debug('setAnnotations', { annotations, newAnnotations })
originalSetAnnotations.call(this, newAnnotations)
}
// register onchange event
aceEditor.on('change', onChange)
return aceEditor
}
function validationErrorToAnnotation (validationError) {
const location = findTextLocation(text, validationError.path)
return {
row: location ? location.row : 0,
column: location ? location.column : 0,
text: validationError.message,
type: 'warning'
}
}
/**
* refresh ERROR annotations state
* error annotations are handled by the ace json mode (ace/mode/json)
* validation annotations are handled by this mode
* therefore in order to refresh we send only the annotations of error type in order to maintain its state
* @private
*/
function refreshAnnotations () {
debug('refresh annotations')
const session = aceEditor && aceEditor.getSession()
if (session) {
const errorAnnotations = session.getAnnotations()
.filter(annotation => annotation.type === 'error')
session.setAnnotations(errorAnnotations)
}
}
function setAceEditorValue (text, force = false) {
if (aceEditorDisabled && !force) {
debug('not applying text: editor is disabled')
Expand Down Expand Up @@ -373,6 +443,7 @@
function checkValidJson (text) {
jsonStatus = JSON_STATUS_VALID
jsonParseError = undefined
validationErrorsList = []
// FIXME: utilize the parse errors coming from AceEditor worker, only try to repair then
if (text.length > MAX_AUTO_REPAIRABLE_SIZE) {
Expand All @@ -388,7 +459,13 @@
try {
// FIXME: instead of parsing the JSON here (which is expensive),
// get the parse error from the Ace Editor worker instead
JSON.parse(text)
const json = JSON.parse(text)
if (validator) {
validationErrorsList = validator(json)
}
refreshAnnotations()
} catch (err) {
jsonParseError = err.toString()
try {
Expand Down Expand Up @@ -473,13 +550,10 @@
/>
{/if}

{#if validator}
<Message
type="warning"
icon={faInfoCircle}
message="This BETA version of code mode doesn't yet have support for JSON Schema or custom validators."
/>
{/if}
<ValidationErrorsOverview
validationErrorsList={validationErrorsList}
selectError={handleSelectValidationError}
/>
</div>

<style src="./CodeMode.scss"></style>
12 changes: 6 additions & 6 deletions src/components/modes/treemode/JSONNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -310,16 +310,16 @@
</div>
{/if}
</div>
{#if validationError && (!expanded || !validationError.isChildError)}
<ValidationError validationError={validationError} onExpand={handleExpand} />
{/if}
{#if expanded}
<div
class="insert-selection-area inside"
data-type="insert-selection-area-inside"
on:mousedown={handleInsertInside}
></div>
{:else}
{#if validationError}
<ValidationError validationError={validationError} onExpand={handleExpand} />
{/if}
<div
class="insert-selection-area after"
data-type="insert-selection-area-after"
Expand Down Expand Up @@ -423,16 +423,16 @@
</div>
{/if}
</div>
{#if validationError && (!expanded || !validationError.isChildError)}
<ValidationError validationError={validationError} onExpand={handleExpand} />
{/if}
{#if expanded}
<div
class="insert-selection-area inside"
data-type="insert-selection-area-inside"
on:mousedown={handleInsertInside}
></div>
{:else}
{#if validationError}
<ValidationError validationError={validationError} onExpand={handleExpand} />
{/if}
{#if !root}
<div
class="insert-selection-area after"
Expand Down
Loading

0 comments on commit b206f10

Please sign in to comment.