Skip to content

Commit 1eae19c

Browse files
petrapazkresimir-coko
authored andcommitted
2920 - refactor: move canInsertMentionForProperty, fix datapill drag ghost, clean up drop highlight
1 parent 1df4b3d commit 1eae19c

File tree

4 files changed

+109
-102
lines changed

4 files changed

+109
-102
lines changed

client/src/pages/platform/workflow-editor/components/datapills/DataPill.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import {Tooltip, TooltipContent, TooltipTrigger} from '@/components/ui/tooltip';
22
import useWorkflowNodeDetailsPanelStore from '@/pages/platform/workflow-editor/stores/useWorkflowNodeDetailsPanelStore';
3+
import {encodePath, transformPathForObjectAccess} from '@/pages/platform/workflow-editor/utils/encodingUtils';
34
import getNestedObject from '@/pages/platform/workflow-editor/utils/getNestedObject';
45
import {TYPE_ICONS} from '@/shared/typeIcons';
56
import {ComponentType, DataPillDragPayloadType, PropertyAllType} from '@/shared/types';
67
import {Editor} from '@tiptap/react';
8+
import resolvePath from 'object-resolve-path';
79
import {DragEvent, MouseEvent} from 'react';
810
import {twMerge} from 'tailwind-merge';
911
import {useShallow} from 'zustand/react/shallow';
1012

1113
import useDataPillPanelStore from '../../stores/useDataPillPanelStore';
12-
import {canInsertMentionForProperty, transformPathForObjectAccess} from '../../utils/encodingUtils';
1314

