Creates the generic new-document form
This commit is contained in:
@@ -28,14 +28,6 @@ export const DocumentEditForm = ({ document }: { document: AnyDocument }) => {
|
|||||||
case "treasure":
|
case "treasure":
|
||||||
return <TreasureEditForm treasure={document} />;
|
return <TreasureEditForm treasure={document} />;
|
||||||
case "thread":
|
case "thread":
|
||||||
return (
|
return <GenericEditForm doc={document} />;
|
||||||
<GenericEditForm
|
|
||||||
doc={document}
|
|
||||||
fields={{
|
|
||||||
text: "multiline",
|
|
||||||
resolved: "checkbox",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,37 +8,45 @@ export type Props = React.PropsWithChildren<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function DocumentLink({ childDocId, className, children }: Props) {
|
export function DocumentLink({ childDocId, className, children }: Props) {
|
||||||
const docPath = useDocumentPath();
|
// const docPath = useDocumentPath();
|
||||||
|
//
|
||||||
|
// const params = useParams({
|
||||||
|
// strict: false,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const campaignSearch = useSearch({
|
||||||
|
// from: "/_app/_authenticated/campaigns/$campaignId",
|
||||||
|
// shouldThrow: false,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const to = params.campaignId
|
||||||
|
// ? `/campaigns/${params.campaignId}`
|
||||||
|
// : docPath
|
||||||
|
// ? makeDocumentPath(
|
||||||
|
// docPath.documentId,
|
||||||
|
// docPath?.relationshipType,
|
||||||
|
// childDocId,
|
||||||
|
// )
|
||||||
|
// : undefined;
|
||||||
|
//
|
||||||
|
// const search = campaignSearch
|
||||||
|
// ? { tab: campaignSearch.tab, docId: childDocId }
|
||||||
|
// : undefined;
|
||||||
|
//
|
||||||
|
// if (to === undefined) {
|
||||||
|
// throw new Error("Not in a document or campaign context");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <Link to={to} search={search} className={className}>
|
||||||
|
// {children}
|
||||||
|
// </Link>
|
||||||
|
// );
|
||||||
|
|
||||||
const params = useParams({
|
const to = makeDocumentPath(childDocId);
|
||||||
strict: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const campaignSearch = useSearch({
|
|
||||||
from: "/_app/_authenticated/campaigns/$campaignId",
|
|
||||||
shouldThrow: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const to = params.campaignId
|
|
||||||
? `/campaigns/${params.campaignId}`
|
|
||||||
: docPath
|
|
||||||
? makeDocumentPath(
|
|
||||||
docPath.documentId,
|
|
||||||
docPath?.relationshipType,
|
|
||||||
childDocId,
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const search = campaignSearch
|
|
||||||
? { tab: campaignSearch.tab, docId: childDocId }
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (to === undefined) {
|
|
||||||
throw new Error("Not in a document or campaign context");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={to} search={search} className={className}>
|
<Link to={to} className={className}>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const GenericEditFormField = <T extends AnyDocument>({
|
|||||||
multiline={true}
|
multiline={true}
|
||||||
value={field.getter(data) as string}
|
value={field.getter(data) as string}
|
||||||
onSave={saveField}
|
onSave={saveField}
|
||||||
|
id={field.name}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "shortText":
|
case "shortText":
|
||||||
@@ -64,6 +65,7 @@ const GenericEditFormField = <T extends AnyDocument>({
|
|||||||
multiline={false}
|
multiline={false}
|
||||||
value={field.getter(data) as string}
|
value={field.getter(data) as string}
|
||||||
onSave={saveField}
|
onSave={saveField}
|
||||||
|
id={field.name}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "toggle":
|
case "toggle":
|
||||||
@@ -73,6 +75,7 @@ const GenericEditFormField = <T extends AnyDocument>({
|
|||||||
checked={!!field.getter(data)}
|
checked={!!field.getter(data)}
|
||||||
onChange={(e) => saveField(!!e.target.value)}
|
onChange={(e) => saveField(!!e.target.value)}
|
||||||
className="accent-emerald-500 w-5 h-5"
|
className="accent-emerald-500 w-5 h-5"
|
||||||
|
id={field.name}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
152
src/components/documents/GenericNewDocumentForm.tsx
Normal file
152
src/components/documents/GenericNewDocumentForm.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
|
||||||
|
import { useDocumentCache } from "@/context/document/hooks";
|
||||||
|
import {
|
||||||
|
getFieldsForType,
|
||||||
|
type DocumentField,
|
||||||
|
type FieldType,
|
||||||
|
type ValueForFieldType,
|
||||||
|
} from "@/lib/fields";
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import {
|
||||||
|
type CampaignId,
|
||||||
|
type DocumentData,
|
||||||
|
type DocumentsByType,
|
||||||
|
type DocumentType,
|
||||||
|
} from "@/lib/types";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
|
||||||
|
|
||||||
|
export type Props<T extends DocumentType> = {
|
||||||
|
docType: T;
|
||||||
|
campaignId: CampaignId;
|
||||||
|
onCreate: (doc: DocumentsByType[T]) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GenericNewDocumentForm = <T extends DocumentType>({
|
||||||
|
docType,
|
||||||
|
campaignId,
|
||||||
|
onCreate,
|
||||||
|
}: Props<T>) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { dispatch } = useDocumentCache();
|
||||||
|
|
||||||
|
const fields = getFieldsForType(docType);
|
||||||
|
|
||||||
|
const [docData, setDocData] = useState<DocumentData<T>>(
|
||||||
|
fields.reduce((d, f) => f.setDefault(d), {} as DocumentData<T>),
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateData =
|
||||||
|
<F extends FieldType>(field: DocumentField<T, F>) =>
|
||||||
|
(value: ValueForFieldType<F>) =>
|
||||||
|
setDocData(field.setter(value, docData));
|
||||||
|
|
||||||
|
const saveData = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
console.log(`Creating ${docType}: `, docData);
|
||||||
|
try {
|
||||||
|
const newDocument: DocumentsByType[T] = await pb
|
||||||
|
.collection("documents")
|
||||||
|
.create({
|
||||||
|
campaign: campaignId,
|
||||||
|
type: docType,
|
||||||
|
data: docData,
|
||||||
|
});
|
||||||
|
await onCreate(newDocument);
|
||||||
|
dispatch({
|
||||||
|
type: "setDocument",
|
||||||
|
doc: newDocument,
|
||||||
|
});
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
setError(e.message);
|
||||||
|
} else {
|
||||||
|
setError("An unknown error occurred while creating the session.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [campaignId, setIsLoading, setError, docData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
{error && (
|
||||||
|
// TODO: class and style for errors
|
||||||
|
<div className="error">{error}</div>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
// The type checker seems to lose the types when using Object.entries here.
|
||||||
|
fields.map((field) => (
|
||||||
|
<GenericNewFormField
|
||||||
|
field={field}
|
||||||
|
value={field.getter(docData)}
|
||||||
|
onUpdate={updateData(field)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<button disabled={isLoading} onClick={saveData}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GenericNewFormField = <T extends DocumentType, F extends FieldType>({
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
onUpdate,
|
||||||
|
}: {
|
||||||
|
field: DocumentField<T, F>;
|
||||||
|
value: ValueForFieldType<F>;
|
||||||
|
onUpdate: (value: ValueForFieldType<F>) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name}>{field.name}</label>
|
||||||
|
<GenericNewFormInput field={field} value={value} onUpdate={onUpdate} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GenericNewFormInput = <T extends DocumentType, F extends FieldType>({
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
onUpdate,
|
||||||
|
}: {
|
||||||
|
field: DocumentField<T, F>;
|
||||||
|
value: ValueForFieldType<F>;
|
||||||
|
onUpdate: (value: ValueForFieldType<F>) => void;
|
||||||
|
}) => {
|
||||||
|
switch (field.fieldType) {
|
||||||
|
case "longText":
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
value={value as string}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate((e.target.value || "") as ValueForFieldType<F>)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "shortText":
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value as string}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate((e.target.value || "") as ValueForFieldType<F>)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "toggle":
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={value as boolean}
|
||||||
|
onChange={(e) =>
|
||||||
|
(onUpdate as (value: boolean) => void)(!!e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type DocumentType,
|
type DocumentType,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import { NewSessionForm } from "./session/NewSessionForm";
|
import { NewSessionForm } from "./session/NewSessionForm";
|
||||||
|
import { GenericNewDocumentForm } from "./GenericNewDocumentForm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a form for any document type depending on the relationship.
|
* Renders a form for any document type depending on the relationship.
|
||||||
@@ -21,7 +22,14 @@ export const NewCampaignDocumentForm = ({
|
|||||||
case "session":
|
case "session":
|
||||||
return <NewSessionForm campaignId={campaignId} onCreate={onCreate} />;
|
return <NewSessionForm campaignId={campaignId} onCreate={onCreate} />;
|
||||||
case "thread":
|
case "thread":
|
||||||
return <NewThreadForm campaignId={campaignId} onCreate={onCreate} />;
|
case "location":
|
||||||
|
return (
|
||||||
|
<GenericNewDocumentForm
|
||||||
|
docType={docType}
|
||||||
|
campaignId={campaignId}
|
||||||
|
onCreate={onCreate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Rendered NewCampaignDocumentForm with unsupported docType: ${docType}`,
|
`Rendered NewCampaignDocumentForm with unsupported docType: ${docType}`,
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import {
|
import { type DocumentData, type DocumentType } from "./types";
|
||||||
type DocumentData,
|
|
||||||
type DocumentsByType,
|
|
||||||
type DocumentType,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
export type FieldType = "shortText" | "longText" | "toggle";
|
export type FieldType = "shortText" | "longText" | "toggle";
|
||||||
|
|
||||||
export type ValueForFieldType<T extends FieldType> = {
|
export type ValueForFieldType<F extends FieldType> = {
|
||||||
shortText: string;
|
shortText: string;
|
||||||
longText: string;
|
longText: string;
|
||||||
toggle: boolean;
|
toggle: boolean;
|
||||||
}[T];
|
}[F];
|
||||||
|
|
||||||
|
function defaultValue<F extends FieldType>(fieldType: F): ValueForFieldType<F> {
|
||||||
|
switch (fieldType) {
|
||||||
|
case "shortText":
|
||||||
|
case "longText":
|
||||||
|
return "" as ValueForFieldType<F>;
|
||||||
|
case "toggle":
|
||||||
|
return false as ValueForFieldType<F>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type DocumentField<D extends DocumentType, F extends FieldType> = {
|
export type DocumentField<D extends DocumentType, F extends FieldType> = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -20,6 +26,7 @@ export type DocumentField<D extends DocumentType, F extends FieldType> = {
|
|||||||
value: ValueForFieldType<F>,
|
value: ValueForFieldType<F>,
|
||||||
doc: DocumentData<D>,
|
doc: DocumentData<D>,
|
||||||
) => DocumentData<D>;
|
) => DocumentData<D>;
|
||||||
|
setDefault: (doc: DocumentData<D>) => DocumentData<D>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const simpleField = <D extends DocumentType, F extends FieldType>(
|
const simpleField = <D extends DocumentType, F extends FieldType>(
|
||||||
@@ -31,6 +38,7 @@ const simpleField = <D extends DocumentType, F extends FieldType>(
|
|||||||
fieldType,
|
fieldType,
|
||||||
getter: (doc) => doc[key] as unknown as ValueForFieldType<F>,
|
getter: (doc) => doc[key] as unknown as ValueForFieldType<F>,
|
||||||
setter: (value, doc) => ({ ...doc, [key]: value }),
|
setter: (value, doc) => ({ ...doc, [key]: value }),
|
||||||
|
setDefault: (doc) => ({ ...doc, [key]: defaultValue(fieldType) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const simpleFields = <D extends DocumentType>(
|
const simpleFields = <D extends DocumentType>(
|
||||||
|
|||||||
Reference in New Issue
Block a user