Implement basic save/load (still needs some fixes)
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
340cc86aa9
commit
e2ad8d6ebd
6 changed files with 149 additions and 30 deletions
|
@ -21,7 +21,9 @@ module.exports = {
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'space-before-function-paren': ['error', 'never'],
|
'space-before-function-paren': ['error', 'never'],
|
||||||
indent: ['warn', 2],
|
indent: ['warn', 2, { SwitchCase: 1 }],
|
||||||
semi: ['warn', 'always']
|
semi: ['warn', 'always'],
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'error'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
58
src/App.tsx
58
src/App.tsx
|
@ -1,6 +1,8 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import './App.scss';
|
||||||
|
import { MainMenu } from './Components/MainMenu/MainMenu';
|
||||||
import { ContainerModel, IContainerModel } from './Components/SVG/Elements/ContainerModel';
|
import { ContainerModel, IContainerModel } from './Components/SVG/Elements/ContainerModel';
|
||||||
import Editor from './Editor';
|
import Editor, { IEditorState } from './Editor';
|
||||||
import { AvailableContainer } from './Interfaces/AvailableContainer';
|
import { AvailableContainer } from './Interfaces/AvailableContainer';
|
||||||
import { Configuration } from './Interfaces/Configuration';
|
import { Configuration } from './Interfaces/Configuration';
|
||||||
|
|
||||||
|
@ -23,40 +25,21 @@ interface IAppState {
|
||||||
export class App extends React.Component<IAppProps> {
|
export class App extends React.Component<IAppProps> {
|
||||||
public state: IAppState;
|
public state: IAppState;
|
||||||
|
|
||||||
public constructor(props: IAppProps) {
|
constructor(props: IAppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const MainContainer = new ContainerModel(
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
id: 'main',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1000,
|
|
||||||
height: 1000,
|
|
||||||
fillOpacity: 0,
|
|
||||||
stroke: 'black'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
this.state = {
|
this.state = {
|
||||||
configuration: {
|
configuration: {
|
||||||
AvailableContainers: [],
|
AvailableContainers: [],
|
||||||
AvailableSymbols: [],
|
AvailableSymbols: [],
|
||||||
MainContainer: {} as AvailableContainer
|
MainContainer: {} as AvailableContainer
|
||||||
},
|
},
|
||||||
history: [
|
history: [],
|
||||||
{
|
|
||||||
MainContainer,
|
|
||||||
SelectedContainer: MainContainer,
|
|
||||||
TypeCounters: {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
historyCurrentStep: 0,
|
historyCurrentStep: 0,
|
||||||
isLoaded: false
|
isLoaded: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public NewEditor() {
|
||||||
// Fetch the configuration from the API
|
// Fetch the configuration from the API
|
||||||
fetchConfiguration().then((configuration: Configuration) => {
|
fetchConfiguration().then((configuration: Configuration) => {
|
||||||
// Set the main container from the given properties of the API
|
// Set the main container from the given properties of the API
|
||||||
|
@ -91,6 +74,25 @@ export class App extends React.Component<IAppProps> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoadEditor(files: FileList | null) {
|
||||||
|
if (files === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
const editorState: IEditorState = JSON.parse(result);
|
||||||
|
this.setState({
|
||||||
|
configuration: editorState.configuration,
|
||||||
|
history: editorState.history,
|
||||||
|
historyCurrentStep: editorState.historyCurrentStep,
|
||||||
|
isLoaded: true
|
||||||
|
} as IAppState);
|
||||||
|
});
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
if (this.state.isLoaded) {
|
if (this.state.isLoaded) {
|
||||||
return (
|
return (
|
||||||
|
@ -102,6 +104,15 @@ export class App extends React.Component<IAppProps> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className='bg-blue-100 h-full w-full'>
|
||||||
|
<MainMenu
|
||||||
|
newEditor={() => this.NewEditor()}
|
||||||
|
loadEditor={(files: FileList | null) => this.LoadEditor(files)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,6 +142,7 @@ export async function fetchConfiguration(): Promise<Configuration> {
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
||||||
resolve(JSON.parse(this.responseText));
|
resolve(JSON.parse(this.responseText));
|
||||||
}
|
}
|
||||||
|
reject(xhr.responseText);
|
||||||
};
|
};
|
||||||
xhr.send();
|
xhr.send();
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,8 @@ export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
|
||||||
const depth: number = getDepth(container);
|
const depth: number = getDepth(container);
|
||||||
const key = container.properties.id.toString();
|
const key = container.properties.id.toString();
|
||||||
const text = '|\t'.repeat(depth) + key;
|
const text = '|\t'.repeat(depth) + key;
|
||||||
const selectedClass: string = this.props.SelectedContainer !== null &&
|
const selectedClass: string = this.props.SelectedContainer !== undefined &&
|
||||||
|
this.props.SelectedContainer !== null &&
|
||||||
this.props.SelectedContainer.properties.id === container.properties.id
|
this.props.SelectedContainer.properties.id === container.properties.id
|
||||||
? 'bg-blue-500 hover:bg-blue-600'
|
? 'bg-blue-500 hover:bg-blue-600'
|
||||||
: 'bg-slate-400 hover:bg-slate-600';
|
: 'bg-slate-400 hover:bg-slate-600';
|
||||||
|
|
74
src/Components/MainMenu/MainMenu.tsx
Normal file
74
src/Components/MainMenu/MainMenu.tsx
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface IMainMenuProps {
|
||||||
|
newEditor: () => void;
|
||||||
|
loadEditor: (files: FileList | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WindowState {
|
||||||
|
MAIN,
|
||||||
|
LOAD,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MainMenu: React.FC<IMainMenuProps> = (props) => {
|
||||||
|
const [windowState, setWindowState] = React.useState(WindowState.MAIN);
|
||||||
|
switch (windowState) {
|
||||||
|
case WindowState.LOAD:
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col drop-shadow-lg bg-blue-50 p-12 rounded-lg absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||||
|
<form className="flex items-center space-x-6">
|
||||||
|
<label className="block">
|
||||||
|
<span className="sr-only">Choose profile photo</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="application/json"
|
||||||
|
onChange={e => {
|
||||||
|
props.loadEditor(e.target.files);
|
||||||
|
}}
|
||||||
|
className="block w-full text-sm text-slate-500
|
||||||
|
file:mr-4 file:py-2 file:px-4
|
||||||
|
file:rounded-full file:border-0
|
||||||
|
file:text-sm file:font-semibold
|
||||||
|
file:transition-all
|
||||||
|
file:cursor-pointer
|
||||||
|
file:bg-blue-100 file:text-blue-700
|
||||||
|
hover:file:bg-blue-200
|
||||||
|
"/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
{/* <button
|
||||||
|
onClick={() => setWindowState(WindowState.MAIN)}
|
||||||
|
className='block text-sm
|
||||||
|
mt-8 py-4 px-4
|
||||||
|
rounded-full border-0
|
||||||
|
font-semibold
|
||||||
|
transition-all
|
||||||
|
bg-blue-100 text-blue-700
|
||||||
|
hover:bg-blue-200'
|
||||||
|
>
|
||||||
|
Load
|
||||||
|
</button> */}
|
||||||
|
<button
|
||||||
|
onClick={() => setWindowState(WindowState.MAIN)}
|
||||||
|
className='block text-sm
|
||||||
|
mt-8 py-2 px-4
|
||||||
|
rounded-full border-0
|
||||||
|
font-semibold
|
||||||
|
transition-all
|
||||||
|
bg-blue-100 text-blue-700
|
||||||
|
hover:bg-blue-200'
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||||
|
<button className='mainmenu-btn' onClick={props.newEditor}>Start from scratch</button>
|
||||||
|
<button className='mainmenu-btn' onClick={() => setWindowState(WindowState.LOAD)}>Load a configuration file</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
import './Editor.scss';
|
import './Editor.scss';
|
||||||
import Sidebar from './Components/Sidebar/Sidebar';
|
import Sidebar from './Components/Sidebar/Sidebar';
|
||||||
import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar';
|
import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar';
|
||||||
import { AvailableContainer } from './Interfaces/AvailableContainer';
|
|
||||||
import { Configuration } from './Interfaces/Configuration';
|
import { Configuration } from './Interfaces/Configuration';
|
||||||
import { SVG } from './Components/SVG/SVG';
|
import { SVG } from './Components/SVG/SVG';
|
||||||
import { History } from './Components/History/History';
|
import { History } from './Components/History/History';
|
||||||
|
@ -16,12 +15,15 @@ interface IEditorProps {
|
||||||
historyCurrentStep: number
|
historyCurrentStep: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEditorState {
|
export interface IEditorState {
|
||||||
isSidebarOpen: boolean,
|
isSidebarOpen: boolean,
|
||||||
isSVGSidebarOpen: boolean,
|
isSVGSidebarOpen: boolean,
|
||||||
isHistoryOpen: boolean,
|
isHistoryOpen: boolean,
|
||||||
history: Array<IHistoryState>,
|
history: Array<IHistoryState>,
|
||||||
historyCurrentStep: number,
|
historyCurrentStep: number,
|
||||||
|
// do not use it, use props.configuration
|
||||||
|
// only used for serialization purpose
|
||||||
|
configuration: Configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
class Editor extends React.Component<IEditorProps> {
|
class Editor extends React.Component<IEditorProps> {
|
||||||
|
@ -33,8 +35,8 @@ class Editor extends React.Component<IEditorProps> {
|
||||||
isSidebarOpen: true,
|
isSidebarOpen: true,
|
||||||
isSVGSidebarOpen: false,
|
isSVGSidebarOpen: false,
|
||||||
isHistoryOpen: false,
|
isHistoryOpen: false,
|
||||||
configuration: props.configuration,
|
configuration: Object.assign({}, props.configuration),
|
||||||
history: props.history,
|
history: [...props.history],
|
||||||
historyCurrentStep: props.historyCurrentStep
|
historyCurrentStep: props.historyCurrentStep
|
||||||
} as IEditorState;
|
} as IEditorState;
|
||||||
}
|
}
|
||||||
|
@ -238,6 +240,17 @@ class Editor extends React.Component<IEditorProps> {
|
||||||
} as IEditorState);
|
} as IEditorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SaveEditor() {
|
||||||
|
const exportName = 'state';
|
||||||
|
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(this.state, getCircularReplacer(), 4))}`;
|
||||||
|
const downloadAnchorNode = document.createElement('a');
|
||||||
|
downloadAnchorNode.setAttribute('href', dataStr);
|
||||||
|
downloadAnchorNode.setAttribute('download', `${exportName}.json`);
|
||||||
|
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||||
|
downloadAnchorNode.click();
|
||||||
|
downloadAnchorNode.remove();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the application
|
* Render the application
|
||||||
* @returns {JSX.Element} Rendered JSX element
|
* @returns {JSX.Element} Rendered JSX element
|
||||||
|
@ -291,9 +304,23 @@ class Editor extends React.Component<IEditorProps> {
|
||||||
<SVG selected={current.SelectedContainer}>
|
<SVG selected={current.SelectedContainer}>
|
||||||
{ current.MainContainer }
|
{ current.MainContainer }
|
||||||
</SVG>
|
</SVG>
|
||||||
|
<button onClick={() => this.SaveEditor()}>Save</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCircularReplacer = () => {
|
||||||
|
const seen = new WeakSet();
|
||||||
|
return (key: any, value: object | null) => {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default Editor;
|
export default Editor;
|
||||||
|
|
|
@ -12,4 +12,7 @@
|
||||||
.close-button {
|
.close-button {
|
||||||
@apply transition-all w-full h-auto p-4 flex
|
@apply transition-all w-full h-auto p-4 flex
|
||||||
}
|
}
|
||||||
|
.mainmenu-btn {
|
||||||
|
@apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue