Quellcode durchsuchen

feat(demo): node title editable (#305)

xiamidaxia vor 7 Monaten
Ursprung
Commit
5d4956f393

+ 20 - 19
apps/demo-fixed-layout/src/form-components/form-header/index.tsx

@@ -1,21 +1,20 @@
-import { useContext, useCallback, useMemo } from 'react';
+import { useContext, useCallback, useMemo, useState } from 'react';
 
-import { Field, FieldRenderProps, useClientContext } from '@flowgram.ai/fixed-layout-editor';
-import { IconButton, Dropdown, Typography, Button } from '@douyinfe/semi-ui';
+import { useClientContext } from '@flowgram.ai/fixed-layout-editor';
+import { IconButton, Dropdown, Button } from '@douyinfe/semi-ui';
 import { IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';
 import { IconMore } from '@douyinfe/semi-icons';
 
-import { Feedback } from '../feedback';
 import { FlowNodeRegistry } from '../../typings';
 import { FlowCommandId } from '../../shortcuts/constants';
 import { useIsSidebar } from '../../hooks';
 import { NodeRenderContext } from '../../context';
 import { getIcon } from './utils';
-import { Header, Operators, Title } from './styles';
+import { TitleInput } from './title-input';
+import { Header, Operators } from './styles';
 
-const { Text } = Typography;
-
-function DropdownContent() {
+function DropdownContent(props: { updateTitleEdit: (editing: boolean) => void }) {
+  const { updateTitleEdit } = props;
   const { node, deleteNode } = useContext(NodeRenderContext);
   const clientContext = useClientContext();
   const registry = node.getNodeRegistry<FlowNodeRegistry>();
@@ -34,6 +33,11 @@ function DropdownContent() {
     },
     [clientContext, node]
   );
+
+  const handleEditTitle = useCallback(() => {
+    updateTitleEdit(true);
+  }, [updateTitleEdit]);
+
   const deleteDisabled = useMemo(() => {
     if (registry.canDelete) {
       return !registry.canDelete(clientContext, node);
@@ -43,6 +47,7 @@ function DropdownContent() {
 
   return (
     <Dropdown.Menu>
+      <Dropdown.Item onClick={handleEditTitle}>Edit Title</Dropdown.Item>
       <Dropdown.Item onClick={handleCopy} disabled={registry.meta!.copyDisable === true}>
         Copy
       </Dropdown.Item>
@@ -55,6 +60,7 @@ function DropdownContent() {
 
 export function FormHeader() {
   const { node, expanded, startDrag, toggleExpand, readonly } = useContext(NodeRenderContext);
+  const [titleEdit, updateTitleEdit] = useState<boolean>(false);
 
   const isSidebar = useIsSidebar();
   const handleExpand = (e: React.MouseEvent) => {
@@ -71,16 +77,7 @@ export function FormHeader() {
       }}
     >
       {getIcon(node)}
-      <Title>
-        <Field name="title">
-          {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
-            <div style={{ height: 24 }}>
-              <Text ellipsis={{ showTooltip: true }}>{value}</Text>
-              <Feedback errors={fieldState?.errors} />
-            </div>
-          )}
-        </Field>
-      </Title>
+      <TitleInput readonly={readonly} titleEdit={titleEdit} updateTitleEdit={updateTitleEdit} />
       {node.renderData.expandable && !isSidebar && (
         <Button
           type="primary"
@@ -92,7 +89,11 @@ export function FormHeader() {
       )}
       {readonly ? undefined : (
         <Operators>
-          <Dropdown trigger="hover" position="bottomRight" render={<DropdownContent />}>
+          <Dropdown
+            trigger="hover"
+            position="bottomRight"
+            render={<DropdownContent updateTitleEdit={updateTitleEdit} />}
+          >
             <IconButton
               color="secondary"
               size="small"

+ 45 - 0
apps/demo-fixed-layout/src/form-components/form-header/title-input.tsx

@@ -0,0 +1,45 @@
+import { useRef, useEffect } from 'react';
+
+import { Field, FieldRenderProps } from '@flowgram.ai/fixed-layout-editor';
+import { Typography, Input } from '@douyinfe/semi-ui';
+
+import { Title } from './styles';
+import { Feedback } from '../feedback';
+const { Text } = Typography;
+
+export function TitleInput(props: {
+  readonly: boolean;
+  titleEdit: boolean;
+  updateTitleEdit: (setEdit: boolean) => void;
+}): JSX.Element {
+  const { readonly, titleEdit, updateTitleEdit } = props;
+  const ref = useRef<any>();
+  const titleEditing = titleEdit && !readonly;
+  useEffect(() => {
+    if (titleEditing) {
+      ref.current?.focus();
+    }
+  }, [titleEditing]);
+
+  return (
+    <Title>
+      <Field name="title">
+        {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
+          <div style={{ height: 24 }}>
+            {titleEditing ? (
+              <Input
+                value={value}
+                onChange={onChange}
+                ref={ref}
+                onBlur={() => updateTitleEdit(false)}
+              />
+            ) : (
+              <Text ellipsis={{ showTooltip: true }}>{value}</Text>
+            )}
+            <Feedback errors={fieldState?.errors} />
+          </div>
+        )}
+      </Field>
+    </Title>
+  );
+}

+ 6 - 1
apps/demo-free-layout/src/components/node-menu/index.tsx

@@ -18,10 +18,11 @@ import { CopyShortcut } from '../../shortcuts/copy';
 
 interface NodeMenuProps {
   node: WorkflowNodeEntity;
+  updateTitleEdit: (setEditing: boolean) => void;
   deleteNode: () => void;
 }
 
-export const NodeMenu: FC<NodeMenuProps> = ({ node, deleteNode }) => {
+export const NodeMenu: FC<NodeMenuProps> = ({ node, deleteNode, updateTitleEdit }) => {
   const [visible, setVisible] = useState(true);
   const clientContext = useClientContext();
   const registry = node.getNodeRegistry<FlowNodeRegistry>();
@@ -77,6 +78,9 @@ export const NodeMenu: FC<NodeMenuProps> = ({ node, deleteNode }) => {
     },
     [clientContext, node]
   );
+  const handleEditTitle = useCallback(() => {
+    updateTitleEdit(true);
+  }, [updateTitleEdit]);
 
   if (!visible) {
     return;
@@ -88,6 +92,7 @@ export const NodeMenu: FC<NodeMenuProps> = ({ node, deleteNode }) => {
       position="bottomRight"
       render={
         <Dropdown.Menu>
+          <Dropdown.Item onClick={handleEditTitle}>Edit Title</Dropdown.Item>
           {canMoveOut && <Dropdown.Item onClick={handleMoveOut}>Move out</Dropdown.Item>}
           <Dropdown.Item onClick={handleCopy} disabled={registry.meta!.copyDisable === true}>
             Create Copy

+ 8 - 17
apps/demo-free-layout/src/form-components/form-header/index.tsx

@@ -1,19 +1,19 @@
-import { Field, FieldRenderProps } from '@flowgram.ai/free-layout-editor';
+import { useState } from 'react';
+
 import { useClientContext, CommandService } from '@flowgram.ai/free-layout-editor';
-import { Typography, Button } from '@douyinfe/semi-ui';
+import { Button } from '@douyinfe/semi-ui';
 import { IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';
 
-import { Feedback } from '../feedback';
 import { FlowCommandId } from '../../shortcuts';
 import { useIsSidebar, useNodeRenderContext } from '../../hooks';
 import { NodeMenu } from '../../components/node-menu';
 import { getIcon } from './utils';
-import { Header, Operators, Title } from './styles';
-
-const { Text } = Typography;
+import { TitleInput } from './title-input';
+import { Header, Operators } from './styles';
 
 export function FormHeader() {
   const { node, expanded, toggleExpand, readonly } = useNodeRenderContext();
+  const [titleEdit, updateTitleEdit] = useState<boolean>(false);
   const ctx = useClientContext();
   const isSidebar = useIsSidebar();
   const handleExpand = (e: React.MouseEvent) => {
@@ -27,16 +27,7 @@ export function FormHeader() {
   return (
     <Header>
       {getIcon(node)}
-      <Title>
-        <Field name="title">
-          {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
-            <div style={{ height: 24 }}>
-              <Text ellipsis={{ showTooltip: true }}>{value}</Text>
-              <Feedback errors={fieldState?.errors} />
-            </div>
-          )}
-        </Field>
-      </Title>
+      <TitleInput readonly={readonly} updateTitleEdit={updateTitleEdit} titleEdit={titleEdit} />
       {node.renderData.expandable && !isSidebar && (
         <Button
           type="primary"
@@ -48,7 +39,7 @@ export function FormHeader() {
       )}
       {readonly ? undefined : (
         <Operators>
-          <NodeMenu node={node} deleteNode={handleDelete} />
+          <NodeMenu node={node} deleteNode={handleDelete} updateTitleEdit={updateTitleEdit} />
         </Operators>
       )}
     </Header>

+ 45 - 0
apps/demo-free-layout/src/form-components/form-header/title-input.tsx

@@ -0,0 +1,45 @@
+import { useRef, useEffect } from 'react';
+
+import { Field, FieldRenderProps } from '@flowgram.ai/free-layout-editor';
+import { Typography, Input } from '@douyinfe/semi-ui';
+
+import { Title } from './styles';
+import { Feedback } from '../feedback';
+const { Text } = Typography;
+
+export function TitleInput(props: {
+  readonly: boolean;
+  titleEdit: boolean;
+  updateTitleEdit: (setEdit: boolean) => void;
+}): JSX.Element {
+  const { readonly, titleEdit, updateTitleEdit } = props;
+  const ref = useRef<any>();
+  const titleEditing = titleEdit && !readonly;
+  useEffect(() => {
+    if (titleEditing) {
+      ref.current?.focus();
+    }
+  }, [titleEditing]);
+
+  return (
+    <Title>
+      <Field name="title">
+        {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
+          <div style={{ height: 24 }}>
+            {titleEditing ? (
+              <Input
+                value={value}
+                onChange={onChange}
+                ref={ref}
+                onBlur={() => updateTitleEdit(false)}
+              />
+            ) : (
+              <Text ellipsis={{ showTooltip: true }}>{value}</Text>
+            )}
+            <Feedback errors={fieldState?.errors} />
+          </div>
+        )}
+      </Field>
+    </Title>
+  );
+}