1415
interface HandleDataPillClickProps {
1516
workflowNodeName: string;
@@ -30,6 +31,25 @@ interface DataPillProps {
3031
sampleOutput?: any;
3132
}
3233

34+
export const canInsertMentionForProperty = (
35+
propertyType: string,
36+
parameters: Record<string, unknown>,
37+
path: string
38+
): boolean => {
39+
if (propertyType === 'STRING') {
40+
return true;
41+
}
42+
43+
try {
44+
const resolvedPath = transformPathForObjectAccess(encodePath(path));
45+
const existingValue = resolvePath(parameters, resolvedPath);
46+
47+
return !existingValue || String(existingValue).startsWith('=');
48+
} catch {
49+
return true;
50+
}
51+
};
52+
3353
const DataPillSampleValue = ({sampleOutput}: {sampleOutput: string | number | boolean | null}) => {
3454
const sampleOutputString = String(sampleOutput);
3555

@@ -155,6 +175,21 @@ const DataPill = ({
155175
mentionId,
156176
};
157177

178+
const target = event.currentTarget;
179+
const clone = target.cloneNode(true) as HTMLDivElement;
180+
181+
clone.style.position = 'absolute';
182+
clone.style.top = '-9999px';
183+
clone.style.left = '-9999px';
184+
185+
document.body.appendChild(clone);
186+
187+
event.dataTransfer.setDragImage(clone, clone.offsetWidth / 2, clone.offsetHeight / 2);
188+
189+
requestAnimationFrame(() => {
190+
document.body.removeChild(clone);
191+
});
192+
158193
event.dataTransfer.setData('application/bytechef-datapill', JSON.stringify(payload));
159194
event.dataTransfer.effectAllowed = 'copy';
160195

@@ -181,11 +216,11 @@ const DataPill = ({
181216
onDragEnd={handleDragEnd}
182217
onDragStart={(event) => handleDragStart(event, {workflowNodeName})}
183218
>
184-
<span className="mr-2" title={property?.type}>
219+
<span className="pointer-events-none mr-2" title={property?.type}>
185220
{TYPE_ICONS[property?.type as keyof typeof TYPE_ICONS]}
186221
</span>
187222

188-
<span>{workflowNodeName}</span>
223+
<span className="pointer-events-none">{workflowNodeName}</span>
189224
</div>
190225

191226
{sampleOutput !== undefined && typeof sampleOutput !== 'object' && (
@@ -207,7 +242,7 @@ const DataPill = ({
207242
<TooltipTrigger asChild>
208243
<div
209244
className={twMerge(
210-
'mr-auto flex cursor-pointer items-center rounded-full border bg-surface-neutral-secondary px-2 py-0.5 text-sm hover:bg-surface-main',
245+
'mr-auto inline-flex cursor-pointer items-center rounded-full border bg-surface-neutral-secondary px-2 py-0.5 text-sm hover:bg-surface-main',
211246
!mentionInput && 'cursor-not-allowed'
212247
)}
213248
data-name={property?.name || workflowNodeName}
@@ -231,18 +266,18 @@ const DataPill = ({
231266
}
232267
>
233268
{property?.name && (
234-
<span className="mr-2" title={property?.type}>
269+
<span className="pointer-events-none mr-2" title={property?.type}>
235270
{TYPE_ICONS[property?.type as keyof typeof TYPE_ICONS]}
236271
</span>
237272
)}
238273

239274
{!property?.name && (
240-
<span className="mr-2" title={property?.type}>
275+
<span className="pointer-events-none mr-2" title={property?.type}>
241276
{TYPE_ICONS[property?.type as keyof typeof TYPE_ICONS]}
242277
</span>
243278
)}
244279

245-
{property?.name || '[index]'}
280+
<span className="pointer-events-none">{property?.name || '[index]'}</span>
246281
</div>
247282
</TooltipTrigger>
248283

client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInput.tsx

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
8282
ref: ForwardedRef<Editor>
8383
) => {
8484
const [isFocused, setIsFocused] = useState(false);
85-
const [isDragOver, setIsDragOver] = useState(false);
8685
const isInitialLoadRef = useRef(true);
8786
const localEditorRef = useRef<Editor | null>(null);
8887

@@ -103,12 +102,7 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
103102
}))
104103
);
105104

106-
const {isDraggingDataPill, setDataPillPanelOpen} = useDataPillPanelStore(
107-
useShallow((state) => ({
108-
isDraggingDataPill: state.isDraggingDataPill,
109-
setDataPillPanelOpen: state.setDataPillPanelOpen,
110-
}))
111-
);
105+
const setDataPillPanelOpen = useDataPillPanelStore((state) => state.setDataPillPanelOpen);
112106

113107
const onFocus = (editor: Editor) => {
114108
setFocusedInput(editor);
@@ -159,27 +153,15 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
159153
if (event.dataTransfer.types.includes('application/bytechef-datapill')) {
160154
event.preventDefault();
161155
event.dataTransfer.dropEffect = 'copy';
162-
setIsDragOver(true);
163156
}
164157
}, []);
165158

166159
const handleDragEnter = useCallback((event: DragEvent<HTMLDivElement>) => {
167160
if (event.dataTransfer.types.includes('application/bytechef-datapill')) {
168161
event.preventDefault();
169-
setIsDragOver(true);
170162
}
171163
}, []);
172164

173-
const handleDragLeave = useCallback((event: DragEvent<HTMLDivElement>) => {
174-
if (!event.currentTarget.contains(event.relatedTarget as Node)) {
175-
setIsDragOver(false);
176-
}
177-
}, []);
178-
179-
const handleDrop = useCallback(() => {
180-
setIsDragOver(false);
181-
}, []);
182-
183165
// Ensure localEditorRef stays in sync with parent ref
184166
useEffect(() => {
185167
if (ref && typeof ref !== 'function' && 'current' in ref && ref.current && !localEditorRef.current) {
@@ -210,8 +192,6 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
210192
}
211193
}, [value, defaultValue, setIsFormulaMode]);
212194

213-
const showDropHighlight = isDraggingDataPill && isDragOver;
214-
215195
return (
216196
<fieldset className={twMerge('w-full', label && 'space-y-1')}>
217197
{(label || description || showInputTypeSwitchButton) && (
@@ -273,14 +253,11 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
273253
className={twMerge(
274254
'flex items-center rounded-md border-gray-200 shadow-sm transition-colors',
275255
isFocused && 'ring-2 ring-blue-500',
276-
showDropHighlight && 'bg-success/10 ring-2 ring-success',
277256
label && 'mt-1',
278257
leadingIcon && 'relative rounded-md border'
279258
)}
280259
onDragEnter={handleDragEnter}
281-
onDragLeave={handleDragLeave}
282260
onDragOver={handleDragOver}
283-
onDrop={handleDrop}
284261
title={controlType}
285262
>
286263
{leadingIcon && (

client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputEditor.tsx

Lines changed: 66 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Button from '@/components/Button/Button';
22
import {getClusterElementByName} from '@/pages/platform/cluster-element-editor/utils/clusterElementsUtils';
3+
import {canInsertMentionForProperty} from '@/pages/platform/workflow-editor/components/datapills/DataPill';
34
import PropertyMentionsInputBubbleMenu from '@/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputBubbleMenu';
45
import {getSuggestionOptions} from '@/pages/platform/workflow-editor/components/properties/components/property-mentions-input/propertyMentionsInputEditorSuggestionOptions';
56
import {useWorkflowEditor} from '@/pages/platform/workflow-editor/providers/workflowEditorProvider';
67
import useWorkflowEditorStore from '@/pages/platform/workflow-editor/stores/useWorkflowEditorStore';
78
import useWorkflowNodeDetailsPanelStore from '@/pages/platform/workflow-editor/stores/useWorkflowNodeDetailsPanelStore';
89
import {
9-
canInsertMentionForProperty,
1010
encodeParameters,
1111
encodePath,
1212
escapeHtmlForParagraph,
@@ -466,6 +466,66 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
466466
}
467467
}, []);
468468

469+
const editorRef = useRef<Editor | null>(null);
470+
471+
const handleDrop = useCallback(
472+
(view: EditorView, event: DragEvent, _slice: unknown, moved: boolean): boolean => {
473+
if (moved) {
474+
return false;
475+
}
476+
477+
if (isFromAi) {
478+
return false;
479+
}
480+
481+
const rawPayload = event.dataTransfer?.getData('application/bytechef-datapill');
482+
483+
if (!rawPayload) {
484+
return false;
485+
}
486+
487+
event.preventDefault();
488+
489+
let payload: DataPillDragPayloadType;
490+
491+
try {
492+
payload = JSON.parse(rawPayload);
493+
} catch {
494+
return false;
495+
}
496+
497+
if (!payload?.mentionId) {
498+
return false;
499+
}
500+
501+
const attributes = view.props.attributes as Record<string, string>;
502+
const parameters = currentComponent?.parameters || {};
503+
504+
if (!canInsertMentionForProperty(attributes.type, parameters, attributes.path)) {
505+
return true;
506+
}
507+
508+
const coordinates = view.posAtCoords({
509+
left: event.clientX,
510+
top: event.clientY,
511+
});
512+
513+
const insertPosition = coordinates?.pos ?? view.state.doc.content.size;
514+
515+
editorRef.current
516+
?.chain()
517+
.insertContentAt(insertPosition, {
518+
attrs: {id: payload.mentionId},
519+
type: 'mention',
520+
})
521+
.focus()
522+
.run();
523+
524+
return true;
525+
},
526+
[currentComponent?.parameters, isFromAi]
527+
);
528+
469529
const editor = useEditor({
470530
coreExtensionOptions: {
471531
clipboardTextSerializer: {
@@ -487,60 +547,7 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
487547
type: type ?? '',
488548
},
489549
handleClick: (view, pos) => moveCursorToEnd(view, pos),
490-
handleDrop: (view: EditorView, event: DragEvent, _slice, moved: boolean) => {
491-
if (moved) {
492-
return false;
493-
}
494-
495-
if (isFromAi) {
496-
return false;
497-
}
498-
499-
const rawPayload = event.dataTransfer?.getData('application/bytechef-datapill');
500-
501-
if (!rawPayload) {
502-
return false;
503-
}
504-
505-
event.preventDefault();
506-
507-
let payload: DataPillDragPayloadType;
508-
509-
try {
510-
payload = JSON.parse(rawPayload);
511-
} catch {
512-
return false;
513-
}
514-
515-
if (!payload?.mentionId) {
516-
return false;
517-
}
518-
519-
const attributes = view.props.attributes as Record<string, string>;
520-
const parameters = currentComponent?.parameters || {};
521-
522-
if (!canInsertMentionForProperty(attributes.type, parameters, attributes.path)) {
523-
return true;
524-
}
525-
526-
const coordinates = view.posAtCoords({
527-
left: event.clientX,
528-
top: event.clientY,
529-
});
530-
531-
const insertPosition = coordinates?.pos ?? view.state.doc.content.size;
532-
533-
editor
534-
?.chain()
535-
.insertContentAt(insertPosition, {
536-
attrs: {id: payload.mentionId},
537-
type: 'mention',
538-
})
539-
.focus()
540-
.run();
541-
542-
return true;
543-
},
550+
handleDrop,
544551
handleKeyPress: (editor: EditorView, event: KeyboardEvent) => {
545552
const isEditorEmpty = editor.state.doc.textContent.length === 0;
546553

@@ -577,6 +584,10 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
577584
);
578585

579586
// Sync ref when editor changes - handle both callback and object refs
587+
useEffect(() => {
588+
editorRef.current = editor;
589+
}, [editor]);
590+
580591
useEffect(() => {
581592
if (!ref) {
582593
return;

client/src/pages/platform/workflow-editor/utils/encodingUtils.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
PATH_UNICODE_REPLACEMENT_PREFIX,
1111
} from '@/shared/constants';
1212
import isObject from 'isobject';
13-
import resolvePath from 'object-resolve-path';
1413

1514
interface EncodeParametersGenericProps {
1615
matchPattern: RegExp;
@@ -335,18 +334,3 @@ export function transformValueForObjectAccess(value: string): string {
335334
export function escapeHtmlForParagraph(line: string): string {
336335
return line.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
337336
}
338-
339-
export const canInsertMentionForProperty = (
340-
propertyType: string,
341-
parameters: Record<string, unknown>,
342-
path: string
343-
): boolean => {
344-
if (propertyType === 'STRING') {
345-
return true;
346-
}
347-
348-
const resolvedPath = transformPathForObjectAccess(encodePath(path));
349-
const existingValue = resolvePath(parameters, resolvedPath);
350-
351-
return !existingValue || String(existingValue).startsWith('=');
352-
};

0 commit comments

Comments
 (0)