diff --git a/src/reactComponents/FileManageModal.tsx b/src/reactComponents/FileManageModal.tsx index fd6bc286..cc44bbdc 100644 --- a/src/reactComponents/FileManageModal.tsx +++ b/src/reactComponents/FileManageModal.tsx @@ -25,13 +25,13 @@ import * as React from 'react'; import * as commonStorage from '../storage/common_storage'; import * as storageModule from '../storage/module'; import * as storageProject from '../storage/project'; -import {EditOutlined, DeleteOutlined, CopyOutlined, SelectOutlined} from '@ant-design/icons'; import ClassNameComponent from './ClassNameComponent'; +import ManageTable from './ManageTable'; /** Represents a module in the file management system. */ interface Module { path: string; - title: string; + name: string; type: TabType; } @@ -47,15 +47,9 @@ interface FileManageModalProps { tabType: TabType; } -/** Default page size for table pagination. */ -const DEFAULT_PAGE_SIZE = 5; - /** Modal width in pixels. */ const MODAL_WIDTH = 800; -/** Actions column width in pixels. */ -const ACTIONS_COLUMN_WIDTH = 160; - /** * Modal component for managing files (mechanisms and opmodes) within a project. * Provides functionality to create, rename, copy, and delete modules. @@ -86,19 +80,19 @@ export default function FileManageModal(props: FileManageModalProps) { if (props.tabType === TabType.MECHANISM) { moduleList = props.project.mechanisms.map((m) => ({ path: m.modulePath, - title: m.className, + name: m.className, type: TabType.MECHANISM, })); } else if (props.tabType === TabType.OPMODE) { moduleList = props.project.opModes.map((o) => ({ path: o.modulePath, - title: o.className, + name: o.className, type: TabType.OPMODE, })); } - // Sort modules alphabetically by title - moduleList.sort((a, b) => a.title.localeCompare(b.title)); + // Sort modules alphabetically by name + moduleList.sort((a, b) => a.name.localeCompare(b.name)); setModules(moduleList); }, [props.project, props.tabType]); @@ -163,7 +157,7 @@ export default function FileManageModal(props: FileManageModalProps) { const newModule = { path: newModulePath, - title: newClassName, + name: newClassName, type: originalModule.type, }; @@ -208,7 +202,7 @@ export default function FileManageModal(props: FileManageModalProps) { if (newModule) { const module: Module = { path: newModule.modulePath, - title: newModule.className, + name: newModule.className, type: props.tabType, }; setModules([...modules, module]); @@ -237,13 +231,8 @@ export default function FileManageModal(props: FileManageModalProps) { } }; - /** Handles button click events to prevent row selection. */ - const handleButtonClick = (e: React.MouseEvent): void => { - e.stopPropagation(); - }; - - /** Handles row double-click to open module in tab. */ - const handleRowDoubleClick = (record: Module): void => { + /** Handles selection to open module in tab. */ + const handleSelect = (record: Module): void => { props.gotoTab(record.path); props.onClose(); }; @@ -251,91 +240,17 @@ export default function FileManageModal(props: FileManageModalProps) { /** Opens the rename modal for a specific module. */ const openRenameModal = (record: Module): void => { setCurrentRecord(record); - setName(record.title); + setName(record.name); setRenameModalOpen(true); }; /** Opens the copy modal for a specific module. */ const openCopyModal = (record: Module): void => { setCurrentRecord(record); - setName(t('COPY_SUFFIX', { name: record.title })); + setName(t('COPY_SUFFIX', { name: record.name })); setCopyModalOpen(true); }; - /** Table column configuration. */ - const columns: Antd.TableProps['columns'] = [ - { - title: t('NAME'), - dataIndex: 'title', - key: 'title', - ellipsis: { - showTitle: false, - }, - render: (title: string) => ( - - {title} - - ), - }, - { - title: t('ACTIONS'), - key: 'actions', - width: ACTIONS_COLUMN_WIDTH, - render: (_, record: Module) => ( - - - } - onClick={() => handleRowDoubleClick(record)} - /> - - - } - onClick={(e) => { - handleButtonClick(e); - openRenameModal(record); - }} - /> - - - } - onClick={(e) => { - handleButtonClick(e); - openCopyModal(record); - }} - /> - - - handleDeleteConfirm(record)} - okText={t('Delete')} - cancelText={t('Cancel')} - okType="danger" - > - } - danger - onClick={handleButtonClick} - /> - - - - ), - }, - ]; - /** Gets the modal title based on module type. */ const getModalTitle = (): string => { return t('TYPE_MANAGEMENT', { type: TabTypeUtils.toString(props.tabType) }); @@ -348,7 +263,7 @@ export default function FileManageModal(props: FileManageModalProps) { } return t('RENAME_TYPE_TITLE', { type: TabTypeUtils.toString(currentRecord.type), - title: currentRecord.title + title: currentRecord.name }); }; @@ -359,7 +274,7 @@ export default function FileManageModal(props: FileManageModalProps) { } return t('COPY_TYPE_TITLE', { type: TabTypeUtils.toString(currentRecord.type), - title: currentRecord.title + title: currentRecord.name }); }; @@ -369,6 +284,14 @@ export default function FileManageModal(props: FileManageModalProps) { return t('NO_FILES_FOUND', { type: tabTypeString.toLowerCase() }); }; + const getModuleFromName = (name: string): Module => { + const module = modules.find((m) => m.name === name); + if (!module) { + throw new Error('Module not found for name: ' + name); + } + return module; + } + return ( <> - - columns={columns} - dataSource={modules} - rowKey="path" - size="small" - pagination={modules.length > DEFAULT_PAGE_SIZE ? { - pageSize: DEFAULT_PAGE_SIZE, - showSizeChanger: false, - showQuickJumper: false, - showTotal: (total, range) => - t('PAGINATION_TOTAL', { start: range[0], end: range[1], total }), - } : false} - bordered - locale={{ - emptyText: getEmptyText(), - }} - onRow={(record) => ({ - onDoubleClick: () => handleRowDoubleClick(record), - })} - /> -
+ handleSelect(getModuleFromName(record.name))} + onRename={(record) => openRenameModal(getModuleFromName(record.name))} + onCopy={(record) => openCopyModal(getModuleFromName(record.name))} + onDelete={(record) => handleDeleteConfirm(getModuleFromName(record.name))} + /> +

