diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 422d1c46..f45f0106 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -9,8 +9,8 @@ "project_rename": "Rename Project", "project_copy": "Copy Project", "fail_list_projects": "Failed to load the list of projects.", - "mechanism": "Mechanism", - "opmode": "OpMode", + "MECHANISM": "Mechanism", + "OPMODE": "OpMode", "class_rule_description": "No spaces are allowed in the name. Each word in the name should start with a capital letter.", "example_mechanism": "For example: GamePieceShooter", "example_opmode": "For example: AutoParkAndShoot", @@ -34,6 +34,11 @@ "HEBREW": "Hebrew", "HELP": "Help", "ABOUT": "About", + "SELECT_HIDDEN": "Select Hidden", + "NO_HIDDEN_MECHANISMS": "No Hidden Mechanisms", + "NO_HIDDEN_OPMODES": "No Hidden Opmodes", + "CREATE_NEW": "Create New", + "CREATE": "Create", "BLOCKS": "Blocks", "CODE": "Code", "COPY": "Copy", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 6622e852..60565328 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -10,8 +10,8 @@ "project_rename": "Renombrar Proyecto", "project_copy": "Copiar Proyecto", "fail_list_projects": "Error al cargar la lista de proyectos.", - "mechanism": "Mecanismo", - "opmode": "OpMode", + "MECHANISM": "Mecanismo", + "OPMODE": "OpMode", "class_rule_description": "No se permiten espacios en el nombre. Cada palabra en el nombre debe comenzar con una letra mayúscula.", "example_mechanism": "Por ejemplo: DisparadorDePiezas", "example_opmode": "Por ejemplo: AutoEstacionarYDisparar", @@ -31,6 +31,11 @@ "HEBREW": "Hebreo", "HELP": "Ayuda", "ABOUT": "Acerca de", + "SELECT_HIDDEN": "Seleccionar Oculto", + "NO_HIDDEN_MECHANISMS": "No Hay Mecanismos Ocultos", + "NO_HIDDEN_OPMODES": "No Hay Opmodes Ocultos", + "CREATE_NEW": "Crear Nuevo", + "CREATE": "Crear", "BLOCKS": "Bloques", "CODE": "Código", "COPY": "Copiar", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 5edd6d82..911fc987 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -9,8 +9,8 @@ "project_rename": "שנה שם פרויקט", "project_copy": "העתק פרויקט", "fail_list_projects": "נכשל בטעינת רשימת הפרויקטים.", - "mechanism": "מנגנון", - "opmode": "אופמוד", + "MECHANISM": "מנגנון", + "OPMODE": "אופמוד", "class_rule_description": "אסור שיהיו רווחים בשם. כל מילה בשם צריכה להתחיל באות גדולה.", "example_mechanism": "לדוגמה: GamePieceShooter", "example_opmode": "לדוגמה: AutoParkAndShoot", @@ -34,6 +34,11 @@ "HEBREW": "עברית", "HELP": "עזרה", "ABOUT": "אודות", + "SELECT_HIDDEN": "בחר נסתר", + "NO_HIDDEN_MECHANISMS": "אין מנגנונים נסתרים", + "NO_HIDDEN_OPMODES": "אין אופמודים נסתרים", + "CREATE_NEW": "צור חדש", + "CREATE": "צור", "BLOCKS": "בלוקים", "CODE": "קוד", "COPY": "העתק", diff --git a/src/reactComponents/AddTabDialog.tsx b/src/reactComponents/AddTabDialog.tsx index 8aa3d642..2e54142e 100644 --- a/src/reactComponents/AddTabDialog.tsx +++ b/src/reactComponents/AddTabDialog.tsx @@ -38,7 +38,7 @@ interface Module { /** Props for the AddTabDialog component. */ interface AddTabDialogProps { isOpen: boolean; - onOk: (newTabs: TabItem[]) => void; + onOk: (newTab: TabItem) => void; onCancel: () => void; project: storageProject.Project | null; setProject: (project: storageProject.Project | null) => void; @@ -48,12 +48,8 @@ interface AddTabDialogProps { /** Height of the scrollable lists in pixels. */ const LIST_HEIGHT = 200; - -/** Maximum width for truncated text in pixels. */ -const MAX_TEXT_WIDTH = 120; - -/** Search input width as a percentage. */ -const SEARCH_INPUT_WIDTH = '70%'; +const ITEM_HEIGHT = 45; +const EMPTY_HEIGHT = 60; /** * Dialog component for adding new tabs to the workspace. @@ -61,18 +57,17 @@ const SEARCH_INPUT_WIDTH = '70%'; */ export default function AddTabDialog(props: AddTabDialogProps) { const {t} = I18Next.useTranslation(); + const { token } = Antd.theme.useToken(); const [tabType, setTabType] = React.useState(TabType.OPMODE); const [availableItems, setAvailableItems] = React.useState([]); - const [selectedItems, setSelectedItems] = React.useState([]); const [newItemName, setNewItemName] = React.useState(''); - const [searchText, setSearchText] = React.useState(''); React.useEffect(() => { if (!props.project) { return; } - // Initialize available items based on project data + // Get all modules of the selected type const mechanisms = props.project.mechanisms.map((m) => ({ path: m.modulePath, title: m.className, @@ -84,48 +79,29 @@ export default function AddTabDialog(props: AddTabDialogProps) { type: TabType.OPMODE, })); - const allItems = [...mechanisms, ...opModes]; - - // Split items into available and selected based on currentTabs - const availableModules = allItems.filter((item) => + // Filter by current tab type and exclude already open tabs + const allItems = tabType === TabType.MECHANISM ? mechanisms : opModes; + const notShownItems = allItems.filter((item) => !props.currentTabs.some((tab) => tab.key === item.path) ); - // Preserve the order from currentTabs for selectedModules - const selectedModules = props.currentTabs - .map((tab) => allItems.find((item) => item.path === tab.key)) - .filter((item) => item !== undefined) as Module[]; + setAvailableItems(notShownItems); + }, [props.project, props.currentTabs, tabType]); - setAvailableItems(availableModules); - setSelectedItems(selectedModules); - }, [props.project, props.currentTabs]); - - const triggerProjectUpdate = (): void => { - if (props.project) { - props.setProject({...props.project}); - } - } + /** Handles selecting an existing module. */ + const handleSelectModule = (item: Module): void => { + const newTab: TabItem = { + key: item.path, + title: item.title, + type: item.type, + }; + props.onOk(newTab); + }; - /** Handles adding a new item or selecting an existing one. */ - const handleAddNewItem = async (): Promise => { + /** Handles creating a new module. */ + const handleCreateNewItem = async (): Promise => { const newClassName = newItemName.trim(); - if (!newClassName) { - return; - } - - // Check if there's an exact match in available items - const matchingItem = availableItems.find((item) => - item.title.toLowerCase() === newClassName.toLowerCase() - ); - - if (matchingItem) { - // Move the matching item to selected - handleSelectItem(matchingItem); - setNewItemName(''); - return; - } - - if (!props.storage || !props.project) { + if (!newClassName || !props.storage || !props.project) { return; } @@ -138,34 +114,14 @@ export default function AddTabDialog(props: AddTabDialogProps) { const newModule = storageProject.findModuleByClassName(props.project, newClassName); if (newModule) { - const module: Module = { - path: newModule.modulePath, + const newTab: TabItem = { + key: newModule.modulePath, title: newModule.className, type: tabType, }; - setSelectedItems([...selectedItems, module]); - triggerProjectUpdate(); + setNewItemName(''); + props.onOk(newTab); } - - - setNewItemName(''); - }; - - /** Moves an item from available to selected list. */ - const handleSelectItem = (item: Module): void => { - const existingItem = selectedItems.find((i) => i.path === item.path); - if (existingItem) { - return; - } - - setSelectedItems([...selectedItems, item]); - setAvailableItems(availableItems.filter((i) => i.path !== item.path)); - }; - - /** Moves an item from selected back to available list. */ - const handleRemoveItem = (item: Module): void => { - setSelectedItems(selectedItems.filter((i) => i !== item)); - setAvailableItems([...availableItems, item]); }; /** Handles radio button change for tab type selection. */ @@ -177,260 +133,77 @@ export default function AddTabDialog(props: AddTabDialogProps) { } }; - /** Handles search input enter key press. */ - const handleSearchEnter = (): void => { - if (filteredAvailableItems.length === 1) { - // If only one item in filtered list, select it - handleSelectItem(filteredAvailableItems[0]); - setSearchText(''); - return; - } - - // If multiple items, look for exact match - const matchingItem = filteredAvailableItems.find((item) => - item.title.toLowerCase() === searchText.trim().toLowerCase() - ); - - if (matchingItem) { - handleSelectItem(matchingItem); - setSearchText(''); - } - }; - - /** Handles drag start for available items. */ - const handleAvailableDragStart = (e: React.DragEvent, item: Module): void => { - e.dataTransfer.setData('application/json', JSON.stringify({ - type: 'available', - item: item, - })); - }; - - /** Handles drag start for selected items. */ - const handleSelectedDragStart = (e: React.DragEvent, index: number): void => { - e.dataTransfer.setData('text/plain', index.toString()); - }; - - /** Handles drop events for the selected items container. */ - const handleSelectedDrop = (e: React.DragEvent): void => { - e.preventDefault(); - try { - const dragData = JSON.parse(e.dataTransfer.getData('application/json')); - if (dragData.type === 'available') { - handleSelectItem(dragData.item); - } - } catch (error) { - // Handle reordering within shown list - const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10); - if (!isNaN(fromIndex)) { - // This is a reorder operation within the shown list - const items = Array.from(selectedItems); - const [reorderedItem] = items.splice(fromIndex, 1); - items.push(reorderedItem); // Add to end - setSelectedItems(items); - } - } - }; - - /** Handles drop events for individual selected items. */ - const handleSelectedItemDrop = (e: React.DragEvent, index: number): void => { - e.preventDefault(); - e.stopPropagation(); - - // First check if it's an item from available list - try { - const dragData = JSON.parse(e.dataTransfer.getData('application/json')); - if (dragData.type === 'available') { - // Insert the available item at this position - const existingItem = selectedItems.find((i) => i.path === dragData.item.path); - if (!existingItem) { - const newItems = Array.from(selectedItems); - newItems.splice(index, 0, dragData.item); - setSelectedItems(newItems); - setAvailableItems(availableItems.filter((i) => i.path !== dragData.item.path)); - } - return; - } - } catch (error) { - // Not JSON data, continue with reordering logic - } - - // Handle reordering within shown list - const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10); - const toIndex = index; - - if (fromIndex !== toIndex && !isNaN(fromIndex)) { - const items = Array.from(selectedItems); - const [reorderedItem] = items.splice(fromIndex, 1); - items.splice(toIndex, 0, reorderedItem); - setSelectedItems(items); - } - }; - - /** Handles the OK button click. */ - const handleOk = (): void => { - const newTabs = selectedItems.map((item) => ({ - key: item.path, - title: item.title, - type: item.type, - })); - props.onOk(newTabs); - }; - - // Filter available items based on search text - const filteredAvailableItems = availableItems - .filter((item) => item.title.toLowerCase().includes(searchText.toLowerCase())); + const getListHeight = (): number => { + return Math.max(EMPTY_HEIGHT, Math.min(LIST_HEIGHT, availableItems.length * ITEM_HEIGHT)); + } return (
+ + + {TabTypeUtils.getIcon(TabType.MECHANISM)} {t('MECHANISM')} + + + {TabTypeUtils.getIcon(TabType.OPMODE)} {t('OPMODE')} + + + +

+ {t('SELECT_HIDDEN')} +

+ ( + handleSelectModule(item)} + style={{cursor: 'pointer'}} + > + + {item.title} + + } + /> + + )} + locale={{emptyText: tabType === TabType.MECHANISM ? t('NO_HIDDEN_MECHANISMS') : t('NO_HIDDEN_OPMODES')}} + /> +

+ {t('CREATE_NEW')} +

+
- - - {TabTypeUtils.getIcon(TabType.MECHANISM)} {t('mechanism')} - - - {TabTypeUtils.getIcon(TabType.OPMODE)} {t('opmode')} - - -
- -
-
-

- {t('Available')} -

- setSearchText(e.target.value)} - onPressEnter={handleSearchEnter} - style={{marginBottom: 8, width: SEARCH_INPUT_WIDTH}} - allowClear - /> - ( - handleAvailableDragStart(e, item)} - actions={[ - handleSelectItem(item)} - > - → - , - ]} - style={{cursor: 'grab'}} - > - - - {item.title} - - - } - /> - - )} - /> -
- -
-

