diff --git a/.changeset/tame-comics-buy.md b/.changeset/tame-comics-buy.md
new file mode 100644
index 000000000..c5ea06a00
--- /dev/null
+++ b/.changeset/tame-comics-buy.md
@@ -0,0 +1,5 @@
+---
+'@theguild/editor': minor
+---
+
+feat: :tada: add `` component
diff --git a/packages/components/package.json b/packages/components/package.json
index 8468acdd2..36c1f0f78 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -28,5 +28,9 @@
},
"publishConfig": {
"access": "public"
+ },
+ "devDependencies": {
+ "@types/dedent": "^0.7.0",
+ "dedent": "0.7.0"
}
}
diff --git a/packages/components/src/components/SchemaType.tsx b/packages/components/src/components/SchemaType.tsx
index d7ab34f66..bc0b23b65 100644
--- a/packages/components/src/components/SchemaType.tsx
+++ b/packages/components/src/components/SchemaType.tsx
@@ -1,4 +1,5 @@
-import React from 'react';
+import React, { FC, useState } from 'react';
+import { buildSchema } from 'graphql';
import { ISchemaPageProps, IEditorProps } from '../types/components';
import { useThemeContext } from '../helpers/theme';
@@ -16,24 +17,24 @@ import {
Frameworks,
} from './SchemaTypes.styles';
import { Tag, TagsContainer } from './Tag';
-import { SchemaEditor } from '../../../monaco-graphql-editor/src/editor/SchemaEditor';
+import { SchemaEditor, ExecutableDocumentEditor } from '../../../monaco-graphql-editor/src';
-const FrameworkList = ({ options }: { options: any }): JSX.Element => {
+const FrameworkList = ({ options }: { options: any[] }): JSX.Element => {
const list = options.reduce((prev: string, curr: string) => [
prev,
- ,
+ ,
curr,
]);
return {list};
};
-const Editor: React.FC = ({
+const Editor: FC> = ({
title,
- frameworks,
- schema,
+ frameworks = [],
icon,
image,
+ children,
}) => (
@@ -41,26 +42,27 @@ const Editor: React.FC = ({
{image && }
{title && {title}
}
- {frameworks && frameworks.length > 0 && (
-
- )}
+ {frameworks.length > 0 && }
-
+ {children}
);
-export const SchemaPage: React.FC = ({
+export const SchemaPage: FC = ({
schemaName,
- tags,
+ tags = [],
editorData,
}) => {
const { isDarkTheme } = useThemeContext();
const marketplaceAssets = marketplaceThemedAssets(isDarkTheme || false);
+ const [schemaObj, setSchemaObj] = useState(() =>
+ buildSchema(editorData[0].schema!)
+ );
return (
@@ -68,9 +70,9 @@ export const SchemaPage: React.FC = ({
{schemaName}
- {tags &&
- tags.length > 0 &&
- tags.map((tagName) => {tagName})}
+ {tags.map((tagName) => (
+ {tagName}
+ ))}
@@ -83,15 +85,30 @@ export const SchemaPage: React.FC = ({
- {(editorData as Array).map((data) => (
+
+ {
+ setSchemaObj(newSchemaObject);
+ }}
+ />
+
+
+
+
+
+
+ {editorData.slice(2).map((data) => (
+ >
+
+
))}
diff --git a/packages/components/src/helpers/dummy.tsx b/packages/components/src/helpers/dummy.tsx
index 38d94d5af..f977f3ed7 100644
--- a/packages/components/src/helpers/dummy.tsx
+++ b/packages/components/src/helpers/dummy.tsx
@@ -1,3 +1,4 @@
+import dedent from 'dedent';
import featureListImage1 from '../static/dummy/envelop/features-pluggable.png';
import featureListImage2 from '../static/dummy/envelop/features-performant.png';
import featureListImage3 from '../static/dummy/envelop/features-modern.png';
@@ -24,8 +25,8 @@ export const dummyFeatureList = {
title: 'Learn more',
target: '_blank',
rel: 'noopener norefereer',
- href: 'https://github.com/the-guild-org'
- }
+ href: 'https://github.com/the-guild-org',
+ },
},
{
image: {
@@ -127,7 +128,7 @@ export const dummyHeroGradient = {
href: '#',
},
version: '1.0.7',
- colors: ['#FF34AE', '#1CC8EE'],
+ colors: ['#ff34ae', '#1cc8ee'],
image: {
src: heroGradientImage,
alt: 'Illustration',
@@ -201,7 +202,7 @@ export const dummyCardsColorful = {
title: 'Learn more',
href: '#',
},
- color: '#3547E5',
+ color: '#3547e5',
},
{
title: 'Clean up your code!',
@@ -211,7 +212,7 @@ export const dummyCardsColorful = {
title: 'Learn more',
href: '#',
},
- color: '#0B0D11',
+ color: '#0b0d11',
},
],
};
@@ -563,7 +564,7 @@ export const dummyMarketplaceSearch = {
'relay',
'jsdoc',
'plugin',
- 'preset'
+ 'preset',
],
placeholder: 'Search...',
primaryList: {
@@ -586,26 +587,47 @@ export const dummyMarketplaceSearch = {
},
};
-const dummySchema = `type Query {
- ping: Boolean
- me: User!
-}
+const dummySchema = dedent(/* GraphQL */ `
+ type Query {
+ ping: Boolean
+ me: User!
+ }
+
+ " represents a valid email "
+ scalar Email
-" represents a valid email "
-scalar Email
+ """
+ Represents a simple user
+ """
+ type User {
+ id: ID!
+ email: Email!
+ profile: Profile!
+ }
-""" Represents a simple user """
-type User {
- id: ID!
- email: Email!
- profile: Profile!
-}
+ type Profile {
+ name: String
+ age: Int
+ }
+`);
-type Profile {
- name: String
- age: Int
-}
-`;
+const dummyOperations = dedent(/* GraphQL */ `
+ query Me {
+ me {
+ id
+ profile {
+ name
+ }
+ }
+ ping
+ }
+
+ fragment UserFields on User {
+ profile {
+ name
+ }
+ }
+`);
export const dummySchemaPage = {
schemaName: 'Schema Type 1',
@@ -620,7 +642,7 @@ export const dummySchemaPage = {
{
title: 'operation.graphql',
frameworks: [],
- schema: dummySchema,
+ operations: dummyOperations,
image: marketplaceListImage,
},
{
diff --git a/packages/components/src/types/components.ts b/packages/components/src/types/components.ts
index 90fea5cd3..7bf0349ae 100644
--- a/packages/components/src/types/components.ts
+++ b/packages/components/src/types/components.ts
@@ -75,6 +75,7 @@ interface IHeaderModalRestProps {
categoryTitleProps?: React.ComponentProps<'h3'>;
modalProps?: IModalRestProps;
}
+
export interface IHeaderModalProps extends IHeaderModalRestProps {
title: string | React.ReactNode;
modalOpen: boolean;
@@ -118,6 +119,7 @@ interface IModalRestProps {
headerLinkProps?: React.ComponentProps<'a'>;
headerImageProps?: React.ComponentProps<'img'>;
}
+
export interface IModalProps extends IModalRestProps {
title: string | React.ReactNode;
description?: string | ILink;
@@ -155,6 +157,7 @@ export interface IFeatureListProps {
itemDescriptionProps?: React.ComponentProps<'p'>;
itemImageProps?: React.ComponentProps<'img'>;
}
+
export interface IInfoListProps {
title?: string | React.ReactNode;
items: {
@@ -170,6 +173,7 @@ export interface IInfoListProps {
itemDescriptionProps?: React.ComponentProps<'p'>;
itemLinkProps?: React.ComponentProps<'a'>;
}
+
export interface IHeroVideoProps {
title: string | React.ReactNode;
description: string | React.ReactNode;
@@ -184,6 +188,7 @@ export interface IHeroVideoProps {
linkProps?: React.ComponentProps<'a'>;
videoProps?: ReactPlayerProps;
}
+
export interface IHeroIllustrationProps {
title: string | React.ReactNode;
description: string | React.ReactNode;
@@ -198,6 +203,7 @@ export interface IHeroIllustrationProps {
linkProps?: React.ComponentProps<'a'>;
imageProps?: React.ComponentProps<'img'>;
}
+
export interface IHeroGradientProps {
title: string | React.ReactNode;
description: string | React.ReactNode;
@@ -269,6 +275,7 @@ interface IMarketplaceItemRestProps {
dateProps?: React.ComponentProps<'span'>;
linkProps?: React.ComponentProps<'a'>;
}
+
export interface IMarketplaceItemsProps extends IMarketplaceItemRestProps {
icon: string;
items: IMarketplaceItemProps[];
@@ -311,13 +318,14 @@ export interface INewsletterProps {
export interface ISchemaPageProps {
schemaName: string;
tags?: string[];
- editorData: string[] | React.ReactNode[];
+ editorData: Omit[];
}
export interface IEditorProps {
title?: string;
frameworks?: string[];
- schema: string;
+ schema?: string;
icon: string;
image?: string;
+ operations?: string
}
diff --git a/packages/monaco-graphql-editor/src/editor/ExecutableDocumentEditor.tsx b/packages/monaco-graphql-editor/src/editor/ExecutableDocumentEditor.tsx
new file mode 100644
index 000000000..b8138a8e6
--- /dev/null
+++ b/packages/monaco-graphql-editor/src/editor/ExecutableDocumentEditor.tsx
@@ -0,0 +1,193 @@
+import React, { FC, useEffect, useRef, useState } from 'react';
+import MonacoEditor, { useMonaco, EditorProps } from '@monaco-editor/react';
+import {
+ getAutocompleteSuggestions,
+ // CompletionItemKind as lsCIK,
+} from 'graphql-language-service';
+import type { GraphQLSchema } from 'graphql';
+import type * as monaco from 'monaco-editor';
+import type { IRange, CompletionItem } from 'graphql-language-service';
+import * as languages from './enums';
+import { toGraphQLPosition, toMonacoRange } from './utils';
+
+// This enum `CompletionItemKind` exist on graphql-language-service v4
+enum lsCIK {
+ Text = 1,
+ Method = 2,
+ Function = 3,
+ Constructor = 4,
+ Field = 5,
+ Variable = 6,
+ Class = 7,
+ Interface = 8,
+ Module = 9,
+ Property = 10,
+ Unit = 11,
+ Value = 12,
+ Enum = 13,
+ Keyword = 14,
+ Snippet = 15,
+ Color = 16,
+ File = 17,
+ Reference = 18,
+ Folder = 19,
+ EnumMember = 20,
+ Constant = 21,
+ Struct = 22,
+ Event = 23,
+ Operator = 24,
+ TypeParameter = 25,
+}
+
+type GraphQLWorkerCompletionItem = CompletionItem & {
+ range?: monaco.IRange;
+ command?: monaco.languages.CompletionItem['command'];
+};
+
+const toCompletionItemKind = (kind: lsCIK): languages.CompletionItemKind => {
+ const CIK = languages.CompletionItemKind;
+ const map = {
+ [lsCIK.Text]: CIK.Text,
+ [lsCIK.Method]: CIK.Method,
+ [lsCIK.Function]: CIK.Function,
+ [lsCIK.Constructor]: CIK.Constructor,
+ [lsCIK.Field]: CIK.Field,
+ [lsCIK.Variable]: CIK.Variable,
+ [lsCIK.Class]: CIK.Class,
+ [lsCIK.Interface]: CIK.Interface,
+ [lsCIK.Module]: CIK.Module,
+ [lsCIK.Property]: CIK.Property,
+ [lsCIK.Unit]: CIK.Unit,
+ [lsCIK.Value]: CIK.Value,
+ [lsCIK.Enum]: CIK.Enum,
+ [lsCIK.Keyword]: CIK.Keyword,
+ [lsCIK.Snippet]: CIK.Snippet,
+ [lsCIK.Color]: CIK.Color,
+ [lsCIK.File]: CIK.File,
+ [lsCIK.Reference]: CIK.Reference,
+ [lsCIK.Folder]: CIK.Folder,
+ [lsCIK.EnumMember]: CIK.EnumMember,
+ [lsCIK.Constant]: CIK.Constant,
+ [lsCIK.Struct]: CIK.Struct,
+ [lsCIK.Event]: CIK.Event,
+ [lsCIK.Operator]: CIK.Operator,
+ [lsCIK.TypeParameter]: CIK.TypeParameter,
+ };
+ return map[kind] || CIK.Text;
+};
+
+const toCompletion = (
+ entry: GraphQLWorkerCompletionItem
+): monaco.languages.CompletionItem => {
+ return {
+ range: entry.range as monaco.IRange,
+ kind: toCompletionItemKind(entry.kind as lsCIK),
+ label: entry.label,
+ insertText: entry.insertText ?? (entry.label as string),
+ insertTextRules: entry.insertText
+ ? languages.CompletionItemInsertTextRule.InsertAsSnippet
+ : undefined,
+ sortText: entry.sortText,
+ filterText: entry.filterText,
+ documentation: entry.documentation,
+ detail: entry.detail,
+ command: entry.command,
+ };
+};
+
+class GraphQLWorker {
+ doComplete(
+ documentModel: monaco.editor.IReadOnlyModel,
+ position: monaco.Position,
+ schema: GraphQLSchema
+ ): GraphQLWorkerCompletionItem[] {
+ const document = documentModel.getValue();
+ if (!document) {
+ console.log('no document');
+ return [];
+ }
+ if (!schema) {
+ console.log('no schema');
+ }
+ const graphQLPosition = toGraphQLPosition(position);
+ const suggestions = getAutocompleteSuggestions(
+ schema,
+ document,
+ graphQLPosition
+ );
+ return suggestions.map((suggestion) => this.toCompletion(suggestion));
+ }
+
+ toCompletion(
+ entry: CompletionItem,
+ range?: IRange
+ ): GraphQLWorkerCompletionItem {
+ return {
+ label: entry.label,
+ insertText: entry.insertText,
+ insertTextFormat: entry.insertTextFormat,
+ sortText: entry.sortText,
+ filterText: entry.filterText,
+ documentation: entry.documentation,
+ detail: entry.detail,
+ range: range ? toMonacoRange(range) : undefined,
+ kind: entry.kind,
+ command: entry.command
+ ? { ...entry.command, id: entry.command.command }
+ : undefined,
+ };
+ }
+}
+
+export const ExecutableDocumentEditor: FC<
+ { schema: GraphQLSchema } & Omit
+> = ({ schema, ...editorProps }) => {
+ const monaco = useMonaco();
+ const [completionProvider, setCompletionProvider] =
+ useState(null);
+ const editorUriRef = useRef();
+
+ useEffect(() => {
+ if (!monaco || !schema) {
+ return;
+ }
+ if (completionProvider) {
+ // dispose instance to prevent having duplicated field
+ completionProvider.dispose();
+ }
+
+ const newProvider = monaco.languages.registerCompletionItemProvider(
+ 'graphql',
+ {
+ triggerCharacters: [':', '$', '\n', ' ', '(', '@'],
+ provideCompletionItems(
+ model: monaco.editor.IReadOnlyModel,
+ position: monaco.Position
+ ): monaco.languages.CompletionList {
+ const isUriEquals = model.uri.path === editorUriRef.current!.path
+ if (!isUriEquals) {
+ return { suggestions: [] };
+ }
+ const worker = new GraphQLWorker();
+ const completionItems = worker.doComplete(model, position, schema);
+ return {
+ incomplete: true,
+ suggestions: completionItems.map((item) => toCompletion(item)),
+ };
+ },
+ }
+ );
+ setCompletionProvider(newProvider);
+ }, [monaco, schema]);
+
+ return (
+ {
+ editorUriRef.current = editor.getModel()!.uri;
+ }}
+ />
+ );
+};
diff --git a/packages/monaco-graphql-editor/src/editor/SchemaEditor.tsx b/packages/monaco-graphql-editor/src/editor/SchemaEditor.tsx
index cfa83d6d2..41be42f24 100644
--- a/packages/monaco-graphql-editor/src/editor/SchemaEditor.tsx
+++ b/packages/monaco-graphql-editor/src/editor/SchemaEditor.tsx
@@ -1,8 +1,20 @@
-import * as React from 'react';
-import MonacoEditor, { EditorProps } from '@monaco-editor/react';
-import type * as monaco from 'monaco-editor';
-import { EnrichedLanguageService } from './EnrichedLanguageService';
+import React, {
+ ForwardedRef,
+ useImperativeHandle,
+ useEffect,
+ useState,
+ forwardRef,
+ useCallback,
+} from 'react';
+import MonacoEditor, {
+ EditorProps,
+ BeforeMount,
+ OnMount,
+ OnChange,
+} from '@monaco-editor/react';
+import type { IDisposable } from 'monaco-editor';
import { GraphQLError, GraphQLSchema } from 'graphql';
+import type { EnrichedLanguageService } from './EnrichedLanguageService';
import {
SchemaEditorApi,
SchemaServicesOptions,
@@ -22,7 +34,7 @@ export type SchemaEditorProps = SchemaServicesOptions & {
function BaseSchemaEditor(
props: SchemaEditorProps,
- ref: React.ForwardedRef
+ ref: ForwardedRef
) {
const {
languageService,
@@ -32,80 +44,89 @@ function BaseSchemaEditor(
editorRef,
setSchema,
} = useSchemaServices(props);
- React.useImperativeHandle(ref, () => editorApi, [editorRef, languageService]);
+ useImperativeHandle(ref, () => editorApi, [editorRef, languageService]);
- React.useEffect(() => {
+ useEffect(() => {
if (languageService && props.onLanguageServiceReady) {
props.onLanguageServiceReady(languageService);
}
}, [languageService, props.onLanguageServiceReady]);
- const [onBlurHandler, setOnBlurSubscription] =
- React.useState();
+ const [onBlurHandler, setOnBlurSubscription] = useState();
- React.useEffect(() => {
+ useEffect(() => {
if (editorRef && props.onBlur) {
onBlurHandler?.dispose();
const subscription = editorRef.onDidBlurEditorText(() => {
- props.onBlur && props.onBlur(editorRef.getValue() || '');
+ props.onBlur?.(editorRef.getValue() || '');
});
setOnBlurSubscription(subscription);
}
}, [props.onBlur, editorRef]);
+ const handleBeforeMount = useCallback(
+ (monaco) => {
+ setMonaco(monaco);
+ props.beforeMount?.(monaco);
+ },
+ [props.beforeMount]
+ );
+
+ const handleMount = useCallback(
+ (editor, monaco) => {
+ setEditor(editor);
+ props.onMount?.(editor, monaco);
+ },
+ [props.onMount]
+ );
+
+ const handleChange = useCallback(
+ async (newValue, ev) => {
+ props.onChange?.(newValue, ev);
+ if (!newValue) {
+ return;
+ }
+
+ try {
+ const schema = await setSchema(newValue);
+ if (schema) {
+ props.onSchemaChange?.(schema, newValue);
+ }
+ } catch (e) {
+ if (!props.onSchemaError) {
+ return;
+ }
+ const error =
+ e instanceof GraphQLError
+ ? e
+ : new GraphQLError(
+ (e as Error).message,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ e as Error
+ );
+ props.onSchemaError([error], newValue, languageService);
+ }
+ },
+ [props.onChange, props.onSchemaChange, props.onSchemaError]
+ );
+
return (
{
- setMonaco(monaco);
- props.beforeMount && props.beforeMount(monaco);
- }}
- onMount={(editor, monaco) => {
- setEditor(editor);
- props.onMount && props.onMount(editor, monaco);
- }}
- onChange={(newValue, ev) => {
- props.onChange && props.onChange(newValue, ev);
-
- if (newValue) {
- setSchema(newValue)
- .then((schema) => {
- if (schema) {
- props.onSchemaChange && props.onSchemaChange(schema, newValue);
- }
- })
- .catch((e: Error | GraphQLError) => {
- if (props.onSchemaError) {
- if (e instanceof GraphQLError) {
- props.onSchemaError([e], newValue, languageService);
- } else {
- props.onSchemaError(
- [
- new GraphQLError(
- e.message,
- undefined,
- undefined,
- undefined,
- undefined,
- e
- ),
- ],
- newValue,
- languageService
- );
- }
- }
- });
- }
- }}
- options={{ glyphMargin: true, ...(props.options || {}) }}
+ beforeMount={handleBeforeMount}
+ onMount={handleMount}
+ onChange={handleChange}
+ options={{ glyphMargin: true, ...props.options }}
language="graphql"
defaultValue={props.defaultValue || props.schema}
/>
);
}
-export const SchemaEditor = React.forwardRef(BaseSchemaEditor);
+export const SchemaEditor = forwardRef(BaseSchemaEditor);
diff --git a/packages/monaco-graphql-editor/src/editor/enums.ts b/packages/monaco-graphql-editor/src/editor/enums.ts
new file mode 100644
index 000000000..8824dca45
--- /dev/null
+++ b/packages/monaco-graphql-editor/src/editor/enums.ts
@@ -0,0 +1,52 @@
+/*
+ * `monaco-editor` throws an error while importing languages enums - Error: PostCSS plugin autoprefixer requires PostCSS 8
+ *
+ * import { languages } from 'monaco-editor'
+ * languages.CompletionItemKind
+ * languages.CompletionItemInsertTextRule
+ *
+ * So I hardcoded them inside this file
+ */
+
+export enum CompletionItemKind {
+ Method = 0,
+ Function = 1,
+ Constructor = 2,
+ Field = 3,
+ Variable = 4,
+ Class = 5,
+ Struct = 6,
+ Interface = 7,
+ Module = 8,
+ Property = 9,
+ Event = 10,
+ Operator = 11,
+ Unit = 12,
+ Value = 13,
+ Constant = 14,
+ Enum = 15,
+ EnumMember = 16,
+ Keyword = 17,
+ Text = 18,
+ Color = 19,
+ File = 20,
+ Reference = 21,
+ Customcolor = 22,
+ Folder = 23,
+ TypeParameter = 24,
+ User = 25,
+ Issue = 26,
+ Snippet = 27,
+}
+
+export enum CompletionItemInsertTextRule {
+ /**
+ * Adjust whitespace/indentation of multiline insert texts to
+ * match the current line indentation.
+ */
+ KeepWhitespace = 1,
+ /**
+ * `insertText` is a snippet.
+ */
+ InsertAsSnippet = 4,
+}
diff --git a/packages/monaco-graphql-editor/src/index.tsx b/packages/monaco-graphql-editor/src/index.tsx
index e194c3e69..a5e7e46f0 100644
--- a/packages/monaco-graphql-editor/src/index.tsx
+++ b/packages/monaco-graphql-editor/src/index.tsx
@@ -1,3 +1,4 @@
export * from './editor/SchemaEditor';
+export * from './editor/ExecutableDocumentEditor';
export * from './editor/utils';
export * from './editor/EnrichedLanguageService';
diff --git a/yarn.lock b/yarn.lock
index ec1488948..3dd685fe1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2994,6 +2994,11 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+"@types/dedent@^0.7.0":
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/@types/dedent/-/dedent-0.7.0.tgz#155f339ca404e6dd90b9ce46a3f78fd69ca9b050"
+ integrity sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==
+
"@types/estree@0.0.39":
version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
@@ -5472,7 +5477,7 @@ decode-uri-component@^0.2.0:
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
-dedent@^0.7.0:
+dedent@0.7.0, dedent@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=