From 2cfe5f3e3bd4e6738507c5d450d8c727937cd590 Mon Sep 17 00:00:00 2001 From: gowtham raj j Date: Mon, 27 Jul 2026 11:42:11 +0530 Subject: [PATCH 01/17] Fix dropdown zoom scale in visual editor --- CHANGELOG.md | 6 + web/src/elements/SelectDropdownContext.tsx | 32 +++++ web/src/elements/SelectDropdownProvider.tsx | 25 ++++ web/src/elements/SelectMulti.tsx | 124 +++++++++++------- web/src/elements/SelectSingle.tsx | 83 +++++++++--- web/src/elements/index.ts | 2 + .../components/JoinConditions.tsx | 11 +- .../nodes/ColumnConfigurationNode.tsx | 2 +- .../nodes/ColumnSelectionNode.tsx | 2 +- web/src/features/ModelWizard/DataModeling.tsx | 20 ++- 10 files changed, 230 insertions(+), 77 deletions(-) create mode 100644 web/src/elements/SelectDropdownContext.tsx create mode 100644 web/src/elements/SelectDropdownProvider.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b25d0e..4b93acd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 2.1.0 + +### UX improvements + +- **Data Modeling canvas selects keep dropdown text sized with zoom.** Single- and multi-select menus in the visual editor stay inside the canvas transform so option text matches node chrome when you zoom in or out; truncated labels show the full value on hover. + ## 2.0.1 - **Python-model mapped Airflow tasks use default map indices.** Mapped task instances no longer label themselves with the model id in the Airflow UI (`etl_helper.py` for 2.7 and 2.10). diff --git a/web/src/elements/SelectDropdownContext.tsx b/web/src/elements/SelectDropdownContext.tsx new file mode 100644 index 0000000..588f5d7 --- /dev/null +++ b/web/src/elements/SelectDropdownContext.tsx @@ -0,0 +1,32 @@ +import { createContext, useContext } from 'react'; + +/** + * How a select dropdown is positioned relative to its trigger. + * + * - `anchored` — body-portaled menu at screen-native text size (forms, dialogs). + * - `inline` — body-portaled menu (so option clicks are never trapped by React + * Flow node stacking) with option text sized to the canvas zoom via fontSize. + */ +export type SelectDropdownPlacement = 'anchored' | 'inline'; + +export type SelectDropdownContextValue = { + placement: SelectDropdownPlacement; + /** React Flow viewport zoom; `1` off-canvas. */ + zoom: number; +}; + +export const SelectDropdownContext = + createContext(null); + +/** Resolves prop override → canvas/context default → anchored. */ +export function useSelectDropdownPlacement( + prop?: SelectDropdownPlacement, +): SelectDropdownPlacement { + const fromContext = useContext(SelectDropdownContext); + return prop ?? fromContext?.placement ?? 'anchored'; +} + +/** Canvas zoom from context, or `1` when not under a provider. */ +export function useSelectDropdownZoom(): number { + return useContext(SelectDropdownContext)?.zoom ?? 1; +} diff --git a/web/src/elements/SelectDropdownProvider.tsx b/web/src/elements/SelectDropdownProvider.tsx new file mode 100644 index 0000000..4652070 --- /dev/null +++ b/web/src/elements/SelectDropdownProvider.tsx @@ -0,0 +1,25 @@ +import React, { useMemo } from 'react'; + +import { + SelectDropdownContext, + type SelectDropdownPlacement, +} from './SelectDropdownContext'; + +export function SelectDropdownProvider({ + placement, + zoom = 1, + children, +}: { + placement: SelectDropdownPlacement; + /** React Flow viewport zoom; omit (defaults to 1) off-canvas. */ + zoom?: number; + children: React.ReactNode; +}) { + const value = useMemo(() => ({ placement, zoom }), [placement, zoom]); + + return ( + + {children} + + ); +} diff --git a/web/src/elements/SelectMulti.tsx b/web/src/elements/SelectMulti.tsx index a4e3391..b087cc0 100644 --- a/web/src/elements/SelectMulti.tsx +++ b/web/src/elements/SelectMulti.tsx @@ -3,6 +3,11 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { InputText } from './InputText'; +import { + type SelectDropdownPlacement, + useSelectDropdownPlacement, + useSelectDropdownZoom, +} from './SelectDropdownContext'; export interface SelectMultiOption { value: string; @@ -17,6 +22,11 @@ export interface SelectMultiProps { className?: string; searchable?: boolean; showSelectedTags?: boolean; + /** + * Dropdown positioning. Defaults to context (canvas sets `inline`) or + * `anchored`. Prefer leaving unset and using `SelectDropdownProvider`. + */ + dropdownPlacement?: SelectDropdownPlacement; } export const SelectMulti: React.FC = ({ @@ -27,7 +37,11 @@ export const SelectMulti: React.FC = ({ className = '', searchable = false, showSelectedTags = true, + dropdownPlacement: dropdownPlacementProp, }) => { + const dropdownPlacement = useSelectDropdownPlacement(dropdownPlacementProp); + const isCanvas = dropdownPlacement === 'inline'; + const canvasZoom = useSelectDropdownZoom(); const [isOpen, setIsOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [dropdownPos, setDropdownPos] = useState<{ @@ -98,8 +112,62 @@ export const SelectMulti: React.FC = ({ ) : options; + const menuFontSize = isCanvas ? `${0.875 * canvasZoom}rem` : '0.875rem'; + + const menuInner = ( + <> + {searchable && ( +
+ setSearchQuery(e.target.value)} + onClick={(e) => e.stopPropagation()} + inputClassName="!mt-0" + /> +
+ )} +
+ {filteredOptions.length === 0 ? ( +
+ {searchQuery ? 'No options found' : 'No options available'} +
+ ) : ( + filteredOptions.map((option) => ( +
{ + e.stopPropagation(); + toggleOption(option.value); + }} + > + {}} + className="w-4 h-4 rounded border-gray-300" + /> + + {option.label} + +
+ )) + )} +
+ + ); + return ( -
+
{/* Trigger */}
= ({ {label}