- {t('Shown')} -

-
-
e.preventDefault()} - onDrop={handleSelectedDrop} - > - {selectedItems.map((item, index) => ( -
handleSelectedDragStart(e, index)} - onDragOver={(e) => e.preventDefault()} - onDrop={(e) => handleSelectedItemDrop(e, index)} - style={{ - padding: '8px 12px', - borderBottom: '1px solid #f0f0f0', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - cursor: 'move', - }} - > -
- {TabTypeUtils.getIcon(item.type)} - - - {item.title} - - -
- handleRemoveItem(item)} - > - ✕ - -
- ))} -
-
-
); -} +} \ No newline at end of file diff --git a/src/reactComponents/Tabs.tsx b/src/reactComponents/Tabs.tsx index e4346ed6..60e59080 100644 --- a/src/reactComponents/Tabs.tsx +++ b/src/reactComponents/Tabs.tsx @@ -149,10 +149,10 @@ export function Component(props: TabsProps): React.JSX.Element { }; /** Handles successful addition of new tabs. */ - const handleAddTabOk = (newTabs: TabItem[]): void => { - props.setTabList([props.tabList[0], ...newTabs]); + const handleAddTabOk = (newTab: TabItem): void => { + props.setTabList([...props.tabList, newTab]); - setActiveKey(props.tabList[0].key); + setActiveKey(newTab.key); setAddTabDialogOpen(false); };