diff --git a/packages/test-app/src/App.tsx b/packages/test-app/src/App.tsx index c96866f..eec0645 100644 --- a/packages/test-app/src/App.tsx +++ b/packages/test-app/src/App.tsx @@ -13,14 +13,18 @@ import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; import { worker } from "./rpc.ts"; -import { useCallback, useState } from "react"; +// import * as worker from "../../ui/src/OpenApiEditorWorker.ts"; +import { useCallback, useRef, useState } from "react"; import { + Button, Flex, + FlexItem, PageSection, Switch, TextArea, Title, } from "@patternfly/react-core"; +import { fromPromise } from "xstate"; self.MonacoEnvironment = { getWorker(_, label) { @@ -42,21 +46,57 @@ self.MonacoEnvironment = { loader.config({ monaco }); function App() { - const [state, send] = useMachine(appMachine, { input: { spec: undefined } }); + const [state, send] = useMachine( + appMachine.provide({ + actors: { + parseSpec: fromPromise(async ({ input }) => { + if (input.spec) { + await worker.parseOasSchema(input.spec); + return true; + } + return false; + }), + }, + }), + { input: { spec: undefined } } + ); const [captureChanges, setCaptureChanges] = useState(true); const [output, setOutput] = useState(""); + const asYamlRef = useRef<(() => Promise) | null>(null); + const onDocumentChange: OpenApiEditorProps["onDocumentChange"] = useCallback( ({ asJson, asYaml }) => { console.log("DOCUMENT_CHANGE"); // this should be run in a debounce if (captureChanges) { - asYaml().then((v) => setOutput(v.substring(0, 1000))); + asYaml().then((v) => { + setOutput(v.substring(0, 1000)); + }); } + asYamlRef.current = asYaml; }, - [captureChanges] + [] ); + const onSaveClick = useCallback(async () => { + if (asYamlRef.current) { + const value = await asYamlRef.current(); + setOutput(value); + send({ + type: "SPEC", + content: ` + { + "openapi": "3.0.3", + "info": { + "title": "Sample API" + } + } + `, + }); + } + }, [send]); + switch (true) { case state.matches("idle"): return ( @@ -97,6 +137,9 @@ function App() { + + + <Switch isChecked={captureChanges} diff --git a/packages/test-app/src/AppMachine.ts b/packages/test-app/src/AppMachine.ts index 2037606..03cbe1d 100644 --- a/packages/test-app/src/AppMachine.ts +++ b/packages/test-app/src/AppMachine.ts @@ -45,7 +45,7 @@ export const appMachine = setup({ }, actors: { checkPreviousSession: fromPromise<Context, Input>(({ input }) => - Input(input), + Input(input) ), parseSpec: fromPromise<boolean, Input>(({ input }) => ParseSpec(input)), }, @@ -78,12 +78,6 @@ export const appMachine = setup({ previousWorkAvailable: {}, invalidSpec: {}, }, - on: { - SPEC: { - target: "parsing", - actions: assign(({ event }) => ({ spec: event.content })), - }, - }, }, parsing: { initial: "parsing", @@ -114,4 +108,10 @@ export const appMachine = setup({ }, parsed: {}, }, + on: { + SPEC: { + target: ".parsing", + actions: assign(({ event }) => ({ spec: event.content })), + }, + }, }); diff --git a/packages/ui/package.json b/packages/ui/package.json index bcb87bc..6cde761 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,7 +14,6 @@ "lib" ], "peerDependencies": { - "@apicurio/data-models": "^1.1.28", "@patternfly/patternfly": "^6.0.0", "@patternfly/react-code-editor": "^6.0.0", "@patternfly/react-core": "^6.0.0", @@ -24,11 +23,9 @@ "monaco-editor": "*", "monaco-yaml": "*", "react": "^18.3.1", - "react-dom": "^18.3.1", - "xstate": "^5.18.2" + "react-dom": "^18.3.1" }, "devDependencies": { - "@apicurio/data-models": "^1.1.28", "@eslint/js": "^9.8.0", "@patternfly/patternfly": "^6.0.0", "@patternfly/react-code-editor": "^6.0.0", @@ -52,14 +49,15 @@ "rimraf": "^6.0.0", "typescript": "^5.5.3", "typescript-eslint": "^8.0.0", - "vite": "^5.4.0", - "xstate": "^5.18.2" + "vite": "^5.4.0" }, "packageManager": "yarn@4.4.0", "dependencies": { + "@apicurio/data-models": "^1.1.28", "lodash": "^4.17.21", "react-markdown": "^9.0.1", "remark-gfm": "^4.0.0", + "xstate": "^5.18.2", "yaml": "^2.6.0" } } diff --git a/packages/ui/src/OpenApiEditor.tsx b/packages/ui/src/OpenApiEditor.tsx index 47a3e5b..b1fd0b6 100644 --- a/packages/ui/src/OpenApiEditor.tsx +++ b/packages/ui/src/OpenApiEditor.tsx @@ -78,6 +78,9 @@ export type OpenApiEditorProps = { asYaml: () => Promise<string>; asJson: () => Promise<string>; }) => void; + enableViewer?: boolean; + enableDesigner?: boolean; + enableSource?: boolean; }; export const OpenApiEditorMachineContext = @@ -101,6 +104,9 @@ export function OpenApiEditor({ undoChange, redoChange, onDocumentChange, + enableViewer = true, + enableDesigner = true, + enableSource = true, }: OpenApiEditorProps) { const containerRef = useRef<HTMLDivElement | null>(null); @@ -230,13 +236,25 @@ export function OpenApiEditor({ ref={containerRef} id={"editor-container"} > - <Editor /> + <Editor + enableViewer={enableViewer} + enableDesigner={enableDesigner} + enableSource={enableSource} + /> </div> </OpenApiEditorMachineContext.Provider> ); } -function Editor() { +function Editor({ + enableViewer, + enableDesigner, + enableSource, +}: { + enableViewer: boolean; + enableDesigner?: boolean; + enableSource: boolean; +}) { const { isSavingSlowly, showNavigation, @@ -286,6 +304,9 @@ function Editor() { label={label} view={view} canGoBack={selectedNode.type !== "root"} + enableViewer={enableViewer} + enableDesigner={enableDesigner} + enableSource={enableSource} /> <Drawer isExpanded={showNavigation} diff --git a/packages/ui/src/OpenApiEditorWorker.ts b/packages/ui/src/OpenApiEditorWorker.ts index 9240588..cf2f496 100644 --- a/packages/ui/src/OpenApiEditorWorker.ts +++ b/packages/ui/src/OpenApiEditorWorker.ts @@ -159,7 +159,9 @@ function getNavigationDataTypes(filter = ""): NavigationDataType[] { }); } -export function getDocumentNavigation(filter = ""): DocumentNavigation { +export async function getDocumentNavigation( + filter = "" +): Promise<DocumentNavigation> { return { paths: getNavigationPaths(filter), responses: getNavigationResponses(filter), @@ -190,7 +192,7 @@ function securitySchemes(): DM.SecurityScheme[] { } } -export function parseOasSchema(schema: string) { +export async function parseOasSchema(schema: string) { try { document = DM.Library.readDocumentFromJSONString(schema) as DM.OasDocument; otEngine = new DM.OtEngine(document); @@ -202,7 +204,7 @@ export function parseOasSchema(schema: string) { } } -export function getPathSnapshot(node: NodePath): DocumentPath { +export async function getPathSnapshot(node: NodePath): Promise<DocumentPath> { const path = resolveNode(node.nodePath); if (document.is3xDocument()) { @@ -233,7 +235,9 @@ export function getPathSnapshot(node: NodePath): DocumentPath { } } -export function getDataTypeSnapshot(node: NodeDataType): DocumentDataType { +export async function getDataTypeSnapshot( + node: NodeDataType +): Promise<DocumentDataType> { const schema = resolveNode(node.nodePath) as DM.OasSchema; const description = schema.description; @@ -292,7 +296,9 @@ export function getDataTypeSnapshot(node: NodeDataType): DocumentDataType { }; } -export function getResponseSnapshot(node: NodeResponse): DocumentResponse { +export async function getResponseSnapshot( + node: NodeResponse +): Promise<DocumentResponse> { const response = resolveNode(node.nodePath); if (document.is3xDocument()) { @@ -308,7 +314,7 @@ export function getResponseSnapshot(node: NodeResponse): DocumentResponse { } } -export function getDocumentRootSnapshot(): DocumentRoot { +export async function getDocumentRootSnapshot(): Promise<DocumentRoot> { console.log("getDocumentRootSnapshot"); return { title: document.info.title, @@ -400,7 +406,7 @@ export async function getEditorState(filter: string): Promise<EditorModel> { return { documentTitle: document.info.title, - navigation: getDocumentNavigation(filter), + navigation: await getDocumentNavigation(filter), canUndo, canRedo, validationProblems: validationProblems.map((v): Validation => { diff --git a/packages/ui/src/components/EditorToolbar.tsx b/packages/ui/src/components/EditorToolbar.tsx index e424ec7..aff32b2 100644 --- a/packages/ui/src/components/EditorToolbar.tsx +++ b/packages/ui/src/components/EditorToolbar.tsx @@ -31,6 +31,9 @@ export type EditorToolbarProps = { canGoBack: boolean; onBack: () => void; onViewChange: (view: EditorToolbarView) => void; + enableViewer: boolean; + enableDesigner?: boolean; + enableSource: boolean; }; export function EditorToolbar({ title, @@ -39,6 +42,9 @@ export function EditorToolbar({ canGoBack, onBack, onViewChange, + enableViewer, + enableDesigner, + enableSource, }: EditorToolbarProps) { const { low, medium, high } = OpenApiEditorMachineContext.useSelector( ({ context }) => { @@ -113,7 +119,7 @@ export function EditorToolbar({ </> )} </ToolbarGroup> - <ToolbarGroup align={{ lg: "alignEnd" }} style={{ maxWidth: "30%" }}> + <ToolbarGroup align={{ lg: "alignEnd" }} style={{ maxWidth: "50%" }}> <ToolbarItem> <UndoRedo /> </ToolbarItem> @@ -122,30 +128,36 @@ export function EditorToolbar({ <ToolbarItem variant={"separator"} /> <ToolbarItem> <ToggleGroup aria-label="View selector"> - <ToggleGroupItem - text="Visualize" - buttonId="toggle-visualize" - isSelected={view === "visualize"} - onChange={() => { - onViewChange("visualize"); - }} - /> - <ToggleGroupItem - text="Design" - buttonId="toggle-designer" - isSelected={view === "design"} - onChange={() => { - onViewChange("design"); - }} - /> - <ToggleGroupItem - text="Source" - buttonId="toggle-yaml" - isSelected={view === "code"} - onChange={() => { - onViewChange("code"); - }} - /> + {enableViewer && ( + <ToggleGroupItem + text="Visualize" + buttonId="toggle-visualize" + isSelected={view === "visualize"} + onChange={() => { + onViewChange("visualize"); + }} + /> + )} + {enableDesigner && ( + <ToggleGroupItem + text="Design" + buttonId="toggle-designer" + isSelected={view === "design"} + onChange={() => { + onViewChange("design"); + }} + /> + )} + {enableSource && ( + <ToggleGroupItem + text="Source" + buttonId="toggle-yaml" + isSelected={view === "code"} + onChange={() => { + onViewChange("code"); + }} + /> + )} </ToggleGroup> </ToolbarItem> </> diff --git a/packages/ui/src/components/NodeHeader.tsx b/packages/ui/src/components/NodeHeader.tsx index 34f41ed..0f0f938 100644 --- a/packages/ui/src/components/NodeHeader.tsx +++ b/packages/ui/src/components/NodeHeader.tsx @@ -9,6 +9,9 @@ export function NodeHeader({ label, view, canGoBack, + enableViewer, + enableDesigner, + enableSource, }: { title?: ReactNode; label?: ReactNode; canGoBack: boolean } & Omit< EditorToolbarProps, "onViewChange" | "onBack" @@ -20,6 +23,9 @@ export function NodeHeader({ label={label} title={title ?? <Skeleton className={classes.skeleton} />} view={view} + enableViewer={enableViewer} + enableDesigner={enableDesigner} + enableSource={enableSource} onViewChange={(view) => { switch (view) { case "visualize":