diff --git a/apps/docs/pages/developer/_meta.js b/apps/docs/pages/developer/_meta.js index 4004ef6e..e8a52431 100644 --- a/apps/docs/pages/developer/_meta.js +++ b/apps/docs/pages/developer/_meta.js @@ -4,4 +4,5 @@ export default { "saving-manifests": { title: "Saving manifests" }, configuration: { title: "Configuration" }, previews: { title: "Previews" }, + creators: { title: "Resource creators" }, }; diff --git a/apps/docs/pages/developer/creators.mdx b/apps/docs/pages/developer/creators.mdx new file mode 100644 index 00000000..831297c8 --- /dev/null +++ b/apps/docs/pages/developer/creators.mdx @@ -0,0 +1,161 @@ +# Resource creators + +The primary functionality of the Manifest Editor is based around the ability to create, update and delete resources. + +A resource creator is made up of a few different parts: + +- A form to create the resource, or custom UI +- An asyncronous function to save the resource +- Metadata about what it creators, and which fields + +A simple example of a resource creator is the `ImageUrlCreator`. + +The form is a simple text input, with a URL to an image hosted somewhere - and a button to submit the form. + +```tsx +import { FormEvent } from "react"; +import { InputContainer, InputLabel, Input } from "@manifest-editor/editors"; +import { CreatorContext } from "@manifest-editor/creator-api"; + +export function CreateImageUrlForm(props: CreatorContext) { + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + const data = new FormData(e.target as HTMLFormElement); + const formData = Object.fromEntries(data.entries()) as any; + + if (formData.url) { + props.runCreate({ url: formData.url }); + } + }; + + return ( +
+ + Link to Image + + + + +
+ ); +} +``` + +The props passed to this component are provided by the `CreatorContext`, which includes the +`props.runCreate()` which is also defined by the creator. + +This is the creator function. This is used by the Form, but can also be used by _other creators_ so you +can compose them together to create more complex resources. + +```ts +import { getImageDimensions, getFormat } from "@manifest-editor/shell"; +import { CreatorFunctionContext } from "@manifest-editor/creator-api"; + +export interface CreateImageUrlPayload { + url: string; + format?: string; + height?: number; + width?: number; +} + +export async function createImageUrl( + data: CreateImageUrlPayload, + ctx: CreatorFunctionContext +): Promise { + if (!data.height || !data.width) { + const dimensions = await getImageDimensions(data.url); + if (dimensions) { + data.height = dimensions.height; + data.width = dimensions.width; + } + } + + return ctx.embed({ + id: data.url, + type: "Image", + format: data.format || (await getFormat(data.url)), + height: data.height, + width: data.width, + }); +} +``` + +Whatever you return from this function is what will be saved to the parent resource. + +You can see what the parent resource is by looking at the `CreatorContext` along with some helpers +for correctly creating references and linking them together in the Vault: + +```ts +interface CreatorFunctionContext { + options: { + targetType: string; + target?: Reference; + parent?: CreatorParent; + initialData?: any; + }; + ref(idOrRef: string | Reference): ReferencedResource; + embed(data: any): CreatorResource; + create(definition: string, payload: any, options?: Partial): Promise; + generateId(type: string, parent?: Reference | ReferencedResource): string; + getParent(): Reference | undefined; + getTarget(): SpecificResource | Reference | undefined; + getParentResource(): SpecificResource | undefined; + getPreviewVault(): Vault; +} +``` + +This is required because of how the Vault stores resources as flat resources. In most +cases you want to save embedded resources to the Vault and reference them by an ID and type. + +There are some exceptions - such as `service` properties, in which case you can use the `embed()` helper. + +There are a lot of examples in the GitHub repository of different ways to use these functions. + +These functions are bound together in a `CreatorDefinition`: + +```ts +import { CreatorDefinition } from "@manifest-editor/creator-api"; + +export const imageUrlCreator: CreatorDefinition = { + id: "@manifest-editor/image-url-creator", + create: createImageUrl, + label: "Image", + summary: "Image from a URL", + icon: , + render(ctx) { + return ; + }, + resourceType: "ContentResource", + resourceFields: ["format"], + supports: { + parentFields: ["logo", "body", "thumbnail"], + }, + staticFields: { + type: "Image", + }, +}; +``` + +This definition specifies some components and labels that will be displayed to users when they go +to use your creator from the UI. + +You can add new creators to your `mapApp` function when creating the Editor. + +```ts +import { mapApp } from "@manifest-editor/shell"; +import * as manifestEditorPreset from "@manifest-editor/manifest-preset"; +import { myCustomCreator } from "./my-custom-creator"; + +const app = mapApp(manifestEditorPreset, (app) => ({ + ...app, + creators: [ + // + ...app.creators, + myCustomCreator, + ], +})); +``` + +Now in the Manifest editor, when the "create" icon is clicked in a valid context - based on the configuration +you provided - they will see your icon and be able to click on it to use your Form component +to create a new resource.