Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,9 @@ detailed, step-by-step upgrade instructions with before/after code examples.
hamburger menu. Set to `true` to render the current user's initials instead or provide a function
to render a custom element for the user.
* Added `AggregationContext` as an additional argument to `CubeField.canAggregateFn`.
* Added `ajvSchema` and `ajvOptions` configs to JsonInputProps.
* `ajvSchema` - Used to validate the input JSON
* `ajvOptions` - Options to be passed to Ajv constructor (JSON schema validator)
* Added `filterMatchMode` option to `ColChooserModel`, allowing customizing match to `start`,
`startWord`, or `any`.
* Added support for reconnecting a `View` to its associated `Cube`.
Expand Down Expand Up @@ -613,6 +616,7 @@ detailed, step-by-step upgrade instructions with before/after code examples.
* @codemirror/state `6.6.0`
* @codemirror/view `6.43.0`
* @uiw/codemirror-theme-github `4.25.10`
* ajv `8.17.1`

## 79.0.0 - 2026-01-05

Expand Down
111 changes: 102 additions & 9 deletions desktop/cmp/input/JsonInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,24 @@
import {hoistCmp} from '@xh/hoist/core';
import '@xh/hoist/desktop/register';
import {fmtJson} from '@xh/hoist/format';
import Ajv, {Options, SchemaObject, ValidateFunction} from 'ajv';
import {codeInput, CodeInputProps} from './CodeInput';
import {jsonlint} from './impl/jsonlint.js';

export type JsonInputProps = CodeInputProps;
export interface JsonInputProps extends CodeInputProps {
/**
* JSON Schema object used to validate the input JSON. Accepts any valid JSON Schema keywords
* supported by AJV, such as `type`, `properties`, `required`, and `additionalProperties`.
* @see https://ajv.js.org/json-schema.html
*/
ajvSchema?: SchemaObject;

/**
* Configuration object with any properties supported by the AJV API.
* @sees https://ajv.js.org/options.html

Check warning on line 24 in desktop/cmp/input/JsonInput.ts

View workflow job for this annotation

GitHub Actions / lint

tsdoc-undefined-tag: The TSDoc tag "@sees" is not defined in this configuration
*/
ajvOptions?: Options;
}

/**
* Code-editor style input for editing and validating JSON, powered by CodeMirror.
Expand All @@ -19,24 +33,52 @@
displayName: 'JsonInput',
className: 'xh-json-input',
render(props, ref) {
const {ajvSchema, ajvOptions, ...rest} = props;

return codeInput({
linter,
linter: jsonLinterWrapper(ajvSchema, ajvOptions),
formatter: fmtJson,
language: 'json',
...props,
...rest,
ref
});
}
});
(JsonInput as any).hasLayoutSupport = true;

//----------------------
// Implementation
// JSON Linter helper
//----------------------
function linter(text: string) {
const annotations: any[] = [];
if (!text) return annotations;
function jsonLinterWrapper(ajvSchema?: SchemaObject, ajvOptions?: Options) {
// No schema → only use JSONLint
if (!ajvSchema) {
return (text: string) => {
const annotations: any[] = [];
if (!text) return annotations;
runJsonLint(text, annotations);
return annotations;
};
}

const ajv = new Ajv(ajvOptions),
validate = ajv.compile(ajvSchema);

return (text: string) => {
const annotations: any[] = [];

if (!text.trim()) return annotations;

runJsonLint(text, annotations);
if (annotations.length) return annotations;

runAjvValidation(text, validate, annotations);

return annotations;
};
}

/** Run JSONLint and append errors to annotations */
function runJsonLint(text: string, annotations: any[]) {
jsonlint.parseError = (message, hash) => {
const {first_line, first_column, last_line, last_column} = hash.loc;
annotations.push({
Expand All @@ -50,11 +92,62 @@
try {
jsonlint.parse(text);
} catch (ignored) {}
}

/** Run AJV schema validation and append errors to annotations */
function runAjvValidation(text: string, validate: ValidateFunction, annotations: any[]) {
let data: any;
try {
data = JSON.parse(text);
} catch (ignored) {
return;
}

const valid = validate(data);
if (valid || !validate.errors) return;

validate.errors.forEach(err => {
const {from, to} = getErrorPosition(err, text),
message = formatAjvMessage(err);

annotations.push({from, to, message, severity: 'error'});
});
}

/** Determine text positions for AJV error highlighting */
function getErrorPosition(err: any, text: string): {from: number; to: number} {
let from = 0,
to = 0,
key: string;

return annotations;
if (err.keyword === 'additionalProperties' && err.params?.additionalProperty) {
key = err.params.additionalProperty;
} else {
const parts = (err.instancePath || '').split('/').filter(Boolean);
key = parts[parts.length - 1];
}

if (key) {
const idx = text.indexOf(`"${key}"`);
if (idx >= 0) {
from = idx;
to = idx + key.length + 2;
}
}

return {from, to};
}

/** Format AJV error messages nicely */
function formatAjvMessage(err: any): string {
const path = err.instancePath || '(root)';
if (err.keyword === 'additionalProperties' && err.params?.additionalProperty) {
return `Unexpected property "${err.params.additionalProperty}"`;
}
return `${path} ${err.message}`;
}

/** Convert line/col (1-based line, 0-based col) to absolute string index. */
/** Convert line/col to string index */
function indexFromLineCol(text: string, line: number, col: number): number {
const lines = text.split('\n');
let idx = 0;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@modelcontextprotocol/sdk": "^1.26.0",
"@seznam/compose-react-refs": "~1.0.5",
"@uiw/codemirror-theme-github": "~4.25.10",
"ajv": "~8.17.1",
"classnames": "~2.5.1",
"clipboard-copy": "~4.0.1",
"commander": "^14.0.3",
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3311,6 +3311,16 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.17.1, ajv@^8.9.0:
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"

ajv@~8.17.1:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
dependencies:
fast-deep-equal "^3.1.3"
fast-uri "^3.0.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"

ajv@~8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc"
Expand Down
Loading