Header removals wait for the code editor to be left: autosave saves with drop=false, so follow_header adds and renames from the header but keeps a port or setting whose parameter is gone, and leaving the editor (blur or closing the panel) saves once more with removals; source saves of a flow run one at a time
This commit is contained in:
@@ -1119,11 +1119,14 @@ export class FlowsService {
|
||||
*
|
||||
* A node with its own code takes its input ports and settings from the
|
||||
* header of ``process`` (see `follow_header`), and the answer carries the
|
||||
* node when that changed it.
|
||||
* node when that changed it. ``drop=false`` is the editor's autosave, which
|
||||
* adds and renames from the header but keeps what it no longer names until
|
||||
* the editor is left.
|
||||
* @param data The data for the request.
|
||||
* @param data.name
|
||||
* @param data.nodeId
|
||||
* @param data.requestBody
|
||||
* @param data.drop
|
||||
* @returns NodeSourceSaved Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
@@ -1135,6 +1138,9 @@ export class FlowsService {
|
||||
name: data.name,
|
||||
node_id: data.nodeId
|
||||
},
|
||||
query: {
|
||||
drop: data.drop
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
|
||||
@@ -1849,6 +1849,7 @@ export type FlowsReadNodeSourceData = {
|
||||
export type FlowsReadNodeSourceResponse = (NodeSource);
|
||||
|
||||
export type FlowsSaveNodeSourceData = {
|
||||
drop?: boolean;
|
||||
name: string;
|
||||
nodeId: string;
|
||||
requestBody: NodeSource;
|
||||
|
||||
@@ -865,7 +865,15 @@ function FlowEditorInner({
|
||||
})
|
||||
|
||||
const sourceMutation = useMutation({
|
||||
mutationFn: async ({ nodeId, code }: { nodeId: string; code: string }) => {
|
||||
mutationFn: async ({
|
||||
nodeId,
|
||||
code,
|
||||
drop,
|
||||
}: {
|
||||
nodeId: string
|
||||
code: string
|
||||
drop?: boolean
|
||||
}) => {
|
||||
// The server reads the ports off the header and compares them with its
|
||||
// draft, so the ports on the canvas have to be there first.
|
||||
await flush()
|
||||
@@ -873,8 +881,12 @@ function FlowEditorInner({
|
||||
name: flowName,
|
||||
nodeId,
|
||||
requestBody: { code },
|
||||
drop,
|
||||
})
|
||||
},
|
||||
// One at a time: the save that drops a port must not land before the
|
||||
// autosave that kept it.
|
||||
scope: { id: `source:${flowName}` },
|
||||
onSuccess: (saved) => {
|
||||
const followed = saved.node
|
||||
if (followed && saved.version) {
|
||||
@@ -1454,8 +1466,9 @@ function FlowEditorInner({
|
||||
onToggleExpand={() => setEditorExpanded((wide) => !wide)}
|
||||
onChange={updateNode}
|
||||
onRenameMessage={proposeRename}
|
||||
onSaveSource={(code) => {
|
||||
if (selected) sourceMutation.mutate({ nodeId: selected.id, code })
|
||||
onSaveSource={(code, drop) => {
|
||||
if (selected)
|
||||
sourceMutation.mutate({ nodeId: selected.id, code, drop })
|
||||
}}
|
||||
// Sharing rewrites the stored document, so the canvas takes the
|
||||
// server's copy rather than keeping its own.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Editor from "@monaco-editor/react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
@@ -13,12 +13,15 @@ import { monacoFontFamily, setupMonaco } from "./monacoSetup"
|
||||
export default function NodeEditor({
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
language = "python",
|
||||
readOnly = false,
|
||||
autoFocus = false,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (next: string) => void
|
||||
/** Focus left the editor, its find and suggest widgets included. */
|
||||
onBlur?: () => void
|
||||
language?: "python" | "json"
|
||||
/** Generated code: readable, and not this panel's to change. */
|
||||
readOnly?: boolean
|
||||
@@ -27,6 +30,8 @@ export default function NodeEditor({
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [ready, setReady] = useState(false)
|
||||
const blur = useRef(onBlur)
|
||||
blur.current = onBlur
|
||||
|
||||
useEffect(() => {
|
||||
setupMonaco()
|
||||
@@ -43,7 +48,10 @@ export default function NodeEditor({
|
||||
theme={resolvedTheme === "dark" ? "fluksio-dark" : "fluksio-light"}
|
||||
value={value}
|
||||
onChange={(next) => onChange(next ?? "")}
|
||||
onMount={autoFocus ? (editor) => editor.focus() : undefined}
|
||||
onMount={(editor) => {
|
||||
editor.onDidBlurEditorWidget(() => blur.current?.())
|
||||
if (autoFocus) editor.focus()
|
||||
}}
|
||||
loading={<Skeleton className="h-full w-full rounded-md" />}
|
||||
options={{
|
||||
readOnly,
|
||||
|
||||
@@ -1409,7 +1409,8 @@ function PanelBody({
|
||||
expanded: boolean
|
||||
onChange: (next: NodeDef_Input) => void
|
||||
onRenameMessage: (previous: string, next: string) => void
|
||||
onSaveSource: (code: string) => void
|
||||
/** `drop` is false while the code is still being typed; see `settle`. */
|
||||
onSaveSource: (code: string, drop: boolean) => void
|
||||
onShared: () => void
|
||||
onToggleExpand: () => void
|
||||
}) {
|
||||
@@ -1431,6 +1432,7 @@ function PanelBody({
|
||||
// only overwrite the first.
|
||||
const portMove = useRef<[string, string | null] | null>(null)
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
// Code typed since the editor was last left.
|
||||
const pending = useRef<string | null>(null)
|
||||
const save = useRef(onSaveSource)
|
||||
save.current = onSaveSource
|
||||
@@ -1441,7 +1443,22 @@ function PanelBody({
|
||||
clearTimeout(timer.current)
|
||||
timer.current = null
|
||||
}
|
||||
if (pending.current !== null) save.current(pending.current)
|
||||
if (pending.current !== null) save.current(pending.current, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* The editor was left, so the code is saved once more, now dropping the
|
||||
* ports and settings its header no longer names. Saves while typing only
|
||||
* add and rename, so a parameter deleted mid-edit keeps its port and
|
||||
* message until then, and typing it back changes nothing.
|
||||
*/
|
||||
const settle = () => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = null
|
||||
}
|
||||
if (pending.current !== null) save.current(pending.current, true)
|
||||
pending.current = null
|
||||
}
|
||||
|
||||
const editCode = (next: string) => {
|
||||
@@ -1450,7 +1467,7 @@ function PanelBody({
|
||||
if (timer.current) clearTimeout(timer.current)
|
||||
timer.current = setTimeout(() => {
|
||||
timer.current = null
|
||||
save.current(next)
|
||||
save.current(next, false)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
@@ -1520,13 +1537,13 @@ function PanelBody({
|
||||
["mod+s"],
|
||||
)
|
||||
|
||||
// Closing the panel must not lose the last keystrokes.
|
||||
// Closing the panel leaves the editor too, and must not lose the last
|
||||
// keystrokes.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
if (pending.current !== null) save.current(pending.current)
|
||||
}
|
||||
if (timer.current) clearTimeout(timer.current)
|
||||
if (pending.current !== null) save.current(pending.current, true)
|
||||
pending.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -1704,6 +1721,7 @@ function PanelBody({
|
||||
<NodeEditor
|
||||
value={code ?? source?.code ?? ""}
|
||||
onChange={editCode}
|
||||
onBlur={settle}
|
||||
readOnly={Boolean(origin) || !mayEditCode}
|
||||
/>
|
||||
</Suspense>
|
||||
@@ -1745,7 +1763,7 @@ export function NodePanel({
|
||||
expanded: boolean
|
||||
onChange: (next: NodeDef_Input) => void
|
||||
onRenameMessage: (previous: string, next: string) => void
|
||||
onSaveSource: (code: string) => void
|
||||
onSaveSource: (code: string, drop: boolean) => void
|
||||
onShared: () => void
|
||||
onToggleExpand: () => void
|
||||
onClose: () => void
|
||||
|
||||
Reference in New Issue
Block a user