diff --git a/app/Routes.js b/app/Routes.js index d989106e..5e8cc2c7 100644 --- a/app/Routes.js +++ b/app/Routes.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { HashRouter, Routes, Route } from 'react-router-dom'; import routes from './constants/routes.json'; import App from './containers/App'; @@ -6,6 +6,18 @@ import ProjectPage from './containers/ProjectPage/ProjectPage'; import ConfigurationPage from './containers/ConfigurationPage/ConfigurationPage'; import SearchPage from './containers/SearchPage/SearchPage'; import AboutPage from './containers/AboutPage/AboutPage'; +import ProjectContext from './contexts/Project'; + +const ProjectPageWithContext = (props) => { + const { selectedProjectId, onSelectProject } = useContext(ProjectContext); + return ( + + ); +}; export default function AppRoutes() { return ( @@ -15,7 +27,7 @@ export default function AppRoutes() { } /> } /> } /> - } /> + } /> diff --git a/app/containers/App.js b/app/containers/App.js index 033e053e..85ce76db 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -25,6 +25,7 @@ import Messages from '../constants/messages'; import routes from '../constants/routes.json'; import UserContext from '../contexts/User'; import SettingsContext from '../contexts/Settings'; +import ProjectContext from '../contexts/Project'; import UserProfileDialog from './UserProfileDialog/UserProfileDialog'; import styles from './App.css'; import GeneralUtil from '../utils/general'; @@ -54,6 +55,7 @@ export default class App extends React.Component { settings: {}, displayUserProfileDialog: false, userProfileDialogKey: 0, + selectedProjectId: null, }; this.handleLoadUserInfoResponse = this.handleLoadUserInfoResponse.bind(this); @@ -62,6 +64,7 @@ export default class App extends React.Component { this.handleCloseUserProfileDialog = this.handleCloseUserProfileDialog.bind(this); this.handleOpenUserProfileDialog = this.handleOpenUserProfileDialog.bind(this); this.handleSaveUserProfile = this.handleSaveUserProfile.bind(this); + this.handleSelectProject = this.handleSelectProject.bind(this); } componentDidMount() { @@ -104,7 +107,7 @@ export default class App extends React.Component { handleUpdateSearchSettingsResponse(sender, response) { this.setState((prevState) => ({ - settings: {...prevState.settings, searchSettings: response.searchSettings } + settings: { ...prevState.settings, searchSettings: response.searchSettings } })); } @@ -129,6 +132,10 @@ export default class App extends React.Component { this.setState({ displayUserProfileDialog: true }); } + handleSelectProject(id) { + this.setState({ selectedProjectId: id }); + } + render() { const { children } = this.props; const theme = createTheme({ @@ -180,40 +187,47 @@ export default class App extends React.Component { - - - StatWrap logo -
- - - - - - - - - - - - - - - - - - - - - -
-
-
- {children} - {userProfileDialog} + + + + StatWrap logo +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ {children} + {userProfileDialog} +
diff --git a/app/containers/ProjectPage/ProjectPage.js b/app/containers/ProjectPage/ProjectPage.js index 0bbb89cd..36efa827 100644 --- a/app/containers/ProjectPage/ProjectPage.js +++ b/app/containers/ProjectPage/ProjectPage.js @@ -11,6 +11,14 @@ import UserContext from '../../contexts/User'; import Messages from '../../constants/messages'; import ChecklistUtil from '../../utils/checklist'; import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels'; +import { + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Button, +} from '@mui/material'; class ProjectPage extends Component { constructor(props) { @@ -54,7 +62,13 @@ class ProjectPage extends Component { // Show progress on scanning details about the project. Allows the user to access what information // is available, yet be aware that more information may be arriving later. projectScanStatus: '', + isProjectDirty: false, + showDirtyConfirmation: false, + pendingProject: null, }; + this.handleProjectDirtyStateChange = this.handleProjectDirtyStateChange.bind(this); + this.handleDiscardChanges = this.handleDiscardChanges.bind(this); + this.handleCancelSwitch = this.handleCancelSwitch.bind(this); this.handleLoadProjectListResponse = this.handleLoadProjectListResponse.bind(this); this.refreshProjectsHandler = this.refreshProjectsHandler.bind(this); @@ -183,7 +197,18 @@ class ProjectPage extends Component { } handleLoadProjectListResponse(sender, response) { - this.setState({ ...response, loaded: true }); + this.setState({ ...response, loaded: true }, () => { + if ( + this.props.selectedProjectId && + (!this.state.selectedProject || + this.state.selectedProject.id !== this.props.selectedProjectId) + ) { + const project = this.state.projects.find((p) => p.id === this.props.selectedProjectId); + if (project) { + this.loadProject(project); + } + } + }); } handleLoadConfigurationResponse(sender, response) { @@ -394,6 +419,9 @@ class ProjectPage extends Component { case Messages.REMOVE_PROJECT_LIST_ENTRY_REQUEST: ipcRenderer.send(Messages.REMOVE_PROJECT_LIST_ENTRY_REQUEST, projectId); break; + case Messages.SHOW_ITEM_IN_FOLDER: + ipcRenderer.send(Messages.SHOW_ITEM_IN_FOLDER, projectId); + break; default: console.warn(`Unknown project list entry menu event: ${event}`); } @@ -404,9 +432,32 @@ class ProjectPage extends Component { // Handle case where user clicks off of all projects (project is null) if (!project) { this.setState({ selectedProject: null }); + if (this.props.onSelectProject) { + this.props.onSelectProject(null); + } + return; + } + + // If the user clicks the project that is already selected, ignore it. + if (this.state.selectedProject && this.state.selectedProject.id === project.id) { return; } + if (this.state.isProjectDirty && this.state.selectedProject && this.state.selectedProject.id !== project.id) { + this.setState({ + showDirtyConfirmation: true, + pendingProject: project + }); + return; + } + + if (this.props.onSelectProject) { + this.props.onSelectProject(project.id); + } + this.loadProject(project); + } + + loadProject(project) { // If the project is offline (has loadError), try to refresh its status if (project.loadError) { // First update the UI to show we're trying to reconnect @@ -446,6 +497,28 @@ class ProjectPage extends Component { } } + handleProjectDirtyStateChange(isDirty) { + this.setState({ isProjectDirty: isDirty }); + } + + handleDiscardChanges() { + this.setState({ + showDirtyConfirmation: false, + isProjectDirty: false + }, () => { + if (this.state.pendingProject) { + this.loadProject(this.state.pendingProject); + } + }); + } + + handleCancelSwitch() { + this.setState({ + showDirtyConfirmation: false, + pendingProject: null + }); + } + handleProjectUpdate(project, actionType, entityType, entityKey, title, description, details) { // Update our cached list of projects from which we get the selected projects. We want to ensure // these are kept in sync with any updates. @@ -534,6 +607,7 @@ class ProjectPage extends Component { configuration={{ assetAttributes: this.state.assetAttributes }} assetDynamicDetails={this.state.assetDynamicDetails} scanStatus={this.state.projectScanStatus} + onDirtyStateChange={this.handleProjectDirtyStateChange} /> @@ -547,12 +621,28 @@ class ProjectPage extends Component { anchorElement={ this.state.projectListMenuAnchor ? this.state.projectListMenuAnchor.element : null } - project={ - this.state.projectListMenuAnchor ? this.state.projectListMenuAnchor.project : null - } onClose={this.handleCloseProjectListMenu} onMenuClick={this.handleClickProjectListMenu} /> + + Discard Changes? + + + You have unsaved changes in the current project. Switching to another project will discard these changes. Are you sure you want to proceed? + + + + + + + ); } diff --git a/app/contexts/Project.js b/app/contexts/Project.js new file mode 100644 index 00000000..09a62fa7 --- /dev/null +++ b/app/contexts/Project.js @@ -0,0 +1,4 @@ +import { createContext } from 'react'; + +const ProjectContext = createContext(); +export default ProjectContext;