1+ /**
2+ * @license
3+ * Copyright 2025 Porpoiseful LLC
4+ *
5+ * Licensed under the Apache License, Version 2.0 (the "License");
6+ * you may not use this file except in compliance with the License.
7+ * You may obtain a copy of the License at
8+ *
9+ * https://www.apache.org/licenses/LICENSE-2.0
10+ *
11+ * Unless required by applicable law or agreed to in writing, software
12+ * distributed under the License is distributed on an "AS IS" BASIS,
13+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+ * See the License for the specific language governing permissions and
15+ * limitations under the License.
16+ */
17+
18+ /**
19+ * @author [email protected] (Alan Smith) 20+ */
21+ import { TabType , TabTypeUtils } from "./Tabs" ;
22+ import * as Antd from "antd" ;
23+ import * as I18Next from "react-i18next" ;
24+ import * as React from "react" ;
25+ import * as commonStorage from "../storage/common_storage" ;
26+ import { EditOutlined , DeleteOutlined , CopyOutlined } from '@ant-design/icons' ;
27+ import ModuleNameComponent from "./ModuleNameComponent" ;
28+
29+ type Module = {
30+ path : string ;
31+ title : string ;
32+ type : TabType ;
33+ }
34+
35+ type FileManageModalProps = {
36+ isOpen : boolean ;
37+ onCancel : ( ) => void ;
38+ project : commonStorage . Project | null ;
39+ setProject : ( project : commonStorage . Project | null ) => void ;
40+ setAlertErrorMessage : ( message : string ) => void ;
41+ storage : commonStorage . Storage | null ;
42+ moduleType : TabType ;
43+ }
44+
45+ export default function FileManageModal ( props : FileManageModalProps ) {
46+ const { t } = I18Next . useTranslation ( ) ;
47+ const [ modules , setModules ] = React . useState < Module [ ] > ( [ ] ) ;
48+ const [ loading , setLoading ] = React . useState ( false ) ;
49+ const [ newItemName , setNewItemName ] = React . useState ( '' ) ;
50+ const [ currentRecord , setCurrentRecord ] = React . useState < Module | null > ( null ) ;
51+ const [ renameModalOpen , setRenameModalOpen ] = React . useState ( false ) ;
52+ const [ name , setName ] = React . useState ( '' ) ;
53+ const [ copyModalOpen , setCopyModalOpen ] = React . useState ( false ) ;
54+
55+ React . useEffect ( ( ) => {
56+ if ( props . project && props . moduleType !== null ) {
57+ let moduleList : Module [ ] = [ ] ;
58+
59+ if ( props . moduleType === TabType . MECHANISM ) {
60+ moduleList = props . project . mechanisms . map ( m => ( {
61+ path : m . modulePath ,
62+ title : m . className ,
63+ type : TabType . MECHANISM
64+ } ) ) ;
65+ } else if ( props . moduleType === TabType . OPMODE ) {
66+ moduleList = props . project . opModes . map ( o => ( {
67+ path : o . modulePath ,
68+ title : o . className ,
69+ type : TabType . OPMODE
70+ } ) ) ;
71+ }
72+
73+ // Sort modules alphabetically by title
74+ moduleList . sort ( ( a , b ) => a . title . localeCompare ( b . title ) ) ;
75+
76+ setModules ( moduleList ) ;
77+ } else {
78+ setModules ( [ ] ) ;
79+ }
80+ } , [ props . project , props . moduleType ] ) ;
81+
82+ const handleDelete = async ( module : Module ) => {
83+ if ( props . storage && props . project ) {
84+ setLoading ( true ) ;
85+ try {
86+ await commonStorage . removeModuleFromProject ( props . storage , props . project , module . path ) ;
87+ // Remove from local state
88+ setModules ( modules . filter ( m => m . path !== module . path ) ) ;
89+ } catch ( error ) {
90+ console . error ( 'Error deleting module:' , error ) ;
91+ Antd . message . error ( 'Failed to delete module' ) ;
92+ } finally {
93+ setLoading ( false ) ;
94+ }
95+ }
96+ } ;
97+
98+ const handleRename = async ( origModule : Module , newName : string ) => {
99+ if ( props . storage && props . project ) {
100+ try {
101+ let newPath = await commonStorage . renameModuleInProject (
102+ props . storage ,
103+ props . project ,
104+ newName ,
105+ origModule . path
106+ ) ;
107+ const newModules = modules . map ( ( module ) => {
108+ if ( module . path === origModule . path ) {
109+ return { ...module , title : newName , path : newPath } ;
110+ }
111+ return module ;
112+ } ) ;
113+ setModules ( newModules ) ;
114+ props . setProject ( { ...props . project } ) ;
115+ } catch ( error ) {
116+ console . error ( 'Error renaming module:' , error ) ;
117+ props . setAlertErrorMessage ( 'Failed to rename module' ) ;
118+ }
119+ }
120+ setRenameModalOpen ( false ) ;
121+ } ;
122+ const handleCopy = async ( origModule : Module , newName : string ) => {
123+ if ( props . storage && props . project ) {
124+ try {
125+ let newPath = await commonStorage . copyModuleInProject (
126+ props . storage ,
127+ props . project ,
128+ newName ,
129+ origModule . path
130+ ) ;
131+ const newModules = [ ...modules ] ;
132+
133+ // find the original module to copy its type
134+ const originalModule = modules . find ( module => module . path === origModule . path ) ;
135+ if ( ! originalModule ) {
136+ console . error ( 'Original module not found for copying:' , origModule . path ) ;
137+ props . setAlertErrorMessage ( 'Original module not found for copying' ) ;
138+ return ;
139+ }
140+ // Add the new module with the copied name and type
141+ newModules . push ( { path : newPath , title : newName , type : originalModule . type } ) ;
142+
143+ setModules ( newModules ) ;
144+ props . setProject ( { ...props . project , } ) ;
145+ } catch ( error ) {
146+ console . error ( 'Error copying module:' , error ) ;
147+ props . setAlertErrorMessage ( 'Failed to copy module' ) ;
148+ }
149+ }
150+ setCopyModalOpen ( false ) ;
151+ } ;
152+
153+ const handleAddNewItem = async ( ) => {
154+ let trimmedName = newItemName . trim ( ) ;
155+ if ( trimmedName ) {
156+ /*
157+ if (props.storage && props.project) {
158+ let storage_type = tabType == TabType.MECHANISM ? commonStorage.MODULE_TYPE_MECHANISM : commonStorage.MODULE_TYPE_OPMODE;
159+ await commonStorage.addModuleToProject(props.storage,
160+ props.project, storage_type, trimmedName);
161+ let m = commonStorage.getClassInProject(props.project, trimmedName);
162+ // add the new item to selected items
163+ if (m) {
164+ const module: Module = {
165+ path: m.modulePath,
166+ title: m.className,
167+ type: tabType
168+ };
169+ setSelectedItems([...selectedItems, module]);
170+ }
171+ }
172+ */
173+ setNewItemName ( "" ) ;
174+ // setProject(null); // Reset project to null to trigger re-fetch
175+ }
176+ } ;
177+ const columns : Antd . TableProps < Module > [ 'columns' ] = [
178+ {
179+ title : 'Name' ,
180+ dataIndex : 'title' ,
181+ key : 'title' ,
182+ ellipsis : {
183+ showTitle : false ,
184+ } ,
185+ render : ( title : string ) => (
186+ < Antd . Tooltip title = { title } >
187+ { title }
188+ </ Antd . Tooltip >
189+ ) ,
190+ } ,
191+ {
192+ title : 'Actions' ,
193+ key : 'actions' ,
194+ width : 120 ,
195+ render : ( _ , record : Module ) => (
196+ < Antd . Space size = "small" >
197+ < Antd . Tooltip title = { t ( "Rename" ) } >
198+ < Antd . Button
199+ type = "text"
200+ size = "small"
201+ icon = { < EditOutlined /> }
202+ onClick = { ( ) => {
203+ setCurrentRecord ( record ) ;
204+ setName ( record . title ) ;
205+ setRenameModalOpen ( true ) ;
206+ } }
207+ />
208+ </ Antd . Tooltip >
209+ < Antd . Tooltip title = { t ( "Copy" ) } >
210+ < Antd . Button
211+ type = "text"
212+ size = "small"
213+ icon = { < CopyOutlined /> }
214+ onClick = { ( ) => {
215+ setCurrentRecord ( record ) ;
216+ setName ( record . title + 'Copy' ) ;
217+ setCopyModalOpen ( true ) ;
218+ } }
219+ />
220+ </ Antd . Tooltip >
221+ < Antd . Tooltip title = { t ( "Delete" ) } >
222+ < Antd . Popconfirm
223+ title = { `Delete ${ record . title } ?` }
224+ description = "This action cannot be undone."
225+ onConfirm = { async ( ) => {
226+ const newModules = modules . filter ( m => m . path !== record . path ) ;
227+ setModules ( newModules ) ;
228+
229+ if ( props . storage && props . project ) {
230+ await commonStorage . removeModuleFromProject ( props . storage , props . project , record . path ) ;
231+ props . setProject ( { ...props . project } ) ;
232+ }
233+ } }
234+
235+ okText = { t ( "Delete" ) }
236+ cancelText = { t ( "Cancel" ) }
237+ okType = "danger"
238+
239+ >
240+ < Antd . Button
241+ type = "text"
242+ size = "small"
243+ icon = { < DeleteOutlined /> }
244+ danger
245+ />
246+ </ Antd . Popconfirm >
247+ </ Antd . Tooltip >
248+ </ Antd . Space >
249+ ) ,
250+ } ,
251+ ] ;
252+
253+ const getModalTitle = ( ) => {
254+ if ( props . moduleType === null ) {
255+ return 'Project Management' ;
256+ }
257+ return `${ TabTypeUtils . toString ( props . moduleType ) } Management` ;
258+ } ;
259+
260+ return (
261+ < >
262+ < Antd . Modal
263+ title = { `Rename ${ currentRecord ? TabTypeUtils . toString ( currentRecord . type ) : '' } : ${ currentRecord ? currentRecord . title : '' } ` }
264+ open = { renameModalOpen }
265+ onCancel = { ( ) => setRenameModalOpen ( false ) }
266+ onOk = { ( ) => {
267+ if ( currentRecord ) {
268+ handleRename ( currentRecord , name ) ;
269+ }
270+ } }
271+ okText = { t ( "Rename" ) }
272+ cancelText = { t ( "Cancel" ) }
273+ >
274+ { currentRecord && (
275+ < ModuleNameComponent
276+ tabType = { currentRecord . type }
277+ newItemName = { name }
278+ setNewItemName = { setName }
279+ onAddNewItem = { ( ) => {
280+ if ( currentRecord ) {
281+ handleRename ( currentRecord , name ) ;
282+ }
283+ } }
284+ project = { props . project }
285+ storage = { props . storage }
286+ buttonLabel = ""
287+ />
288+ ) }
289+ </ Antd . Modal >
290+ < Antd . Modal
291+ title = { `Copy ${ currentRecord ? TabTypeUtils . toString ( currentRecord . type ) : '' } : ${ currentRecord ? currentRecord . title : '' } ` }
292+ open = { copyModalOpen }
293+ onCancel = { ( ) => setCopyModalOpen ( false ) }
294+ onOk = { ( ) => {
295+ if ( currentRecord ) {
296+ handleCopy ( currentRecord , name ) ;
297+ }
298+ } }
299+ okText = { t ( "Copy" ) }
300+ cancelText = { t ( "Cancel" ) }
301+ >
302+ { currentRecord && (
303+ < ModuleNameComponent
304+ tabType = { currentRecord . type }
305+ newItemName = { name }
306+ setNewItemName = { setName }
307+ onAddNewItem = { ( ) => {
308+ if ( currentRecord ) {
309+ handleCopy ( currentRecord , name ) ;
310+ }
311+ } }
312+ project = { props . project }
313+ storage = { props . storage }
314+ buttonLabel = ""
315+ />
316+ ) }
317+ </ Antd . Modal >
318+
319+ < Antd . Modal
320+ title = { getModalTitle ( ) }
321+ open = { props . isOpen }
322+ onCancel = { props . onCancel }
323+ footer = { [
324+ < Antd . Button key = "close" onClick = { props . onCancel } >
325+ { t ( "Close" ) }
326+ </ Antd . Button >
327+ ] }
328+ width = { 800 }
329+ >
330+ < div style = { {
331+ marginBottom : 16 ,
332+ border : '1px solid #d9d9d9' ,
333+ borderRadius : '6px' ,
334+ padding : '12px'
335+ } } >
336+ < ModuleNameComponent
337+ tabType = { props . moduleType }
338+ newItemName = { newItemName }
339+ setNewItemName = { setNewItemName }
340+ onAddNewItem = { handleAddNewItem }
341+ project = { props . project }
342+ storage = { props . storage }
343+ buttonLabel = { t ( "New" ) }
344+ />
345+ </ div >
346+ < Antd . Table < Module >
347+ columns = { columns }
348+ dataSource = { modules }
349+ rowKey = "path"
350+ loading = { loading }
351+ size = "small"
352+ pagination = { modules . length > 5 ? {
353+ pageSize : 5 ,
354+ showSizeChanger : false ,
355+ showQuickJumper : false ,
356+ showTotal : ( total , range ) =>
357+ `${ range [ 0 ] } -${ range [ 1 ] } of ${ total } items` ,
358+ } : false }
359+ bordered
360+ locale = { {
361+ emptyText : `No ${ TabTypeUtils . toString ( props . moduleType || TabType . OPMODE ) . toLowerCase ( ) } files found`
362+ } }
363+ />
364+ </ Antd . Modal >
365+ </ >
366+ ) ;
367+ }
0 commit comments