Implement basic save/load (still needs some fixes)
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Siklos 2022-08-04 17:09:42 +02:00
parent 340cc86aa9
commit e2ad8d6ebd
6 changed files with 149 additions and 30 deletions

View file

@ -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'
}
};

View file

@ -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();
});

View file

@ -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';

View 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>
);
}
};

View file

@ -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;

View file

@ -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
}
}