rename History -> Editor

This commit is contained in:
falsycat 2025-07-12 20:27:51 +09:00
parent 532fc0c006
commit 5ec5574488
8 changed files with 179 additions and 50 deletions

View File

@ -11,6 +11,9 @@
"pages": { "pages": {
"screening": { "screening": {
"nodes": { "nodes": {
"base": {
"remove": "削除"
},
"fetch_ToshoListed": { "fetch_ToshoListed": {
"title": "東証上場銘柄一覧" "title": "東証上場銘柄一覧"
}, },

View File

@ -1,38 +1,59 @@
import { useCallback } from "react"; import { useCallback, useMemo } from "react";
import { Background, Controls, ReactFlow, Node, useNodesState, useEdgesState, addEdge, Connection } from "@xyflow/react"; import { Background, Controls, Edge, ReactFlow, Node, useNodesState, useEdgesState, Connection } from "@xyflow/react";
import * as node from "./node/"; import * as node from "./node/";
import "@xyflow/react/dist/style.css"; import "@xyflow/react/dist/style.css";
import { History, Impl as HistoryImpl } from "./context/History"; import { Editor, Impl as EditorImpl } from "./context/Editor";
const nodeTypes = { const nodeTypes = {
fetch_ToshoListed: node.fetch.ToshoListed, fetch_ToshoListed: node.fetch.ToshoListed,
present_TableDisplay: node.present.TableDisplay, present_TableDisplay: node.present.TableDisplay,
}; };
const initialNodes: Node[] = [ const initialNodes: Node[] = [
{id: "n1", type: "fetch_ToshoListed", position: {x:0, y:0}, data: {prime: true, standard: true, growth: true}}, {
{id: "n2", type: "present_TableDisplay", position: {x:0, y:100}, data: {}}, id: "n1",
{id: "n3", position: {x:0, y:200}, data: {label: "hello"}}, type: "fetch_ToshoListed",
position: {x:0, y:0},
data: {prime: true, standard: true, growth: true},
deletable: false,
},
{
id: "n2",
type: "present_TableDisplay",
position: {x:0, y:100},
data: {},
},
{
id: "n3",
position: {x:0, y:200},
data: {label: "hello"},
},
]; ];
export default function Screening() { export default function Screening() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]);
const editor = useMemo(()=>new EditorImpl(setNodes, setEdges, 100), []);
const onConnect = useCallback( const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)), (conn: Connection)=>editor.connect(conn), []);
[] const onDisconnect = useCallback(
); (conns: Edge[])=>editor.disconnect(conns.map((x)=>x as Connection)), []);
const onNodesDelete = useCallback(
(nodes: Node[])=>editor.remove(nodes.map((x)=>x.id)), []);
return ( return (
<History.Provider value={new HistoryImpl(setNodes)}> <Editor.Provider value={editor}>
<div style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}> <div style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}>
<ReactFlow <ReactFlow
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onNodesDelete={onNodesDelete}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesDelete={onDisconnect}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
fitView fitView
@ -41,6 +62,6 @@ export default function Screening() {
<Background /> <Background />
</ReactFlow> </ReactFlow>
</div> </div>
</History.Provider> </Editor.Provider>
); );
} }

View File

@ -0,0 +1,72 @@
import { Dispatch, SetStateAction, createContext } from "react";
import { Connection, EdgeType, NodeType, addEdge } from "@xyflow/react";
type Pos = {x: int, y: int,};
export interface Iface {
add(type: string, pos: Pos, data: any): string;
remove(id: string[]): void;
modify(id: string, data: any): void;
connect(conn: Connection): void;
disconnect(conn: Connection[]): void;
};
export const Editor = createContext<Iface|undefined>(undefined);
export default Editor;
type SetNodes = Dispatch<SetStateAction<NodeType[]>>;
type SetEdges = Dispatch<SetStateAction<EdgeType[]>>;
export class Impl implements Iface {
_setNodes: SetNodes;
_setEdges: SetEdges;
_nextId: number = 0;
constructor(setNodes: SetNodes, setEdges: SetEdges, nextId: number = 0) {
this._setNodes = setNodes;
this._setEdges = setEdges;
this._nextId = nextId;
}
add(type: string, pos: Pos, data: any): string {
const ret = this._genNextId();
this._setNodes((nds)=>[...nds, {
id: ret,
type: type,
pos: pos,
data: data,
}]);
return ret;
}
remove(ids: string[]): void {
this._setNodes((nds)=>nds.filter((x)=>!ids.includes(x.id)));
}
modify(id: string, data: any): void {
this._setNodes(
(nds)=>nds.map((n)=>n.id === id? {...n, data: data}: n)
);
}
connect(c: Connection): void {
this._setEdges((eds)=>addEdge({
...c,
deletable: true,
}, eds));
}
disconnect(cs: Connection[]): void {
this._setEdges((eds)=>eds.filter((e)=>!cs.some(c=>matchEdgeAndConn(e, c))));
}
_genNextId(): number {
return "n"+(_nextId++);
}
};
function matchEdgeAndConn(e: Edge, c: Connection) {
return (
(e.source === c.source) &&
((e.sourceHandle??null) == (c.sourceHandle??null)) &&
(e.target == c.target) &&
((e.targetHandle??null) == (c.targetHandle??null))
);
}

View File

