diff --git a/packages/ui/src/OpenApiEditor.tsx b/packages/ui/src/OpenApiEditor.tsx index f7c42c9..ab14d06 100644 --- a/packages/ui/src/OpenApiEditor.tsx +++ b/packages/ui/src/OpenApiEditor.tsx @@ -263,7 +263,7 @@ function Editor() { switch (selectedNode.type) { case "validation": case "root": - return undefined; + return ; case "path": return ; case "datatype": @@ -296,7 +296,8 @@ function Editor() { switch (true) { case selectedNode.type === "validation": return ; - case view === "designer": + case view === "design": + case view === "visualize": switch (selectedNode.type) { case "root": return actorRef ? ( diff --git a/packages/ui/src/OpenApiEditorMachine.ts b/packages/ui/src/OpenApiEditorMachine.ts index bb9dbd0..2acdce1 100644 --- a/packages/ui/src/OpenApiEditorMachine.ts +++ b/packages/ui/src/OpenApiEditorMachine.ts @@ -17,11 +17,12 @@ import { CodeEditorMachine } from "./codeEditor/CodeEditorMachine.ts"; import { PathDesignerMachine } from "./pathDesigner/PathDesignerMachine.ts"; import { DataTypeDesignerMachine } from "./dataTypeDesigner/DataTypeDesignerMachine.ts"; import { ResponseDesignerMachine } from "./responseDesigner/ResponseDesignerMachine.ts"; +import { EditorToolbarView } from "./components/EditorToolbar.tsx"; type Context = EditorModel & { navigationFilter: string; selectedNode: SelectedNode | { type: "validation" }; - view: "designer" | "code"; + view: Exclude; actorRef?: | ActorRefFrom | ActorRefFrom @@ -41,6 +42,21 @@ type Events = | { readonly type: "SELECT_DOCUMENT_ROOT_DESIGNER"; } + | { + readonly type: "SELECT_PATH_VISUALIZER"; + readonly path: string; + readonly nodePath: string; + } + | { + readonly type: "SELECT_DATA_TYPE_VISUALIZER"; + readonly name: string; + readonly nodePath: string; + } + | { + readonly type: "SELECT_RESPONSE_VISUALIZER"; + readonly name: string; + readonly nodePath: string; + } | { readonly type: "SELECT_PATH_DESIGNER"; readonly path: string; @@ -56,6 +72,9 @@ type Events = readonly name: string; readonly nodePath: string; } + | { + readonly type: "SELECT_DOCUMENT_ROOT_VISUALIZER"; + } | { readonly type: "SELECT_DOCUMENT_ROOT_CODE"; } @@ -107,6 +126,9 @@ type Events = | { readonly type: "REDO"; } + | { + readonly type: "GO_TO_VISUALIZER_VIEW"; + } | { readonly type: "GO_TO_DESIGNER_VIEW"; } @@ -162,7 +184,7 @@ export const OpenApiEditorMachine = setup({ selectedNode: { type: "root", }, - view: "designer", + view: "visualize", }, initial: "viewChanged", states: { @@ -184,12 +206,15 @@ export const OpenApiEditorMachine = setup({ return undefined; } switch (context.view) { - case "designer": + case "design": + case "visualize": switch (context.selectedNode.type) { case "root": + console.log("SPAWN", context.view); return spawn("documentRootDesigner", { input: { parentRef: self, + editable: context.view === "design", }, }); case "path": @@ -328,13 +353,53 @@ export const OpenApiEditorMachine = setup({ target: ".debouncing", actions: assign({ navigationFilter: ({ event }) => event.filter }), }, + SELECT_DOCUMENT_ROOT_VISUALIZER: { + target: ".viewChanged", + actions: assign({ + selectedNode: { type: "root" }, + view: "visualize", + }), + }, SELECT_DOCUMENT_ROOT_DESIGNER: { target: ".viewChanged", actions: assign({ selectedNode: { type: "root" }, - view: "designer", + view: "design", }), }, + SELECT_PATH_VISUALIZER: { + target: ".viewChanged", + actions: assign(({ event }) => ({ + selectedNode: { + type: "path", + path: event.path, + nodePath: event.nodePath, + }, + view: "visualize", + })), + }, + SELECT_DATA_TYPE_VISUALIZER: { + target: ".viewChanged", + actions: assign(({ event }) => ({ + selectedNode: { + type: "datatype", + name: event.name, + nodePath: event.nodePath, + }, + view: "visualize", + })), + }, + SELECT_RESPONSE_VISUALIZER: { + target: ".viewChanged", + actions: assign(({ event }) => ({ + selectedNode: { + type: "response", + name: event.name, + nodePath: event.nodePath, + }, + view: "visualize", + })), + }, SELECT_PATH_DESIGNER: { target: ".viewChanged", actions: assign(({ event }) => ({ @@ -343,7 +408,7 @@ export const OpenApiEditorMachine = setup({ path: event.path, nodePath: event.nodePath, }, - view: "designer", + view: "design", })), }, SELECT_DATA_TYPE_DESIGNER: { @@ -354,7 +419,7 @@ export const OpenApiEditorMachine = setup({ name: event.name, nodePath: event.nodePath, }, - view: "designer", + view: "design", })), }, SELECT_RESPONSE_DESIGNER: { @@ -365,7 +430,7 @@ export const OpenApiEditorMachine = setup({ name: event.name, nodePath: event.nodePath, }, - view: "designer", + view: "design", })), }, SELECT_DOCUMENT_ROOT_CODE: { @@ -416,13 +481,19 @@ export const OpenApiEditorMachine = setup({ GO_TO_DESIGNER_VIEW: { target: ".viewChanged", actions: assign({ - view: "code", + view: "design", }), }, GO_TO_CODE_VIEW: { target: ".viewChanged", actions: assign({ - view: "designer", + view: "code", + }), + }, + GO_TO_VISUALIZER_VIEW: { + target: ".viewChanged", + actions: assign({ + view: "visualize", }), }, UNDO: ".undoing", diff --git a/packages/ui/src/components/EditorSidebar.tsx b/packages/ui/src/components/EditorSidebar.tsx index bd7b6f2..eb707ca 100644 --- a/packages/ui/src/components/EditorSidebar.tsx +++ b/packages/ui/src/components/EditorSidebar.tsx @@ -11,7 +11,7 @@ import { NavigationDataTypes } from "./NavigationDataTypes.tsx"; export function EditorSidebar() { const { isFiltering, - isDesignerView, + view, paths, responses, dataTypes, @@ -19,7 +19,7 @@ export function EditorSidebar() { selectedNode, } = OpenApiEditorMachineContext.useSelector((state) => ({ isFiltering: state.matches("debouncing") || state.matches("filtering"), - isDesignerView: state.context.view === "designer", + view: state.context.view, paths: state.context.navigation.paths, responses: state.context.navigation.responses, dataTypes: state.context.navigation.dataTypes, @@ -62,15 +62,23 @@ export function EditorSidebar() { + onClick={(p) => { + const type = (() => { + switch (view) { + case "visualize": + return "SELECT_PATH_VISUALIZER"; + case "design": + return "SELECT_PATH_DESIGNER"; + case "code": + return "SELECT_PATH_CODE"; + } + })(); actorRef.send({ - type: isDesignerView - ? "SELECT_PATH_DESIGNER" - : "SELECT_PATH_CODE", + type, path: p.path, nodePath: p.nodePath, - }) - } + }); + }} isActive={(p) => "path" in selectedNode && p.path === selectedNode?.path } @@ -80,15 +88,23 @@ export function EditorSidebar() { + onClick={(dt) => { + const type = (() => { + switch (view) { + case "visualize": + return "SELECT_DATA_TYPE_VISUALIZER"; + case "design": + return "SELECT_DATA_TYPE_DESIGNER"; + case "code": + return "SELECT_DATA_TYPE_CODE"; + } + })(); actorRef.send({ - type: isDesignerView - ? "SELECT_DATA_TYPE_DESIGNER" - : "SELECT_DATA_TYPE_CODE", + type, name: dt.name, nodePath: dt.nodePath, - }) - } + }); + }} isActive={(p) => "name" in selectedNode && p.name === selectedNode?.name } @@ -98,15 +114,23 @@ export function EditorSidebar() { + onClick={(r) => { + const type = (() => { + switch (view) { + case "visualize": + return "SELECT_RESPONSE_VISUALIZER"; + case "design": + return "SELECT_RESPONSE_DESIGNER"; + case "code": + return "SELECT_RESPONSE_CODE"; + } + })(); actorRef.send({ - type: isDesignerView - ? "SELECT_RESPONSE_DESIGNER" - : "SELECT_RESPONSE_CODE", + type, name: r.name, nodePath: r.nodePath, - }) - } + }); + }} isActive={(p) => "name" in selectedNode && p.name === selectedNode?.name } diff --git a/packages/ui/src/components/EditorToolbar.tsx b/packages/ui/src/components/EditorToolbar.tsx index 43bd387..117f991 100644 --- a/packages/ui/src/components/EditorToolbar.tsx +++ b/packages/ui/src/components/EditorToolbar.tsx @@ -22,14 +22,14 @@ import { } from "@patternfly/react-icons"; import { ReactNode } from "react"; -type View = "designer" | "code" | "no-code"; +export type EditorToolbarView = "design" | "code" | "visualize" | "hidden"; export type EditorToolbarProps = { title: ReactNode; label?: ReactNode; - view: View; + view: EditorToolbarView; canGoBack: boolean; onBack: () => void; - onViewChange: (view: View) => void; + onViewChange: (view: EditorToolbarView) => void; }; export function EditorToolbar({ title, @@ -56,75 +56,84 @@ export function EditorToolbar({ return ( - {canGoBack && ( - - - + + {canGoBack && ( + + + + + + )} + + - {view !== "no-code" && ( + {view !== "hidden" && ( <> - { + onViewChange("visualize"); + }} + /> + { - onViewChange("designer"); + onViewChange("design"); }} /> { diff --git a/packages/ui/src/components/InlineEdit.tsx b/packages/ui/src/components/InlineEdit.tsx index 5f7f0ad..3461327 100644 --- a/packages/ui/src/components/InlineEdit.tsx +++ b/packages/ui/src/components/InlineEdit.tsx @@ -9,12 +9,7 @@ import { InputGroupItem, TextInput, } from "@patternfly/react-core"; -import { - CheckIcon, - ExclamationCircleIcon, - PencilAltIcon, - TimesIcon, -} from "@patternfly/react-icons"; +import { CheckIcon, ExclamationCircleIcon } from "@patternfly/react-icons"; import { FormEventHandler, FunctionComponent, @@ -36,27 +31,19 @@ interface IInlineEdit { validator?: (value: string) => ValidationResult; onChange?: (value: string) => void; onClick?: () => void; - disabled?: boolean; + editing?: boolean; + autoFocus?: boolean; } export const InlineEdit: FunctionComponent = (props) => { const [localValue, setLocalValue] = useState(props.value ?? ""); - const [isReadOnly, setIsReadOnly] = useState(true); - - const focusTextInput = useCallback((element: HTMLInputElement) => { - element?.focus(); - }, []); + const [isReadOnly, setIsReadOnly] = useState(false); const [validationResult, setValidationResult] = useState({ status: "default", errMessages: [], }); - const onEdit: MouseEventHandler = useCallback((event) => { - setIsReadOnly(false); - event.stopPropagation(); - }, []); - const onChange = useCallback( (_event: unknown, value: string) => { setLocalValue(value); @@ -113,25 +100,20 @@ export const InlineEdit: FunctionComponent = (props) => { [saveValue] ); - const onCancel: MouseEventHandler = useCallback( - (event) => { - cancelValue(); - event.stopPropagation(); - }, - [cancelValue] - ); - const noop: FormEventHandler = useCallback((event) => { event.preventDefault(); }, []); useEffect(() => { setLocalValue(props.value ?? ""); + setIsReadOnly(false); }, [props.value]); + const isChanged = props.value !== localValue; + return ( <> - {isReadOnly || props.disabled ? ( + {!props.editing ? ( <> = (props) => { )} -    - {!props.disabled && ( -