Skip to content

Commit aedb6b8

Browse files
committed
feat/COMPASS-9798 inline field name editing
1 parent 2a20aa0 commit aedb6b8

File tree

8 files changed

+132
-8
lines changed

8 files changed

+132
-8
lines changed

src/components/canvas/canvas.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const Canvas = ({
5959
id,
6060
onAddFieldToNodeClick,
6161
onAddFieldToObjectFieldClick,
62+
onFieldNameChange,
6263
onFieldClick,
6364
onNodeContextMenu,
6465
onNodeDrag,
@@ -147,6 +148,7 @@ export const Canvas = ({
147148
onFieldClick={onFieldClick}
148149
onAddFieldToNodeClick={onAddFieldToNodeClick}
149150
onAddFieldToObjectFieldClick={onAddFieldToObjectFieldClick}
151+
onFieldNameChange={onFieldNameChange}
150152
>
151153
<ReactFlowWrapper>
152154
<ReactFlow

src/components/diagram.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const DiagramWithEditInteractions: Story = {
6565
...field,
6666
id: idFromDepthAccumulator(field.name, field.depth),
6767
selectable: true,
68+
editable: true,
6869
})),
6970
],
7071
},
@@ -75,6 +76,7 @@ export const DiagramWithEditInteractions: Story = {
7576
...field,
7677
id: idFromDepthAccumulator(field.name, field.depth),
7778
selectable: true,
79+
editable: true,
7880
})),
7981
],
8082
},
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { styled } from 'storybook/internal/theming';
2+
import { useCallback, useState } from 'react';
3+
4+
import { ellipsisTruncation } from '@/styles/styles';
5+
import { DEFAULT_FIELD_HEIGHT } from '@/utilities/constants';
6+
7+
const InnerFieldName = styled.div`
8+
${ellipsisTruncation}
9+
`;
10+
11+
const InlineTextarea = styled.textarea`
12+
border: none;
13+
background: none;
14+
height: ${DEFAULT_FIELD_HEIGHT}px;
15+
color: inherit;
16+
font-size: inherit;
17+
font-family: inherit;
18+
font-style: inherit;
19+
`;
20+
21+
interface FieldNameProps {
22+
name: string;
23+
isEditable?: boolean;
24+
onChange?: (newName: string) => void;
25+
onBlur?: () => void;
26+
}
27+
28+
export const FieldNameContent = ({ name, isEditable, onChange }: FieldNameProps) => {
29+
const [isEditing, setIsEditing] = useState(false);
30+
const [value, setValue] = useState(name);
31+
32+
const handleSubmit = useCallback(() => {
33+
setIsEditing(false);
34+
onChange?.(value);
35+
}, [value, onChange]);
36+
37+
return isEditing ? (
38+
<InlineTextarea
39+
value={value}
40+
onChange={e => {
41+
setValue(e.target.value);
42+
}}
43+
onBlur={handleSubmit}
44+
onKeyDown={e => {
45+
if (e.key === 'Enter') handleSubmit();
46+
if (e.key === 'Escape') setIsEditing(false);
47+
}}
48+
/>
49+
) : (
50+
<InnerFieldName
51+
onDoubleClick={
52+
onChange && isEditable
53+
? () => {
54+
setIsEditing(true);
55+
}
56+
: undefined
57+
}
58+
>
59+
{value}
60+
</InnerFieldName>
61+
);
62+
};

src/components/field/field.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { NodeField, NodeGlyph, NodeType } from '@/types';
1414
import { PreviewGroupArea } from '@/utilities/get-preview-group-area';
1515
import { useEditableDiagramInteractions } from '@/hooks/use-editable-diagram-interactions';
1616

17+
import { FieldNameContent } from './field-name-content';
18+
1719
const FIELD_BORDER_ANIMATED_PADDING = spacing[100];
1820
const FIELD_GLYPH_SPACING = spacing[400];
1921

@@ -105,10 +107,6 @@ const FieldName = styled.div`
105107
${ellipsisTruncation}
106108
`;
107109

