ported schema builder
This commit is contained in:
135
components/schema/field-editor.tsx
Normal file
135
components/schema/field-editor.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
import { useObjectStateWrapper } from "../../hooks/useObjectState";
|
||||
import { ValueField } from "./value-field";
|
||||
import { HelpPopper } from "../Poppables/help";
|
||||
import { Icon } from "../Icon";
|
||||
import { RESERVED_FIELDS } from "../../constants/ReservedFields";
|
||||
import {
|
||||
fieldTypeOptions,
|
||||
FieldTypes,
|
||||
fieldTypesWithValues,
|
||||
} from "./fieldtypes";
|
||||
|
||||
interface IProps {
|
||||
update: (arg: FieldType) => void;
|
||||
field: FieldType;
|
||||
fieldName: string;
|
||||
deleteField: (arg: string) => void;
|
||||
}
|
||||
|
||||
export const FieldEditor: FC<IProps> = (
|
||||
{ update, field, fieldName, deleteField },
|
||||
) => {
|
||||
const { bindProperty, bindPropertyCheck } = useObjectStateWrapper(
|
||||
field,
|
||||
(e) => update(typeof e === "function" ? e(field) : e),
|
||||
);
|
||||
|
||||
const shouldShowValueField = useCallback(
|
||||
() => fieldTypesWithValues.includes(field.type) || field.isConstant,
|
||||
[field.isConstant, field.type],
|
||||
);
|
||||
|
||||
const [reserved, setReserved] = useState<FieldType | undefined>(
|
||||
RESERVED_FIELDS[fieldName],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setReserved(RESERVED_FIELDS[fieldName]);
|
||||
}, [fieldName]);
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log(field.value);
|
||||
// }, [field])
|
||||
|
||||
return (
|
||||
<li className="odd:bg-black/50">
|
||||
<div className="flex gap-2 items-center">
|
||||
<p>{fieldName}</p>
|
||||
{reserved && (
|
||||
<HelpPopper>
|
||||
<p className="text-xs">
|
||||
This is a reserved field name, these exist for internal purposes,
|
||||
but are still useful when creating a type, and as such have
|
||||
specific settings that you cannot override. If you need control
|
||||
over the field properties, please use a different field name
|
||||
</p>
|
||||
</HelpPopper>
|
||||
)}
|
||||
</div>
|
||||
{!reserved && (
|
||||
<div className=" flex gap-x-4 items-center p-2 w-full">
|
||||
<label className="w-min">
|
||||
Field Type:
|
||||
<select
|
||||
className="capitalize"
|
||||
{...bindProperty("type")}
|
||||
disabled={!!reserved}
|
||||
>
|
||||
{fieldTypeOptions.map((o) => (
|
||||
<option
|
||||
key={"fieldtypes" + o}
|
||||
className="capitalize"
|
||||
value={FieldTypes[o]}
|
||||
>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{shouldShowValueField() && (
|
||||
<ValueField type={field.type} bind={bindProperty("value")} />
|
||||
)}
|
||||
<span className="flex items-center gap-2">
|
||||
<label>
|
||||
<input type="checkbox" {...bindPropertyCheck("isConstant")} />
|
||||
{" "}
|
||||
Is constant
|
||||
</label>
|
||||
<HelpPopper>
|
||||
<p className="text-sm">
|
||||
Constant values can't be overwritten in publications. When
|
||||
a dice field is set to a constant value, it instead rolls a dice
|
||||
of that value whenever this field is displayed (unless
|
||||
exported). This could be useful for a randomly generated
|
||||
scenario or for cards being drawn as the dice value will
|
||||
automatically be determined by the dice roll.
|
||||
</p>
|
||||
</HelpPopper>
|
||||
</span>
|
||||
<label className="w-min">
|
||||
Minimum:
|
||||
<input
|
||||
className="w-12"
|
||||
type="number"
|
||||
{...bindProperty("minimum")}
|
||||
/>
|
||||
</label>
|
||||
<label className="w-min">
|
||||
Limit:
|
||||
<input className="w-12" type="number" {...bindProperty("limit")} />
|
||||
</label>
|
||||
<HelpPopper>
|
||||
<p className="text-sm">
|
||||
Minimum and Limit apply to the number of entries allowed for this
|
||||
field, not the maximum and minimum value. Set the minimum to 0 to
|
||||
make a field optional. Set the limit to 0 to allow for unlimited
|
||||
entries.
|
||||
</p>
|
||||
</HelpPopper>
|
||||
<button
|
||||
className="no-default self-end ml-auto"
|
||||
onClick={() => deleteField(fieldName)}
|
||||
>
|
||||
<Icon
|
||||
className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6"
|
||||
icon="Trash"
|
||||
>
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
31
components/schema/field-type-input.tsx
Normal file
31
components/schema/field-type-input.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { SchemaEditAtom } from "../../recoil/atoms/schema";
|
||||
import { TEMPLATE_TYPES } from "../../constants/TemplateTypes";
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
|
||||
interface IProps {
|
||||
bind: InputBinder;
|
||||
}
|
||||
|
||||
export const FieldTypeInput: FC<PropsWithChildren<IProps>> = ({ bind }) => {
|
||||
const schema = useRecoilValue(SchemaEditAtom);
|
||||
|
||||
return (
|
||||
<label className="w-min">
|
||||
Type:
|
||||
<input type="text" {...bind} list="type-editor-type-list" />
|
||||
<datalist id="type-editor-type-list">
|
||||
{Object.keys(TEMPLATE_TYPES).map((k) => (
|
||||
<option key={"templatetypes" + k} className="capitalize" value={k}>
|
||||
{k}
|
||||
</option>
|
||||
))}
|
||||
{Object.keys(schema.types).map((k) => (
|
||||
<option key={"schematypes" + k} className="capitalize" value={k}>
|
||||
{k}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
);
|
||||
};
|
26
components/schema/fieldtypes.ts
Normal file
26
components/schema/fieldtypes.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export const fieldTypeOptions: (keyof typeof FieldTypes)[] = [
|
||||
"number",
|
||||
"text",
|
||||
"long text",
|
||||
"checkbox",
|
||||
"type",
|
||||
"dice",
|
||||
"select",
|
||||
"any",
|
||||
];
|
||||
export enum FieldTypes {
|
||||
number = "number",
|
||||
text = "text",
|
||||
"long text" = "long text",
|
||||
checkbox = "checkbox",
|
||||
type = "@type",
|
||||
dice = "dice",
|
||||
any = "@select",
|
||||
select = "select",
|
||||
}
|
||||
export const fieldTypesWithValues = [
|
||||
FieldTypes.dice,
|
||||
FieldTypes.type,
|
||||
FieldTypes.select,
|
||||
FieldTypes.any,
|
||||
];
|
198
components/schema/index.tsx
Normal file
198
components/schema/index.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useCallback, 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, useResetRecoilState } 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";
|
||||
|
||||
export const SchemaBuilder: FC = () => {
|
||||
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
||||
const resetSchema = useResetRecoilState(SchemaEditAtom);
|
||||
const { update: updateSchema, bindProperty: bindSchemaProperty } =
|
||||
useObjectStateWrapper<Schema>(schema, setSchema);
|
||||
|
||||
const { schemaId } = useParams<{ schemaId: string }>();
|
||||
|
||||
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 () => {
|
||||
setLastSaved(schema);
|
||||
// const sid = await GameSystemsService.saveSchema(schema);
|
||||
// if (schemaId === 'new') navigate('/schema/'+sid)
|
||||
}, [schema]);
|
||||
|
||||
const selectTypeForEdit = useCallback((typeKey: string) => {
|
||||
setSelectedType(typeKey);
|
||||
setPageNumber(1);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
value: schemaFieldName,
|
||||
bind: bindTemplateName,
|
||||
reset: resetTemplateName,
|
||||
} = useInput("", { disallowSpaces: true });
|
||||
const addSchemaField = useCallback(() => {
|
||||
updateSchema((s) => ({
|
||||
schema: {
|
||||
...s.schema,
|
||||
[schemaFieldName]: {
|
||||
display: "",
|
||||
type: FieldTypes.any,
|
||||
},
|
||||
},
|
||||
}));
|
||||
resetTemplateName();
|
||||
}, [resetTemplateName, 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" {...bindTemplateName} />
|
||||
<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
|
||||
onClick={saveSchema}
|
||||
disabled={lastSaved === schema}
|
||||
>
|
||||
Save Schema
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-800"
|
||||
onClick={() => setSchema(lastSaved)}
|
||||
disabled={lastSaved === schema}
|
||||
>
|
||||
Discard Changes
|
||||
</button>
|
||||
</div>
|
||||
<SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
94
components/schema/schema-viewer.tsx
Normal file
94
components/schema/schema-viewer.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { Truncate } from "@/components/Poppables/truncation";
|
||||
import { Accordion, AccordionContent } from "../../lib/accordion";
|
||||
import { FieldTypes, fieldTypesWithValues } from "./fieldtypes";
|
||||
|
||||
interface IProps {
|
||||
schema: Schema;
|
||||
onTypeClick?: (arg: string, arg1: TypeType) => void;
|
||||
}
|
||||
|
||||
export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
|
||||
const createValueLable = useCallback((field: FieldType) => {
|
||||
if (field.isConstant) {
|
||||
if (field.type === FieldTypes.dice) return "Auto-rolled";
|
||||
return "Constant value:";
|
||||
}
|
||||
switch (field.type) {
|
||||
case FieldTypes.type:
|
||||
return "Type:";
|
||||
case FieldTypes.dice:
|
||||
return "Dice:";
|
||||
case FieldTypes.select:
|
||||
return "Options:";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <div className="whitespace-pre-wrap">{JSON.stringify(schema, null, 2)}</div> */}
|
||||
<div>
|
||||
<p className="font-bold text-lg">{schema.name}</p>
|
||||
<hr />
|
||||
<p className="font-bold italic">Templates</p>
|
||||
<ul>
|
||||
{Object.entries(schema.schema).map(([templateKey, template]) => (
|
||||
<li key={templateKey}>
|
||||
<p className="font-bold">{templateKey}</p>
|
||||
<p className="font-thin text-xs">{template.type}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<hr />
|
||||
<p className="font-bold italic">Types</p>
|
||||
<ul className="rounded-lg overflow-hidden grid">
|
||||
{Object.entries(schema.types).map(([typeKey, type]) => (
|
||||
<li
|
||||
key={"type viewer" + typeKey}
|
||||
// onClick={() => onTypeClick && onTypeClick(typeKey, type)}
|
||||
data-clickable={!!onTypeClick}
|
||||
className="odd:bg-black/50 p-2 group overflow-hidden"
|
||||
>
|
||||
<Accordion
|
||||
title={
|
||||
<p className="group-data-[expanded]/controlled:mb-2 transition-all font-bold">
|
||||
{typeKey}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<AccordionContent>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.entries(type).map(([fieldKey, field]) => (
|
||||
<div
|
||||
key={"field viewer" + fieldKey}
|
||||
className="rounded-lg border border-olive-drab p-2"
|
||||
>
|
||||
<p className="font-bold">{fieldKey}</p>
|
||||
<p className="font-thin capitalize text-xs">
|
||||
{field.type}
|
||||
</p>
|
||||
<p className="font-thin capitalize text-xs">
|
||||
Maximum entries:{" "}
|
||||
{field.limit === 0 ? "unlimited " : field.limit}
|
||||
</p>
|
||||
{(field.isConstant ||
|
||||
fieldTypesWithValues.includes(field.type)) && (
|
||||
<p className="font-thin capitalize text-xs">
|
||||
{createValueLable(field)}{" "}
|
||||
<Truncate>{field.value}</Truncate>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</Accordion>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
75
components/schema/template-editor.tsx
Normal file
75
components/schema/template-editor.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { useObjectStateWrapper } from "@/hooks/useObjectState";
|
||||
import { TEMPLATE_TYPES } from "@/constants/TemplateTypes";
|
||||
import { SchemaEditAtom } from "@/recoil/atoms/schema";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { Icon } from "@/components/Icon";
|
||||
|
||||
interface IProps {
|
||||
templateKey: string;
|
||||
update: (arg0: string, arg1: Template) => void;
|
||||
template: Template;
|
||||
}
|
||||
|
||||
export const TemplateEditor: FC<IProps> = (
|
||||
{ templateKey, update, template },
|
||||
) => {
|
||||
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
||||
const updateTemplate = useCallback(
|
||||
(t: Template | ((arg: Template) => Template)) => {
|
||||
update(templateKey, typeof t === "function" ? t(template) : t);
|
||||
},
|
||||
[templateKey, update, template],
|
||||
);
|
||||
|
||||
const { bindProperty } = useObjectStateWrapper(
|
||||
template,
|
||||
updateTemplate,
|
||||
);
|
||||
|
||||
const deleteTemplate = useCallback(() => {
|
||||
setSchema((s: Schema) => {
|
||||
const templates = { ...s.schema };
|
||||
delete templates[templateKey];
|
||||
return {
|
||||
...s,
|
||||
schema: templates,
|
||||
};
|
||||
});
|
||||
}, [setSchema, templateKey]);
|
||||
return (
|
||||
<li className="odd:bg-black/50 p-2">
|
||||
<p className="font-bold">{templateKey}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 pl-2">
|
||||
<label className="w-min">
|
||||
Type:
|
||||
<input
|
||||
type="text"
|
||||
{...bindProperty("type", { disallowSpaces: true })}
|
||||
list="type-editor-type-list"
|
||||
/>
|
||||
<datalist id="type-editor-type-list">
|
||||
{Object.keys(TEMPLATE_TYPES).map((k) => (
|
||||
<option key={"templatetype" + k} value={k}>{k}</option>
|
||||
))}
|
||||
{Object.keys(schema.types).map((k) => (
|
||||
<option key={"schematype" + k} value={k}>{k}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
<textarea {...bindProperty("display")} cols={30} rows={10}></textarea>
|
||||
</div>
|
||||
<button
|
||||
className="no-default"
|
||||
onClick={deleteTemplate}
|
||||
>
|
||||
<Icon
|
||||
icon="Trash"
|
||||
className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
104
components/schema/type-editor.tsx
Normal file
104
components/schema/type-editor.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
FC,
|
||||
FormEvent,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useObjectState } from "../../hooks/useObjectState";
|
||||
import { useInput } from "../../hooks/useInput";
|
||||
import { FieldEditor } from "./field-editor";
|
||||
import { FieldTypes } from "./fieldtypes";
|
||||
|
||||
interface IProps {
|
||||
name: string;
|
||||
saveType: (arg0: string, arg1: TypeType) => void;
|
||||
type?: TypeType;
|
||||
}
|
||||
|
||||
const constantProperties = ["metadata"];
|
||||
|
||||
export const TypeEditor: FC<PropsWithChildren<IProps>> = (
|
||||
{ saveType, name, type: passedType },
|
||||
) => {
|
||||
const {
|
||||
update: updateType,
|
||||
reset: resetType,
|
||||
state: type,
|
||||
setState: setType,
|
||||
} = useObjectState<TypeType>({});
|
||||
|
||||
const {
|
||||
value: propertyName,
|
||||
bind: bindPropertyName,
|
||||
reset: resetPropertyName,
|
||||
} = useInput("", { disallowSpaces: true });
|
||||
|
||||
const save = () => {
|
||||
saveType(name, type);
|
||||
resetType();
|
||||
};
|
||||
|
||||
const addField = useCallback((e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
updateType({
|
||||
[propertyName]: {
|
||||
type: FieldTypes.number,
|
||||
value: "",
|
||||
isConstant: false,
|
||||
limit: 1,
|
||||
minimum: 1,
|
||||
},
|
||||
});
|
||||
resetPropertyName();
|
||||
}, [propertyName, updateType, resetPropertyName]);
|
||||
|
||||
const updateField = useCallback(
|
||||
(k: keyof typeof type) => (field: FieldType) => {
|
||||
updateType({ [k]: field });
|
||||
},
|
||||
[updateType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
passedType && setType(passedType);
|
||||
}, [passedType, setType]);
|
||||
|
||||
const deleteField = useCallback((name: string) => {
|
||||
setType((t) => {
|
||||
const fields = { ...t };
|
||||
delete fields[name];
|
||||
return fields;
|
||||
});
|
||||
}, [setType]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="subheader">
|
||||
{passedType ? "Editing" : "Creating"} type "{name}"
|
||||
</p>
|
||||
<form onSubmit={addField}>
|
||||
<input type="text" {...bindPropertyName} />
|
||||
<button disabled={!propertyName}>Add Field</button>
|
||||
</form>
|
||||
<ul className="rounded-lg overflow-hidden">
|
||||
{Object.entries(type).reverse().filter(([k]) =>
|
||||
!constantProperties.includes(k)
|
||||
).map(([key, value]) => (
|
||||
<FieldEditor
|
||||
key={"field-editor" + key}
|
||||
field={value}
|
||||
update={updateField(key)}
|
||||
fieldName={key}
|
||||
deleteField={deleteField}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<button onClick={save} disabled={!Object.keys(type).length}>
|
||||
Save Type
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
129
components/schema/value-field.tsx
Normal file
129
components/schema/value-field.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { ChangeEvent, FC, useRef } from "react";
|
||||
import { FieldTypeInput } from "./field-type-input";
|
||||
import { useInput } from "../../hooks/useInput";
|
||||
import { HelpPopper } from "../Poppables/help";
|
||||
import { FieldTypes } from "./fieldtypes";
|
||||
|
||||
interface IValueProps {
|
||||
type: FieldTypes;
|
||||
bind: InputBinder;
|
||||
}
|
||||
|
||||
const DICE_SIDES = [3, 4, 6, 8, 10, 12, 20, 100];
|
||||
|
||||
export const ValueField: FC<IValueProps> = ({ type, bind }) => {
|
||||
const { value: diceCount, bind: bindDiceCount } = useInput(1);
|
||||
const { value: diceSides, bind: bindDiceSides } = useInput("");
|
||||
|
||||
const diceInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
switch (type) {
|
||||
case FieldTypes.dice: {
|
||||
const onChange = (
|
||||
handler: (
|
||||
arg: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => void,
|
||||
) =>
|
||||
(e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
handler(e);
|
||||
setTimeout(() => {
|
||||
if (!diceInputRef.current) return;
|
||||
e.target = diceInputRef.current;
|
||||
bind.onChange(e);
|
||||
}, 0);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<label className="w-min">
|
||||
Count:
|
||||
<input
|
||||
className="w-12"
|
||||
type="number"
|
||||
{...bindDiceCount}
|
||||
onChange={onChange(bindDiceCount.onChange)}
|
||||
/>
|
||||
</label>
|
||||
<label className="w-min">
|
||||
Sides:
|
||||
<select
|
||||
{...bindDiceSides}
|
||||
onChange={onChange(bindDiceSides.onChange)}
|
||||
>
|
||||
<option value=""></option>
|
||||
{DICE_SIDES.map((d) => (
|
||||
<option key={"dice sides" + d} value={"d" + d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<input
|
||||
ref={diceInputRef}
|
||||
className="hidden"
|
||||
type="text"
|
||||
name={bind.name}
|
||||
value={diceCount + diceSides}
|
||||
readOnly
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case FieldTypes.type:
|
||||
return <FieldTypeInput bind={bind} />;
|
||||
case FieldTypes.number:
|
||||
return (
|
||||
<label className="w-min">
|
||||
Value:<input className="w-16" type="number" {...bind} />
|
||||
</label>
|
||||
);
|
||||
case FieldTypes.text:
|
||||
return (
|
||||
<label className="w-min">
|
||||
Value:<input type="text" {...bind} />
|
||||
</label>
|
||||
);
|
||||
case FieldTypes.select:
|
||||
return (
|
||||
<>
|
||||
<label className="w-min">
|
||||
<div className="flex gap-2 items-center">
|
||||
Values:
|
||||
<HelpPopper>
|
||||
<p className="text-xs">
|
||||
A comma separated list (no spaces, spaces are reserved for
|
||||
values) of options that can be chosen while creating
|
||||
publications. Ex: earthquake,wind storm,fire tornado,rainbow.
|
||||
Alternatively, you can specify a display value and an actual
|
||||
value separated with a colon. This is useful for when you want
|
||||
to create a reference in a publication with a dropdown field.
|
||||
Ex: Rapid Fire:^core.weaponAbilities[name=rapid
|
||||
fire],Heavy:^core.weaponAbilities[name=heavy]
|
||||
</p>
|
||||
</HelpPopper>
|
||||
</div>
|
||||
<input type="text" {...bind} />
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
case FieldTypes.any:
|
||||
return (
|
||||
<>
|
||||
<label className="w-min">
|
||||
<div className="flex gap-2 items-center">
|
||||
Type options:
|
||||
<HelpPopper>
|
||||
<p className="text-xs">
|
||||
A comma separated list (no spaces, spaces are reserved for
|
||||
values) of options that are names of types that can be
|
||||
selected when creating a publication, Ex: dice,number,text. Do
|
||||
not leave this blank, allowing for any type to be selected
|
||||
makes querying gross.
|
||||
</p>
|
||||
</HelpPopper>
|
||||
</div>
|
||||
<input type="text" {...bind} />
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user