import React, {Component} from 'react';
import { exportProject, fetchProject, fetchProjects, importProject } from "../Utils/utilFunctions/fetchFunctions";
import Header from "../Header/Header";
import { ProjectMenu } from "../Header/Elements";
import Help from '../Body/Help/Help';
import Home from "../Body/Home/Home";
import Import from "../Body/Import/Import";
import ProjectTabs from "../Body/Project/ProjectTabs";
import Project from "../Utils/Classes/Project";
import LoadingDelay from "../Utils/Feedback/LoadingDelay";
import LoadingSnackbar from "../Utils/Feedback/LoadingSnackbar";
import StyledAlert from "../Utils/Feedback/StyledAlert";
import DeleteProjectDialog from "../Utils/Dialogs/DeleteProjectDialog";
import ImportProjectDialog from "../Utils/Dialogs/ImportProjectDialog";
import RenameProjectDialog from "../Utils/Dialogs/RenameProjectDialog";
import SettingsProjectDialog from "../Utils/Dialogs/SettingsProjectDialog";
import { DarkTheme, LightTheme } from "../Utils/Themes/Themes";
import CssBaseline from "@material-ui/core/CssBaseline";
import {MuiThemeProvider} from "@material-ui/core/styles";
import { Switch, Route } from 'react-router-dom';
import { tabNames } from "../Utils/Constants/TabsNamesInPath";
/**
* <h3>Overview</h3>
* The main component that contains all other elements.
* Provides two themes: dark and light.
*
* @constructor
*/
class App extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
loadingTitle: "",
deleting: false,
body: "Home",
currentProject: -1,
projects: [],
objectGlobalName: null,
darkTheme: true,
serverBase: "http://localhost:8080",
open: {
deleteDialog: false,
importDialog: false,
renameDialog: false,
settingsDialog: false
},
alertProps: undefined
};
this.appBarRef = React.createRef();
this.existsPath = "";
}
/**
* <h3>Overview</h3>
* A component's lifecycle method. Fired once when component was mounted.
*
* <h3>Goal</h3>
* Makes an API call on projects to receive the latest list of all projects.
* Then, updates states and makes necessary changes in display.
*
* @function
* @memberOf App
*/
componentDidMount() {
const base = window.location.origin.toString();
this.setState({
loading: true,
loadingTitle: "Loading projects",
}, () => {
fetchProjects(
"GET", null, base
).then(result => {
if (Array.isArray(result)) {
this.setState({
projects: result.map(item => new Project(item.id, item.name))
}, () => {this.checkIfProjectInURLExists()});
} else {
this.setState({
alertProps: {
message: "Server isn't responding :(",
open: true,
severity: "error"
}
});
}
}).catch(
this.onSnackbarOpen
).finally(() => {
this.setState({
loading: false,
serverBase: base
});
});
});
this.props.history.listen((location, action) => {
if (action === "POP") {
if (location.state && location.state.bodyName) {
if (location.state.bodyName === "Project") {
if (location.state.projectIndex >= 0 && location.state.projectIndex < this.state.projects.length) {
this.existsPath = `${this.state.projects[location.state.projectIndex].id}`;
this.setState({
body: location.state.bodyName,
currentProject: location.state.projectIndex
});
} else {
this.setState({
body: "Import",
currentProject: -1
});
this.props.history.replace({
pathname: "/newProject",
state: { bodyName: "Import" }
});
}
} else {
this.setState({
body: location.state.bodyName,
currentProject: -1
});
}
} else if (location.state) {
let i;
for (i = 0; i < this.state.projects.length; i++) {
if (this.state.projects[i].id === location.state.projectId) {
break;
}
}
if (i < this.state.projects.length) {
this.existsPath = `${location.state.projectId}`;
this.setState({
body: "Project",
currentProject: i
});
} else {
this.setState({
body: "Import",
currentProject: -1
});
this.props.history.replace({
pathname: "/newProject",
state: { bodyName: "Import" }
});
}
}
}
});
};
/**
* <h3>Overview</h3>
* Method is forwarded to the {@link ProjectTabs} and further to all tabs except {@link Data}.
* Saves changes from provided project in the {@link App}'s state and updates index options.
*
* @function
* @memberOf App
* @param {Object} project - Project with unsaved changes.
*/
updateProject = (project) => {
this.setState(({projects}) => {
if (projects.length > 0) {
let index = -1;
for (let i = 0; i < projects.length; i++) {
if (projects[i].id === project.id) {
index = i;
break;
}
}
if (index === -1) {
return { projects: projects };
}
return {
projects: [
...projects.slice(0, index),
{
...projects[index],
...project
},
...projects.slice(index + 1)
]
};
} else {
return { projects: projects };
}
});
};
onBodyChange = (name) => {
this.setState(({currentProject}) => ({
body: name,
currentProject: name !== "Project" ? -1 : currentProject
}));
switch (name) {
case "Help":
this.props.history.push({
pathname: "/help",
state: { bodyName: name }
});
break;
case "Import":
this.props.history.push({
pathname: "/newProject",
state: { bodyName: name }
});
break;
default:
this.props.history.push({
pathname: "/home",
state: { bodyName: "Home" }
});
break;
}
};
onCurrentProjectChange = (index, tabName) => {
const { projects } = this.state;
if (tabName !== "" && tabNames.includes(tabName)) {
this.props.history.push({
pathname: `/${projects[index].id}/${tabName}`,
state: { bodyName: "Project", projectIndex: index }
});
} else {
this.props.history.push({
pathname: `/${projects[index].id}/${tabNames[0]}`,
state: { bodyName: "Project", projectIndex: index }
});
}
this.existsPath = `${projects[index].id}`;
this.setState({
body: "Project",
currentProject: index
});
};
onColorsChange = () => {
this.setState(prevState => ({
darkTheme: !prevState.darkTheme
}))
};
onToggleDialog = (dialogName) => {
this.setState(({open}) => ({
open: {...open, [dialogName]: !open[dialogName]}
}));
};
onSnackbarOpen = (exception) => {
if (!(exception.hasOwnProperty("type") && exception.type === "AlertError")) {
console.error(exception);
return;
}
this.setState({
alertProps: exception
});
};
onSnackbarClose = (event, reason) => {
if (reason !== "clickaway") {
this.setState(({alertProps}) => ({
alertProps: {...alertProps, open: false}
}));
}
};
/**
* <h3>Overview</h3>
* Method forwarded to the {@link Import} section.
* Fired when user accepts their selection and requests to create project.
* <br>
* <br>
* Method checks if project name is already used.
* Then, makes an API call on projects to create new project.
* Eventually, adds new project to {@link App}'s state and changes section to "Project".
*
* @function
* @memberOf App
* @param {string} name - The name of the new project.
* @param {Object[]} files - The list of files that are used to build new project.
* @param {Object} [csvSpecs] - If a file containing data was in CSV format, this object contains CSV settings.
*/
onFilesAccepted = (name, files, csvSpecs) => {
if (!this.isNameUnique(name)) {
this.setState({
alertProps: {
message: "Project name already exists :(",
open: true,
severity: "warning"
}
});
} else {
const { serverBase } = this.state;
this.setState({
loading: true,
loadingTitle: "Creating project"
}, () => {
const method = "POST";
const data = new FormData();
data.append("name", name);
files.map(file => data.append(file.type, file.file));
if (csvSpecs != null && Object.keys(csvSpecs).length) {
Object.keys(csvSpecs).map(key => data.append(key, csvSpecs[key]));
}
fetchProjects(
method, data, serverBase
).then(result => {
if (result != null && result.hasOwnProperty("id")
&& result.hasOwnProperty("name")) {
this.props.history.push({
pathname: `/${result.id}`,
state: { bodyName: "Project", projectIndex: this.state.projects.length }
});
this.existsPath = `${result.id}`;
this.setState(({projects}) => ({
body: "Project",
currentProject: projects.length,
projects: [...projects, new Project(result.id, result.name)],
alertProps: {
message: `${result.name} has been created!`,
open: true,
severity: "success"
}
}));
}
}).catch(
this.onSnackbarOpen
).finally(() => {
this.setState({ loading: false });
});
});
}
};
onSaveProject = () => {
const { serverBase, currentProject, projects } = this.state;
const pathParams = { projectId: projects[currentProject].id };
if (currentProject >= 0) {
this.setState({
loading: true,
loadingTitle: "Compressing project"
}, () => {
exportProject(pathParams, serverBase)
.catch(this.onSnackbarOpen)
.finally(() => {
this.setState({ loading: false });
});
});
}
};
onUploadProject = (file) => {
if (file != null) {
const { serverBase } = this.state;
this.setState({
loading: true,
loadingTitle: "Importing project"
}, () => {
const body = new FormData();
body.append("importFile", file);
importProject(
body, serverBase
).then(result => {
if (result != null && result.hasOwnProperty("id")
&& result.hasOwnProperty("name")) {
this.props.history.push({
pathname: `/${result.id}`,
state: { bodyName: "Project", projectIndex: this.state.projects.length }
});
this.existsPath = `${result.id}`;
this.setState(({projects}) => ({
body: "Project",
currentProject: projects.length,
projects: [...projects, new Project(result.id, result.name)],
alertProps: {
message: `${result.name} has been imported!`,
open: true,
severity: "success"
}
}));
}
}).catch(
this.onSnackbarOpen
).finally(() => {
this.setState({ loading: false, loadingTitle: "" });
});
});
}
this.setState(({open}) => ({
open: { ...open, importDialog: false }
}));
}
/**
* <h3>Overview</h3>
* Callback fired when {@link SettingsProjectDialog} changed global visible object name.
*
* @function
* @memberOf App
*/
onObjectNamesChange = (objectVisibleName) => {
this.setState({
objectGlobalName: objectVisibleName
});
};
/**
* <h3>Overview</h3>
* Callback fired when {@link DeleteProjectDialog} requests to be closed.
* If user confirmed the deletion, method proceeds to delete current project.
* Then updates {@link App}'s state and closes dialog.
*
* @function
* @memberOf App
* @param {boolean} action - If <code>true</code> the method will proceed to delete current project.
*/
onDeleteDialogClose = (action) => {
const { currentProject, projects, serverBase } = this.state;
if (action && currentProject !== -1) {
this.setState({
loading: true,
loadingTitle: "Deleting project",
deleting: true
}, () => {
const pathParams = { projectId: projects[currentProject].id };
const method = "DELETE";
fetchProject(
pathParams, method, null, serverBase
).then(() => {
this.props.history.replace({
pathname: `/newProject`,
state: { bodyName: "Import" }
});
const removedProject = projects[currentProject].name;
this.setState(({projects, currentProject}) => ({
body: "Import",
currentProject: -1,
projects: [
...projects.slice(0, currentProject),
...projects.slice(currentProject + 1)
],
alertProps: {
message: `${removedProject} has been successfully deleted!`,
open: true,
severity: "success"
}
}));
}).catch(
this.onSnackbarOpen
).finally(() => {
this.setState({ loading: false, deleting: false });
});
});
}
this.setState(({open}) => ({
open: {...open, deleteDialog: false}
}));
};
/**
* <h3>Overview</h3>
* Callback fired when {@link RenameProjectDialog} requests to be closed.
* If user provided new name and when the new name is unique, method proceeds to update project's name.
* Then updates {@link App}'s state and closes dialog.
*
* @function
* @memberOf App
* @param {string} name - The new name for current project.
*/
onRenameDialogClose = (name) => {
if (name) {
if (this.isNameUnique(name)) {
const { currentProject, projects, serverBase } = this.state;
this.setState({
loading: true,
loadingTitle: "Modifying project name"
}, () => {
const pathParams = { projectId: projects[currentProject].id };
const method = "PATCH"
const body = new FormData();
body.append("name", name);
fetchProject(
pathParams, method, body, serverBase
).then(result => {
if (result) {
this.setState(({currentProject, projects}) => ({
projects: [
...projects.slice(0, currentProject),
{ ...projects[currentProject], name: result.name },
...projects.slice(currentProject + 1)
],
alertProps: {
message: "Project name changed successfully!",
open: true,
severity: "success"
},
}));
}
}).catch(
this.onSnackbarOpen
).finally(() => {
this.setState({ loading: false });
});
});
} else {
this.setState({
alertProps: {
message: "Project name already exists!",
open: true,
severity: 'warning'
}
});
return;
}
}
this.setState(({open}) => ({
open: {...open, renameDialog: false}
}));
};
/**
* <h3>Overview</h3>
* Checks whether a provided name is unique among other project's names.
*
* @function
* @memberOf App
* @param {string} name - Project's name.
* @returns {boolean} - If <code>true</code> the provided name is unique.
*/
isNameUnique = (name) => {
const { currentProject, open: { renameDialog }, projects } = this.state;
for (let i = 0; i < projects.length; i++) {
if (projects[i].name === name) {
return renameDialog && currentProject === i;
}
}
return true;
};
checkIfProjectInURLExists = () => {
const { projects } = this.state;
const url = window.location.href.toString();
const urlSplitted = url.split('/');
let projectFound = false;
if (urlSplitted.length >= 4) {
const projectId = url.split('/')[3];
for (let i = 0; i < projects.length; i++) {
if (projects[i].id === projectId) {
projectFound = true;
this.existsPath = `${projects[i].id}`;
if (urlSplitted.length >= 5) {
const tabName = url.split('/')[4];
const { projects } = this.state;
if (tabName !== "" && tabNames.includes(tabName)) {
this.props.history.replace({
pathname: `/${projects[i].id}/${tabName}`,
state: { bodyName: "Project", projectIndex: i }
});
} else {
this.props.history.replace({
pathname: `/${projects[i].id}/${tabNames[0]}`,
state: { bodyName: "Project", projectIndex: i }
});
}
}
else {
this.props.history.replace({
pathname: `/${projects[i].id}/${tabNames[0]}`,
state: { bodyName: "Project", projectIndex: i }
});
}
this.setState({
body: "Project",
currentProject: i
});
}
}
if (!projectFound) {
switch (projectId) {
case "help":
this.props.history.replace({
pathname: "/help",
state: {bodyName: "Help"}
});
this.setState({
body: "Help",
currentProject: -1
})
break;
case "newProject":
this.props.history.replace({
pathname: "/newProject",
state: { bodyName: "Import" }
});
this.setState({
body: "Import",
currentProject: -1
})
break;
default:
this.props.history.replace({
pathname: "/home",
state: { bodyName: "Home" }
});
this.setState({
body: "Home",
currentProject: -1
})
break;
}
}
}
};
render() {
const { deleting, currentProject, projects, objectGlobalName, open, serverBase, alertProps } = this.state;
const { deleteDialog, importDialog, renameDialog, settingsDialog } = open;
return (
<MuiThemeProvider theme={this.state.darkTheme ? DarkTheme : LightTheme}>
<CssBaseline />
<Header
appBarRef={this.appBarRef}
onBodyChange={this.onBodyChange}
onColorsChange={this.onColorsChange}
onImportOpen={() => this.onToggleDialog("importDialog")}
>
<ProjectMenu
currentProject={currentProject + 1}
onProjectClick={this.onCurrentProjectChange}
onDialogOpen={this.onToggleDialog}
onSaveProject={this.onSaveProject}
onSnackbarOpen={this.onSnackbarOpen}
projects={["Select your project", ...projects]}
serverBase={serverBase}
/>
</Header>
<Switch>
{
{
"Help": <Route
path={`/help`}
render={() => <Help
upperMargin={this.appBarRef.current ? this.appBarRef.current.offsetHeight : undefined}/>
}
/>,
"Home": <Route
path={`/home`}
render={() => <Home
goToHelp={() => this.onBodyChange("Help")}
goToNewProject={() => this.onBodyChange("Import")}
isDarkTheme={this.state.darkTheme}/>
}
/>,
"Import": <Route
path={`/newProject`}
render={() => <Import onFilesAccepted={this.onFilesAccepted}/>}
/>,
"Project": <Route
path={`/${this.existsPath}`}
render={(routerProps) => <ProjectTabs
deleting={deleting}
objectGlobalName={objectGlobalName}
onSnackbarOpen={this.onSnackbarOpen}
project={projects[currentProject]}
serverBase={serverBase}
updateProject={this.updateProject}
{...routerProps}/>
}
/>
}[this.state.body]
}
</Switch>
{currentProject >= 0 &&
<React.Fragment>
<RenameProjectDialog
currentName={projects[currentProject].name}
open={renameDialog}
onClose={this.onRenameDialogClose}
/>
<SettingsProjectDialog
onClose={() => this.onToggleDialog("settingsDialog")}
onObjectNamesChange={this.onObjectNamesChange}
onSnackbarOpen={this.onSnackbarOpen}
open={settingsDialog}
projectId={projects[currentProject].id}
serverBase={serverBase}
/>
<DeleteProjectDialog
currentName={projects[currentProject].name}
open={deleteDialog}
onClose={this.onDeleteDialogClose}
/>
</React.Fragment>
}
<ImportProjectDialog onImportProject={this.onUploadProject} open={importDialog} />
<StyledAlert {...alertProps} onClose={this.onSnackbarClose} />
{this.state.loading &&
<LoadingDelay>
<LoadingSnackbar
message={this.state.loadingTitle}
open={this.state.loading}
/>
</LoadingDelay>
}
</MuiThemeProvider>
);
}
}
export default App;
Source