108-
const InnerFieldName = styled.div`
109-
${ellipsisTruncation}
110-
`;
111-
112110
const FieldType = styled.div`
113111
color: ${props => props.color};
114112
flex: 0 0 ${LGSpacing[200] * 10}px;
@@ -149,11 +147,12 @@ export const Field = ({
149147
spacing = 0,
150148
selectable = false,
151149
selected = false,
150+
editable = false,
152151
variant,
153152
}: Props) => {
154153
const { theme } = useDarkMode();
155154

156-
const { onClickField } = useEditableDiagramInteractions();
155+
const { onClickField, onChangeFieldName } = useEditableDiagramInteractions();
157156

158157
const internalTheme = useTheme();
159158

@@ -215,7 +214,15 @@ export const Field = ({
215214
<>
216215
<FieldName>
217216
<FieldDepth depth={depth} />
218-
<InnerFieldName>{name}</InnerFieldName>
217+
<FieldNameContent
218+
name={name}
219+
isEditable={editable}
220+
onChange={
221+
onChangeFieldName
222+
? (newName: string) => onChangeFieldName(nodeId, Array.isArray(id) ? id : [id], newName)
223+
: undefined
224+
}
225+
/>
219226
</FieldName>
220227
<FieldType color={getSecondaryTextColor()}>
221228
<FieldTypeContent type={type} nodeId={nodeId} id={id} />

src/hooks/use-editable-diagram-interactions.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import React, { createContext, useContext, useMemo, ReactNode } from 'react';
22

3-
import { OnFieldClickHandler, OnAddFieldToNodeClickHandler, OnAddFieldToObjectFieldClickHandler } from '@/types';
3+
import {
4+
OnFieldClickHandler,
5+
OnAddFieldToNodeClickHandler,
6+
OnAddFieldToObjectFieldClickHandler,
7+
OnFieldNameChangeHandler,
8+
} from '@/types';
49

510
interface EditableDiagramInteractionsContextType {
611
onClickField?: OnFieldClickHandler;
712
onClickAddFieldToNode?: OnAddFieldToNodeClickHandler;
813
onClickAddFieldToObjectField?: OnAddFieldToObjectFieldClickHandler;
14+
onChangeFieldName?: OnFieldNameChangeHandler;
915
}
1016

1117
const EditableDiagramInteractionsContext = createContext<EditableDiagramInteractionsContextType | undefined>(undefined);
@@ -15,13 +21,15 @@ interface EditableDiagramInteractionsProviderProps {
1521
onFieldClick?: OnFieldClickHandler;
1622
onAddFieldToNodeClick?: OnAddFieldToNodeClickHandler;
1723
onAddFieldToObjectFieldClick?: OnAddFieldToObjectFieldClickHandler;
24+
onFieldNameChange?: OnFieldNameChangeHandler;
1825
}
1926

2027
export const EditableDiagramInteractionsProvider: React.FC<EditableDiagramInteractionsProviderProps> = ({
2128
children,
2229
onFieldClick,
2330
onAddFieldToNodeClick,
2431
onAddFieldToObjectFieldClick,
32+
onFieldNameChange,
2533
}) => {
2634
const value: EditableDiagramInteractionsContextType = useMemo(() => {
2735
return {
@@ -40,8 +48,13 @@ export const EditableDiagramInteractionsProvider: React.FC<EditableDiagramIntera
4048
onClickAddFieldToObjectField: onAddFieldToObjectFieldClick,
4149
}
4250
: undefined),
51+
...(onFieldNameChange
52+
? {
53+
onChangeFieldName: onFieldNameChange,
54+
}
55+
: undefined),
4356
};
44-
}, [onFieldClick, onAddFieldToNodeClick, onAddFieldToObjectFieldClick]);
57+
}, [onFieldClick, onAddFieldToNodeClick, onAddFieldToObjectFieldClick, onFieldNameChange]);
4558

4659
return (
4760
<EditableDiagramInteractionsContext.Provider value={value}>{children}</EditableDiagramInteractionsContext.Provider>

src/mocks/decorators/diagram-editable-interactions.decorator.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ function addFieldToNode(existingFields: NodeField[], parentFieldPath: string[])
4949
return fields;
5050
}
5151

52+
function renameField(existingFields: NodeField[], fieldPath: string[], newName: string) {
53+
const fields = existingFields.map(field => {
54+
if (JSON.stringify(field.id) !== JSON.stringify(fieldPath)) return field;
55+
return { ...field, name: newName, id: [...fieldPath.slice(0, -1), newName] };
56+
});
57+
console.log('Renamed fields:', fields);
58+
return fields;
59+
}
60+
5261
export const DiagramEditableInteractionsDecorator: Decorator<DiagramProps> = (Story, context) => {
5362
const [nodes, setNodes] = useState<NodeProps[]>(context.args.nodes);
5463

@@ -107,6 +116,19 @@ export const DiagramEditableInteractionsDecorator: Decorator<DiagramProps> = (St
107116
[],
108117
);
109118

119+
const onFieldNameChange = useCallback((nodeId: string, fieldPath: string[], newName: string) => {
120+
setNodes(nodes =>
121+
nodes.map(node =>
122+
node.id === nodeId
123+
? {
124+
...node,
125+
fields: renameField(node.fields, fieldPath, newName),
126+
}
127+
: node,
128+
),
129+
);
130+
}, []);
131+
110132
return Story({
111133
...context,
112134
args: {
@@ -115,6 +137,7 @@ export const DiagramEditableInteractionsDecorator: Decorator<DiagramProps> = (St
115137
onFieldClick,
116138
onAddFieldToNodeClick,
117139
onAddFieldToObjectFieldClick,
140+
onFieldNameChange,
118141
},
119142
});
120143
};

src/types/component-props.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export type OnAddFieldToNodeClickHandler = (event: ReactMouseEvent, nodeId: stri
3030
*/
3131
export type OnAddFieldToObjectFieldClickHandler = (event: ReactMouseEvent, nodeId: string, fieldPath: string[]) => void;
3232

33+
/**
34+
* Called when a field's name is edited.
35+
*/
36+
export type OnFieldNameChangeHandler = (nodeId: string, fieldPath: string[], newName: string) => void;
37+
3338
/**
3439
* Called when the canvas (pane) is clicked.
3540
*/
@@ -184,6 +189,11 @@ export interface DiagramProps {
184189
*/
185190
onAddFieldToObjectFieldClick?: OnAddFieldToObjectFieldClickHandler;
186191

192+
/**
193+
* Callback when a field's name is changed.
194+
*/
195+
onFieldNameChange?: OnFieldNameChangeHandler;
196+
187197
/**
188198
* Whether the diagram should pan when dragging elements.
189199
*/

src/types/node.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,9 @@ export interface NodeField {
188188
* Indicates if the field is currently selected.
189189
*/
190190
selected?: boolean;
191+
192+
/**
193+
* Indicates if the field is editable (name and type can be changed).
194+
*/
195+
editable?: boolean;
191196
}

0 commit comments

Comments
 (0)