157 lines
5.4 KiB
TypeScript
157 lines
5.4 KiB
TypeScript
import { FC, useCallback, useEffect, useState } from 'react'
|
|
import AnimatedPageContainer from '../AnimatedPageContainer';
|
|
import { TypeEditor } from './type-editor';
|
|
import { useObjectState, useObjectStateWrapper } from '../../hooks/useObjectState';
|
|
import { FieldTypes, Schema, Template, TypeType } from '../../types/schema';
|
|
import { useInput } from '../../hooks/useInput';
|
|
import { useRecoilState } from 'recoil';
|
|
import { SchemaEditAtom } from '../../recoil/atoms/schema';
|
|
import { GameSystemsService } from '../../services/game-systems';
|
|
import { SchemaViewer } from './schema-viewer';
|
|
import { TemplateEditor } from './template-editor';
|
|
import { Icon } from '../Icon';
|
|
|
|
|
|
export const SchemaBuilder: FC = () => {
|
|
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
|
const { update: updateSchema } = useObjectStateWrapper<Schema>(schema, setSchema);
|
|
|
|
const { value: typeName, bind: bindTypeName, reset: resetTypeName } = useInput('');
|
|
|
|
const [pageNumber, setPageNumber] = useState(0);
|
|
|
|
const [lastSaved, setLastSaved] = useState(schema);
|
|
|
|
const fetchSchema = useCallback(async () => {
|
|
const result = await GameSystemsService.getSchema('286f4c18-d280-444b-8d7e-9a3dd09f64ef')
|
|
if (result.status !== 200) return;
|
|
const fetchedSchema = await result.json();
|
|
// if (fetchedSchema.name === schema.name) return;
|
|
if (!fetchedSchema.templates) fetchedSchema.templates = {}
|
|
setSchema(fetchedSchema);
|
|
setLastSaved(fetchedSchema);
|
|
}, [setSchema])
|
|
|
|
useEffect(() => {
|
|
fetchSchema();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
const [selectedType, setSelectedType] = useState('');
|
|
|
|
const saveType = useCallback((name: string, type: TypeType) => {
|
|
updateSchema(e => ({
|
|
types: {
|
|
...e.types,
|
|
[name]: type
|
|
}
|
|
}));
|
|
resetTypeName();
|
|
setPageNumber(0);
|
|
setSelectedType('');
|
|
}, [resetTypeName, updateSchema]);
|
|
|
|
const saveSchema = useCallback(() => {
|
|
GameSystemsService.saveSchema(schema);
|
|
setLastSaved(schema);
|
|
}, [schema])
|
|
|
|
const selectTypeForEdit = useCallback((typeKey: string) => {
|
|
setSelectedType(typeKey);
|
|
setPageNumber(1);
|
|
}, [])
|
|
|
|
const { value: templateName, bind: bindTemplateName, reset: resetTemplateName } = useInput('', { disallowSpaces: true });
|
|
const addTemplate = useCallback(() => {
|
|
updateSchema(s => ({
|
|
templates: {
|
|
...s.templates,
|
|
[templateName]: {
|
|
publishable: false,
|
|
type: FieldTypes.any
|
|
}
|
|
}
|
|
}));
|
|
resetTemplateName();
|
|
}, [resetTemplateName, templateName, updateSchema])
|
|
|
|
const updateTemplate = useCallback((key: string, template: Template) => {
|
|
updateSchema(s => ({
|
|
templates: {
|
|
...s.templates,
|
|
[key]: template
|
|
}
|
|
}))
|
|
}, [updateSchema])
|
|
|
|
const deleteType = useCallback((key: string) => {
|
|
updateSchema(s => {
|
|
const types = { ...s.types };
|
|
delete types[key];
|
|
return { types }
|
|
})
|
|
}, [updateSchema])
|
|
|
|
return (
|
|
<div className="container flex gap-4 p-8">
|
|
<div className="panel w-2/3 h-full flex flex-col gap-4">
|
|
<div>
|
|
<p className="subheader mb-2">Add a template</p>
|
|
<div className="mb-2">
|
|
<input type="text" {...bindTemplateName} />
|
|
<button onClick={addTemplate} disabled={!templateName}>Add</button>
|
|
</div>
|
|
<ul className="rounded-lg overflow-hidden">
|
|
{Object.entries(schema.templates).map(([templateKey, template]) => (
|
|
<TemplateEditor templateKey={templateKey} template={template} update={updateTemplate} />
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<hr />
|
|
<div>
|
|
<AnimatedPageContainer currentPage={pageNumber}>
|
|
<div>
|
|
<p className="subheader mb-2">Add a type</p>
|
|
<input type="text" {...bindTypeName} />
|
|
<button className="interactive" disabled={!typeName} onClick={() => setPageNumber(1)}>Configure</button>
|
|
</div>
|
|
<TypeEditor name={selectedType || typeName} saveType={saveType} type={selectedType ? schema.types[selectedType as keyof typeof schema.types] : undefined} />
|
|
</AnimatedPageContainer>
|
|
<ul className="mt-3 w-96">
|
|
{Object.keys(schema.types).map(t => (
|
|
<li className="odd:bg-black/50 flex justify-between p-2">
|
|
{t}
|
|
<div className="flex gap-3">
|
|
<button title="Edit" className="no-default" onClick={() => selectTypeForEdit(t)}>
|
|
<Icon icon="anvil" className="anvil svg-olive-drab hover:svg-olive-drab-100 w-6 h-6" />
|
|
</button>
|
|
<button title="Delete" className="no-default" onClick={() => deleteType(t)}>
|
|
<Icon icon="trash" className="trash-can svg-red-700 hover:svg-red-500 w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div className="panel basis-1/3">
|
|
<SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} />
|
|
<div className="flex gap-2 mt-2">
|
|
<button
|
|
onClick={saveSchema}
|
|
disabled={lastSaved === schema}
|
|
>
|
|
Save Schema
|
|
</button>
|
|
<button
|
|
className="bg-red-800"
|
|
onClick={() => setSchema(lastSaved)}
|
|
disabled={lastSaved === schema}
|
|
>
|
|
Discard Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
} |