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: {
|
||||
'space-before-function-paren': ['error', 'never'],
|
||||
indent: ['warn', 2],
|
||||
semi: ['warn', 'always']
|
||||
indent: ['warn', 2, { SwitchCase: 1 }],
|
||||
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 './App.scss';
|
||||
import { MainMenu } from './Components/MainMenu/MainMenu';
|
||||
import { ContainerModel, IContainerModel } from './Components/SVG/Elements/ContainerModel';
|
||||
import Editor from './Editor';
|
||||
import Editor, { IEditorState } from './Editor';
|
||||
import { AvailableContainer } from './Interfaces/AvailableContainer';
|
||||
import { Configuration } from './Interfaces/Configuration';
|
||||
|
||||
|
@ -23,40 +25,21 @@ interface IAppState {
|
|||
export class App extends React.Component<IAppProps> {
|
||||
public state: IAppState;
|
||||
|
||||
public constructor(props: IAppProps) {
|
||||
constructor(props: IAppProps) {
|
||||
super(props);
|
||||
|
||||
const MainContainer = new ContainerModel(
|
||||
null,
|
||||
{
|
||||
id: 'main',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
);
|
||||
this.state = {
|
||||
configuration: {
|
||||
AvailableContainers: [],
|
||||
AvailableSymbols: [],
|
||||
MainContainer: {} as AvailableContainer
|
||||
},
|
||||
history: [
|
||||
{
|
||||
MainContainer,
|
||||
SelectedContainer: MainContainer,
|
||||
TypeCounters: {}
|
||||
}
|
||||
],
|
||||
history: [],
|
||||
historyCurrentStep: 0,
|
||||
isLoaded: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public NewEditor() {
|
||||
// Fetch the configuration from the API
|
||||
fetchConfiguration().then((configuration: Configuration) => {
|
||||
// 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() {
|
||||
if (this.state.isLoaded) {
|
||||
return (
|
||||
|
@ -102,6 +104,15 @@ export class App extends React.Component<IAppProps> {
|
|||
/>
|
||||
</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) {
|
||||
resolve(JSON.parse(this.responseText));
|
||||
}
|
||||
reject(xhr.responseText);
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
|
|
|
@ -38,7 +38,8 @@ export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
|
|||
const depth: number = getDepth(container);
|
||||
const key = container.properties.id.toString();
|
||||
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
|
||||
? 'bg-blue-500 hover:bg-blue-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 Sidebar from './Components/Sidebar/Sidebar';
|
||||
import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar';
|
||||
import { AvailableContainer } from './Interfaces/AvailableContainer';
|
||||
import { Configuration } from './Interfaces/Configuration';
|
||||
import { SVG } from './Components/SVG/SVG';
|
||||
import { History } from './Components/History/History';
|
||||
|
@ -16,12 +15,15 @@ interface IEditorProps {
|
|||
historyCurrentStep: number
|
||||
}
|
||||
|
||||
interface IEditorState {
|
||||
export interface IEditorState {
|
||||
isSidebarOpen: boolean,
|
||||
isSVGSidebarOpen: boolean,
|
||||
isHistoryOpen: boolean,
|
||||
history: Array<IHistoryState>,
|
||||
historyCurrentStep: number,
|
||||
// do not use it, use props.configuration
|
||||
// only used for serialization purpose
|
||||
configuration: Configuration
|
||||
}
|
||||
|
||||
class Editor extends React.Component<IEditorProps> {
|
||||
|
@ -33,8 +35,8 @@ class Editor extends React.Component<IEditorProps> {
|
|||
isSidebarOpen: true,
|
||||
isSVGSidebarOpen: false,
|
||||
isHistoryOpen: false,
|
||||
configuration: props.configuration,
|
||||
history: props.history,
|
||||
configuration: Object.assign({}, props.configuration),
|
||||
history: [...props.history],
|
||||
historyCurrentStep: props.historyCurrentStep
|
||||
} as IEditorState;
|
||||
}
|
||||
|
@ -238,6 +240,17 @@ class Editor extends React.Component<IEditorProps> {
|
|||
} 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
|
||||
* @returns {JSX.Element} Rendered JSX element
|
||||
|
@ -291,9 +304,23 @@ class Editor extends React.Component<IEditorProps> {
|
|||
<SVG selected={current.SelectedContainer}>
|
||||
{ current.MainContainer }
|
||||
</SVG>
|
||||
<button onClick={() => this.SaveEditor()}>Save</button>
|
||||
</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;
|
||||
|
|
|
@ -12,4 +12,7 @@
|
|||
.close-button {
|
||||
@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