229 lines
6.7 KiB
TypeScript
229 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { FC, useCallback, useEffect, useState } from "react";
|
|
import AnimatedPageContainer from "@/components/AnimatedPageContainer";
|
|
import { TypeEditor } from "./type-editor";
|
|
import { useObjectStateWrapper } from "@/hooks/useObjectState";
|
|
import { useInput } from "../../hooks/useInput";
|
|
import { useRecoilState } from "recoil";
|
|
import { SchemaEditAtom } from "@/recoil/atoms/schema";
|
|
import { SchemaViewer } from "./schema-viewer";
|
|
import { TemplateEditor } from "./template-editor";
|
|
import { Icon } from "@/components/Icon";
|
|
import { useParams } from "next/navigation";
|
|
import { FieldTypes } from "./fieldtypes";
|
|
import { findSchema, saveSchemaDb } from "@/actions/Schemas/index";
|
|
import { useToast } from "../toast";
|
|
|
|
export const SchemaBuilder: FC = () => {
|
|
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
|
// const resetSchema = useResetRecoilState(SchemaEditAtom);
|
|
const { createToast } = useToast();
|
|
const { update: updateSchema, bindProperty: bindSchemaProperty } =
|
|
useObjectStateWrapper<Schema>(schema, setSchema);
|
|
|
|
const { schemaId, id: gameSystemId } = useParams<{
|
|
schemaId: string;
|
|
id: string;
|
|
}>();
|
|
|
|
useEffect(() => {
|
|
if (schemaId !== "create" && schemaId !== schema.id)
|
|
findSchema(schemaId).then((sc) => {
|
|
if (!sc) return;
|
|
setSchema(sc);
|
|
});
|
|
}, [schema.id, schemaId, setSchema]);
|
|
|
|
useEffect(() => {
|
|
if (gameSystemId && !schema.gameSystemId)
|
|
setSchema((sc) => ({ ...sc, gameSystemId }));
|
|
}, [gameSystemId, schema.gameSystemId, setSchema]);
|
|
|
|
const {
|
|
value: typeName,
|
|
bind: bindTypeName,
|
|
reset: resetTypeName,
|
|
} = useInput("");
|
|
|
|
const [pageNumber, setPageNumber] = useState(0);
|
|
|
|
const [lastSaved, _setLastSaved] = useState(schema);
|
|
|
|
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(async () => {
|
|
createToast({ msg: "Saving Schema", fading: true });
|
|
await saveSchemaDb(schema);
|
|
}, [createToast, schema]);
|
|
|
|
const selectTypeForEdit = useCallback((typeKey: string) => {
|
|
setSelectedType(typeKey);
|
|
setPageNumber(1);
|
|
}, []);
|
|
|
|
const {
|
|
value: schemaFieldName,
|
|
bind: bindSchemaFieldName,
|
|
reset: resetSchemaFieldName,
|
|
} = useInput("", { disallowSpaces: true });
|
|
const addSchemaField = useCallback(() => {
|
|
updateSchema((s) => ({
|
|
schema: {
|
|
...s.schema,
|
|
[schemaFieldName]: FieldTypes.any,
|
|
},
|
|
}));
|
|
resetSchemaFieldName();
|
|
}, [resetSchemaFieldName, schemaFieldName, updateSchema]);
|
|
|
|
const updateSchemaField = useCallback(
|
|
(key: string, template: Template) => {
|
|
updateSchema((s) => ({
|
|
schema: {
|
|
...s.schema,
|
|
[key]: template,
|
|
},
|
|
}));
|
|
},
|
|
[updateSchema],
|
|
);
|
|
|
|
const deleteType = useCallback(
|
|
(key: string) => {
|
|
updateSchema((s) => {
|
|
const types = { ...s.types };
|
|
delete types[key];
|
|
return { types };
|
|
});
|
|
},
|
|
[updateSchema],
|
|
);
|
|
|
|
return (
|
|
<div className="flex gap-4 p-8">
|
|
<div className="panel w-2/3 h-full flex flex-col gap-4">
|
|
<div>
|
|
<input
|
|
type="text"
|
|
{...bindSchemaProperty("name")}
|
|
placeholder="Schema Name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<p className="subheader mb-2">Add Schema Field</p>
|
|
<div className="mb-2">
|
|
<input type="text" {...bindSchemaFieldName} />
|
|
<button onClick={addSchemaField} disabled={!schemaFieldName}>
|
|
Add
|
|
</button>
|
|
</div>
|
|
<ul className="rounded-lg overflow-hidden">
|
|
{Object.entries(schema.schema).map(
|
|
([schemaFieldKey, schemaField]) => (
|
|
<TemplateEditor
|
|
key={schemaFieldKey}
|
|
templateKey={schemaFieldKey}
|
|
template={schemaField}
|
|
update={updateSchemaField}
|
|
/>
|
|
),
|
|
)}
|
|
</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
|
|
key={"type" + t}
|
|
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">
|
|
<div className="flex gap-2 mb-2">
|
|
<button
|
|
className="btn btn-small bg-green-800"
|
|
onClick={saveSchema}
|
|
disabled={lastSaved === schema}
|
|
>
|
|
Save Schema
|
|
</button>
|
|
<button
|
|
className="bg-red-800 btn btn-small"
|
|
onClick={() => setSchema(lastSaved)}
|
|
disabled={lastSaved === schema}
|
|
>
|
|
Discard Changes
|
|
</button>
|
|
</div>
|
|
<SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|