{t('CREATE_NEW', { type: TabTypeUtils.toString(props.tabType) })}

diff --git a/src/reactComponents/ManageTable.tsx b/src/reactComponents/ManageTable.tsx new file mode 100644 index 00000000..512a1d50 --- /dev/null +++ b/src/reactComponents/ManageTable.tsx @@ -0,0 +1,143 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Antd from 'antd'; +import * as I18Next from 'react-i18next'; +import * as React from 'react'; + +import {EditOutlined, DeleteOutlined, CopyOutlined, SelectOutlined} from '@ant-design/icons'; + +interface ManageTableRecord { + name: string, +} + +interface ManageTableProps { + textOnEmpty: string, + records: ManageTableRecord[], + showDelete: boolean, + deleteDialogTitle: string, + onSelect: (record: ManageTableRecord) => void, + onRename: (record: ManageTableRecord) => void, + onCopy: (record: ManageTableRecord) => void, + onDelete: (record: ManageTableRecord) => void, +} + +/** Default page size for table pagination. */ +const DEFAULT_PAGE_SIZE = 5; + +/** Actions column width in pixels. */ +const ACTIONS_COLUMN_WIDTH = 160; + +export default function ManageTable(props: ManageTableProps): React.JSX.Element { + const { t } = I18Next.useTranslation(); + + /** Table column configuration. */ + const columns: Antd.TableProps['columns'] = [ + { + title: t('NAME'), + dataIndex: 'name', + key: 'name', + ellipsis: { + showTitle: false, + }, + render: (name: string) => ( + + {name} + + ), + }, + { + title: t('ACTIONS'), + key: 'actions', + width: ACTIONS_COLUMN_WIDTH, + render: (_, record: ManageTableRecord) => ( + + + } + onClick={() => props.onSelect(record)} + /> + + + } + onClick={() => props.onRename(record)} + /> + + + } + onClick={() => props.onCopy(record)} + /> + + {props.showDelete && ( + + props.onDelete(record)} + okText={t('Delete')} + cancelText={t('Cancel')} + okType="danger" + > + } + danger + /> + + + )} + + ), + }, + ]; + + + return ( + + columns={columns} + dataSource={props.records} + rowKey="name" + size="small" + pagination={props.records.length > DEFAULT_PAGE_SIZE ? { + pageSize: DEFAULT_PAGE_SIZE, + showSizeChanger: false, + showQuickJumper: false, + showTotal: (total, range) => + t('PAGINATION_ITEMS', { range0: range[0], range1: range[1], total }), + } : false} + bordered + locale={{ + emptyText: props.textOnEmpty, + }} + onRow={(record) => ({ + onDoubleClick: () => props.onSelect(record), + })} + /> + ); +} \ No newline at end of file diff --git a/src/reactComponents/ProjectManageModal.tsx b/src/reactComponents/ProjectManageModal.tsx index 1d7ff85a..61114f10 100644 --- a/src/reactComponents/ProjectManageModal.tsx +++ b/src/reactComponents/ProjectManageModal.tsx @@ -23,8 +23,8 @@ import * as I18Next from 'react-i18next'; import * as React from 'react'; import * as commonStorage from '../storage/common_storage'; import * as storageProject from '../storage/project'; -import {EditOutlined, DeleteOutlined, CopyOutlined, SelectOutlined} from '@ant-design/icons'; import ProjectNameComponent from './ProjectNameComponent'; +import ManageTable from './ManageTable'; /** Props for the ProjectManageModal component. */ interface ProjectManageModalProps { @@ -37,18 +37,12 @@ interface ProjectManageModalProps { } type ProjectRecord = { - projectName: string, + name: string, }; -/** Default page size for table pagination. */ -const DEFAULT_PAGE_SIZE = 5; - /** Modal width in pixels. */ const MODAL_WIDTH = 800; -/** Actions column width in pixels. */ -const ACTIONS_COLUMN_WIDTH = 160; - /** Default copy suffix for project names. */ const COPY_SUFFIX = 'Copy'; @@ -85,7 +79,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac const projectRecords: ProjectRecord[] = []; projectNames.forEach(projectName => { projectRecords.push({ - projectName: projectName, + name: projectName, }); }); setAllProjectRecords(projectRecords); @@ -159,24 +153,24 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac setNewItemName(''); await loadProjectNames(props.storage); - handleSelectProject(newProjectName); + handleSelectProject({ name: newProjectName }); }; /** Handles project deletion with proper cleanup. */ - const handleDeleteProject = async (projectNameToDelete: string): Promise => { + const handleDeleteProject = async (project: ProjectRecord): Promise => { if (!props.storage) { return; } - const updatedProjectNames = allProjectNames.filter(projectName => projectName !== projectNameToDelete); + const updatedProjectNames = allProjectNames.filter(projectName => projectName !== project.name); setAllProjectNames(updatedProjectNames); - const updatedProjectRecords = allProjectRecords.filter((r) => r.projectName !== projectNameToDelete); + const updatedProjectRecords = allProjectRecords.filter((r) => r.name !== project.name); setAllProjectRecords(updatedProjectRecords); // Find another project to set as current let foundAnotherProject = false; for (const projectName of allProjectNames) { - if (projectName !== projectNameToDelete) { + if (projectName !== project.name) { const project = await storageProject.fetchProject(props.storage, projectName); props.setProject(project); foundAnotherProject = true; @@ -189,7 +183,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac } try { - await storageProject.deleteProject(props.storage, projectNameToDelete); + await storageProject.deleteProject(props.storage, project.name); } catch (e) { console.error('Failed to delete the project:', e); props.setAlertErrorMessage(t('FAILED_TO_DELETE_PROJECT')); @@ -197,12 +191,12 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac }; /** Handles project selection. */ - const handleSelectProject = async (projectName: string): Promise => { + const handleSelectProject = async (projectRecord: ProjectRecord): Promise => { if (!props.storage) { return; } - const project = await storageProject.fetchProject(props.storage, projectName); + const project = await storageProject.fetchProject(props.storage, projectRecord.name); props.setProject(project); props.onCancel(); }; @@ -210,25 +204,25 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac /** Opens the rename modal for a specific project. */ const openRenameModal = (record: ProjectRecord): void => { setCurrentRecord(record); - setName(record.projectName); + setName(record.name); setRenameModalOpen(true); }; /** Opens the copy modal for a specific project. */ const openCopyModal = (record: ProjectRecord): void => { setCurrentRecord(record); - setName(record.projectName + COPY_SUFFIX); + setName(record.name + COPY_SUFFIX); setCopyModalOpen(true); }; /** Gets the rename modal title. */ const getRenameModalTitle = (): string => { - return `${t('RENAME_PROJECT')}: ${currentRecord ? currentRecord.projectName : ''}`; + return `${t('RENAME_PROJECT')}: ${currentRecord ? currentRecord.name : ''}`; }; /** Gets the copy modal title. */ const getCopyModalTitle = (): string => { - return `${t('COPY_PROJECT')}: ${currentRecord ? currentRecord.projectName : ''}`; + return `${t('COPY_PROJECT')}: ${currentRecord ? currentRecord.name : ''}`; }; /** Creates the container style object. */ @@ -244,75 +238,6 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac marginBottom: ALERT_MARGIN_BOTTOM, }); - /** Table column configuration. */ - const columns: Antd.TableProps['columns'] = [ - { - title: t('NAME'), - dataIndex: 'projectName', - key: 'projectName', - ellipsis: { - showTitle: false, - }, - render: (className: string) => ( - - {className} - - ), - }, - { - title: t('ACTIONS'), - key: 'actions', - width: ACTIONS_COLUMN_WIDTH, - render: (_, record: ProjectRecord) => ( - - - } - onClick={() => handleSelectProject(record.projectName)} - /> - - - } - onClick={() => openRenameModal(record)} - /> - - - } - onClick={() => openCopyModal(record)} - /> - - {allProjectRecords.length > 1 && ( - - handleDeleteProject(record.projectName)} - okText={t('Delete')} - cancelText={t('Cancel')} - okType="danger" - > - } - danger - /> - - - )} - - ), - }, - ]; - // Load project names when storage becomes available or modal opens React.useEffect(() => { if (props.storage) { @@ -328,7 +253,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac onCancel={() => setRenameModalOpen(false)} onOk={() => { if (currentRecord) { - handleRename(currentRecord.projectName, name); + handleRename(currentRecord.name, name); } }} okText={t('Rename')} @@ -340,7 +265,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac setNewItemName={setName} onAddNewItem={() => { if (currentRecord) { - handleRename(currentRecord.projectName, name); + handleRename(currentRecord.name, name); } }} projectNames={allProjectNames} @@ -354,7 +279,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac onCancel={() => setCopyModalOpen(false)} onOk={() => { if (currentRecord) { - handleCopy(currentRecord.projectName, name); + handleCopy(currentRecord.name, name); } }} okText={t('Copy')} @@ -366,7 +291,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac setNewItemName={setName} onAddNewItem={() => { if (currentRecord) { - handleCopy(currentRecord.projectName, name); + handleCopy(currentRecord.name, name); } }} projectNames={allProjectNames} @@ -391,25 +316,15 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac /> )} {!props.noProjects && ( - - columns={columns} - dataSource={allProjectRecords} - rowKey="projectName" - size="small" - pagination={allProjectRecords.length > DEFAULT_PAGE_SIZE ? { - pageSize: DEFAULT_PAGE_SIZE, - showSizeChanger: false, - showQuickJumper: false, - showTotal: (total, range) => - t('PAGINATION_ITEMS', { range0: range[0], range1: range[1], total }), - } : false} - bordered - locale={{ - emptyText: t('NO_PROJECTS_FOUND'), - }} - onRow={(record) => ({ - onDoubleClick: () => handleSelectProject(record.projectName), - })} + 1} + deleteDialogTitle="DELETE_PROJECT_CONFIRM" + onSelect={(record) => handleSelectProject(record)} + onRename={(record) => openRenameModal(record)} + onCopy={(record) => openCopyModal(record)} + onDelete={(record) => handleDeleteProject(record)} /> )}