@ -1,22 +0,0 @@
import { Dispatch, SetStateAction, createContext } from "react";
import { Node, } from "@xyflow/react";
export interface Iface {
push(id: string, data: any): void,
};
export const History = createContext<Iface|undefined>(undefined);
export default History;
export class Impl implements Iface {
setNodes: Dispatch<SetStateAction<Node[]>>;
constructor(setNodes: Dispatch<SetStateAction<Node[]>>) {
this.setNodes = setNodes;
}
push(id: string, data: any): void {
this.setNodes(
(nds)=>nds.map((n)=>n.id == id? {...n, data: data}: n)
);
}
};

View File

@ -1,14 +1,39 @@
import { ReactNode } from "react"; import { useTranslation } from "react-i18next";
import { ReactNode, useContext, useCallback } from "react";
import { NodeProps } from "@xyflow/react";
import style from "./style.module.scss";
import Editor from "../context/Editor";
type Params = { type Params = {
title: ReactNode, title: ReactNode,
children: ReactNode, children: ReactNode,
node: NodeProps,
}; };
export default function Base(p: Params) { export default function Base(p: Params) {
const {t} = useTranslation();
const editor = useContext(Editor);
if (editor === undefined) {
throw new Error("no editor context");
}
const onDelete = useCallback(()=>editor.remove([p.node.id]), [p.node]);
return ( return (
<div> <div className={style.node}>
<div>{p.title}</div> <div className={style.header}>
<div className={style.title}>{p.title}</div>
<div className={style.buttons}>
<button onClick={onDelete} disabled={!p.node.deletable}>
{t("pages.screening.nodes.base.remove")}
</button>
</div>
</div>
<div className={style.body}>
{p.children} {p.children}
</div> </div>
</div>
); );
} }

View File

@ -3,30 +3,35 @@ import { useTranslation } from "react-i18next";
import { Handle, Node, NodeProps, Position } from "@xyflow/react"; import { Handle, Node, NodeProps, Position } from "@xyflow/react";
import Base from "./Base"; import Base from "./Base";
import History from "../context/History"; import Editor from "../context/Editor";
export type ToshoListed = Node<{ export type ToshoListed = Node<{
prime: boolean, prime: boolean,
standard: boolean, standard: boolean,
growth: boolean, growth: boolean,
}, "fetch_ToshoListed">; }, "fetch_ToshoListed">;
export function ToshoListed({id, data}: NodeProps<ToshoListed>) { export function ToshoListed(node: NodeProps<ToshoListed>) {
const {id, data} = node;
const {t} = useTranslation(); const {t} = useTranslation();
const history = useContext(History); const editor = useContext(Editor);
if (history === undefined) { if (editor === undefined) {
throw new Error("no History context"); throw new Error("no editor context");
} }
return ( return (
<Base title={t("pages.screening.nodes.fetch_ToshoListed.title")}> <Base
node={node}
title={t("pages.screening.nodes.fetch_ToshoListed.title")}
>
<div> <div>
<div><label> <div><label>
<input <input
type="checkbox" type="checkbox"
name="prime" name="prime"
checked={data.prime} checked={data.prime}
onChange={(e)=>history.push(id, {...data, prime: e.target.checked})} onChange={(e)=>editor.modify(id, {...data, prime: e.target.checked})}
/> />
{t("terms.toshoPrime")} {t("terms.toshoPrime")}
</label></div> </label></div>
@ -35,7 +40,7 @@ export function ToshoListed({id, data}: NodeProps<ToshoListed>) {
type="checkbox" type="checkbox"
name="standard" name="standard"
checked={data.standard} checked={data.standard}
onChange={(e)=>history.push(id, {...data, standard: e.target.checked})} onChange={(e)=>editor.modify(id, {...data, standard: e.target.checked})}
/> />
{t("terms.toshoStandard")} {t("terms.toshoStandard")}
</label></div> </label></div>
@ -44,7 +49,7 @@ export function ToshoListed({id, data}: NodeProps<ToshoListed>) {
type="checkbox" type="checkbox"
name="growth" name="growth"
checked={data.growth} checked={data.growth}
onChange={(e)=>history.push(id, {...data, growth: e.target.checked})} onChange={(e)=>editor.modify(id, {...data, growth: e.target.checked})}
/> />
{t("terms.toshoGrowth")} {t("terms.toshoGrowth")}
</label></div> </label></div>

View File

@ -1,11 +1,15 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Handle, Position } from "@xyflow/react"; import { Handle, NodeProps, Position } from "@xyflow/react";
import Base from "./Base"; import Base from "./Base";
export function TableDisplay() { export function TableDisplay(node: NodeProps) {
const {t} = useTranslation(); const {t} = useTranslation();
return ( return (
<Base title={t("pages.screening.nodes.present_TableDisplay.title")}> <Base
node={node}
title={t("pages.screening.nodes.present_TableDisplay.title")}
removable={true}
>
<div> <div>
<div><a href="#">{t("pages.screening.nodes.present_TableDisplay.open")}</a></div> <div><a href="#">{t("pages.screening.nodes.present_TableDisplay.open")}</a></div>
</div> </div>

View File

@ -0,0 +1,21 @@
.node {
background-color: #FFF9E5;
border: 1px solid #4A9782;
border-radius: 0.5rem;
overflow: hidden;
.header {
background-color: #DCD0A8;
display: flex;
padding: 0 .5rem;
gap: 1rem;
.title {
}
.buttons {
}
}
.body {
padding: .5rem;
}
}