Skip to content

Commit

Permalink
Improved variable editor using codemirror-json-schema
Browse files Browse the repository at this point in the history
  • Loading branch information
imolorhe committed Dec 7, 2023
1 parent a8399bd commit 1a437d0
Show file tree
Hide file tree
Showing 7 changed files with 449 additions and 174 deletions.
2 changes: 1 addition & 1 deletion packages/altair-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"cm6-graphql": "0.0.12",
"codemirror": "5.61.0",
"codemirror-graphql": "1.0.2",
"codemirror-json-schema": "^0.4.4",
"codemirror-json-schema": "^0.6.0",
"comlink": "4.3.0",
"cookie-parser": "1.4.5",
"core-js": "3.6.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`utils vttToJsonSchema converts variabltToType to a JSONSchema7 object 1`] = `
{
"properties": {
"myvar": {
"description": undefined,
"properties": {
"age": {
"anyOf": [
{
"default": undefined,
"description": "The \`Int\` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",
"type": "integer",
},
{
"type": "null",
},
],
},
"allergies": {
"default": [],
"description": "The \`String\` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"items": {
"anyOf": [
{
"default": undefined,
"description": "The \`String\` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"type": "string",
},
{
"type": "null",
},
],
},
"type": "array",
},
"bestFriend": {
"description": undefined,
"properties": {
"age": {
"anyOf": [
{
"default": 0,
"description": "The \`Int\` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",
"type": "integer",
},
{
"type": "null",
},
],
},
"name": {
"anyOf": [
{
"default": undefined,
"description": "The \`String\` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"type": "string",
},
{
"type": "null",
},
],
},
},
"type": "object",
},
"customScalar": {
"anyOf": [
{
"description": undefined,
"type": "string",
},
{
"type": "null",
},
],
},
"favoriteColor": {
"anyOf": [
{
"description": "The color of blood",
"enum": [
"red",
],
},
{
"description": "The color of grass",
"enum": [
"green",
],
},
{
"description": "The color of the sky",
"enum": [
"blue",
],
},
],
"default": undefined,
"description": undefined,
"type": "string",
},
"friends": {
"anyOf": [
{
"default": [],
"description": "The \`String\` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"items": {
"anyOf": [
{
"default": undefined,
"description": "The \`String\` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"type": "string",
},
{
"type": "null",
},
],
},
"type": "array",
},
{
"type": "null",
},
],
},
"height": {
"anyOf": [
{
"default": 1.75,
"description": "The \`Float\` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",
"type": "number",
},
{
"type": "null",
},
],
},
"id": {
"default": undefined,
"description": "The \`ID\` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as \`"4"\`) or integer (such as \`4\`) input value will be accepted as an ID.",
"type": "string",
},
"isHuman": {
"anyOf": [
{
"default": undefined,
"description": "The \`Boolean\` scalar type represents \`true\` or \`false\`.",
"type": "boolean",
},
{
"type": "null",
},
],
},
"location": {
"default": "Earth",
"description": "The \`String\` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"type": "string",
},
"name": {
"anyOf": [
{
"default": undefined,
"description": "The \`String\` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"type": "string",
},
{
"type": "null",
},
],
},
},
"type": "object",
},
},
"type": "object",
}
`;
Original file line number Diff line number Diff line change
@@ -1,159 +1,11 @@
import {
Completion,
CompletionContext,
CompletionResult,
} from '@codemirror/autocomplete';
import { json, jsonLanguage } from '@codemirror/lang-json';
import { syntaxTree } from '@codemirror/language';
import { EditorState, StateEffect, StateField } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { IDictionary } from 'altair-graphql-core/build/types/shared';
import { GraphQLInputObjectType, GraphQLInputType } from 'graphql';

const variableToTypeEffect =
StateEffect.define<IDictionary<GraphQLInputType> | undefined>();
const variableToTypeStateField = StateField.define<
IDictionary<GraphQLInputType> | undefined
>({
create() {
return undefined;
},
update(variableToType, tr) {
for (const e of tr.effects) {
if (e.is(variableToTypeEffect)) {
return e.value;
}
}

return variableToType;
},
});
export const updateVariableToType = (
view: EditorView,
variableToType?: IDictionary
) => {
view.dispatch({
effects: variableToTypeEffect.of(variableToType),
});
};
export const getVariableToType = (state: EditorState) => {
return state.field(variableToTypeStateField);
};
import { jsonSchema } from 'codemirror-json-schema';

export const gqlVariables = () => {
return [
json(),
jsonLanguage.data.of({
autocomplete: (ctx: CompletionContext): CompletionResult | null => {
const nodeBefore = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
const variableToType = getVariableToType(ctx.state);
let curNode = nodeBefore;
const nodeNames = [{ type: curNode.name, name: '' }];
while (curNode.parent) {
let propertyName = '';
const propertyNameNode = curNode.parent.getChild('PropertyName');
if (propertyNameNode) {
propertyName = ctx.state.doc.sliceString(
propertyNameNode.from,
propertyNameNode.to
);
}
nodeNames.push({
type: curNode.parent.name,
// trim quotes around string, since JSON property name is always quoted
name: propertyName.replace(/(^['"]|['"]$)/g, ''),
});
curNode = curNode.parent;
}

if (nodeNames[0]?.type === 'JsonText') {
if (ctx.explicit) {
return {
from: ctx.pos,
to: ctx.pos,
options: [
{
label: '{',
},
],
};
}
}

if (!variableToType) {
return null;
}

// const curField = '';
// const curType = undefined;
let dataSource = variableToType;
nodeNames.reverse().forEach((nodeName) => {
switch (nodeName.type) {
case 'JsonText':
dataSource = variableToType;
return;
case 'Object':
return;
case 'Property': {
const propName = nodeName.name;
if (!propName) {
return;
}
const curType = dataSource[propName];
if (!curType) {
dataSource = {};
return;
}

dataSource = typeToVTT(curType);
return;
}
}
});

// TODO: Top level object?
// TODO: Handle nested types
// TODO: Refactor to a "getHints" function
if (nodeBefore.name === 'Object') {
const variableNames = Object.keys(dataSource);
return {
from: ctx.pos,
options: variableNames.map((name): Completion => {
return {
label: `"${name}": `,
detail: dataSource[name]?.toString(),
};
}),
};
}
if (nodeBefore.name === 'PropertyName') {
const variableNames = Object.keys(dataSource);
return {
from: ctx.pos,
options: variableNames.map((name): Completion => {
return {
label: name,
detail: dataSource[name]?.toString(),
};
}),
};
}

return null;
},
// start with an empty schema
jsonSchema({
type: 'object',
properties: {},
}),
variableToTypeStateField,
];
};

const typeToVTT = (type: GraphQLInputType) => {
// TODO: Handle other type instances
if (type instanceof GraphQLInputObjectType) {
return Object.entries(type.getFields()).reduce(
(acc, [k, v]) => ({ ...acc, [k]: v.type }),
{} as IDictionary<GraphQLInputType>
);
}

return {};
};
Loading

0 comments on commit 1a437d0

Please sign in to comment.