Skip to main content
Glama

Storyden

by Southclaws
Mozilla Public License 2.0
229
store.ts14.6 kB
import { dequal } from "dequal"; import { uniqueId } from "lodash"; import { immer } from "zustand/middleware/immer"; import { useStoreWithEqualityFn } from "zustand/traditional"; import { createStore } from "zustand/vanilla"; import { handle } from "@/api/client"; import { Asset, Identifier, LinkReference, NodeMutableProps, NodeWithChildren, NodeWithChildrenAllOf, PropertyName, PropertySchema, PropertyType, } from "@/api/openapi-schema"; import { MutationSet, deriveMutationFromDifference } from "@/lib/library/diff"; import { CoverImageArgs } from "@/lib/library/library"; import { DefaultLayout, LibraryPageBlock, LibraryPageBlockType, WithMetadata, } from "@/lib/library/metadata"; import { applyNodeChanges } from "@/lib/library/mutators"; import { useLibraryPageContext } from "./Context"; export type State = { original: WithMetadata<NodeWithChildren>; draft: WithMetadata<NodeWithChildren>; }; export type Actions = { // Simple mutations - direct draft changes and mutation events. setName: (v: string) => void; setSlug: (v: string) => void; setContent: (v: string) => void; setTags: (tags: string[]) => void; // Complex mutations - require slightly more logic to change the draft. setPrimaryImage(args: CoverImageArgs): void; removePrimaryImage(): void; setLink: (link: LinkReference) => void; removeLink: () => void; addAsset: (asset: Asset) => void; removeAsset: (asset: Asset) => void; // Properties addProperty: (name: PropertyName, type: PropertyType, value?: string) => void; removePropertyByName: (name: PropertyName) => void; removePropertyByID: (id: Identifier) => void; setPropertyName: (name: PropertyName, newName: PropertyName) => void; setPropertyValue: (name: PropertyName, value: string) => void; // Properties, from parent perspective addChildProperty(p: PropertySchema): void; removeChildPropertyByID: (id: Identifier) => void; setChildPropertyName: (name: PropertyName, newName: PropertyName) => void; setChildPropertyValue: ( nodeID: Identifier, fid: string, value: string, ) => void; setChildPropertyHiddenState: (fid: string, hidden: boolean) => void; // Layout blocks moveBlock: (type: LibraryPageBlockType, newIndex: number) => void; addBlock: (type: LibraryPageBlockType, index?: number) => void; removeBlock: (type: LibraryPageBlockType) => void; overwriteBlock: (type: LibraryPageBlock) => void; commit: ( callback: (draft: MutationSet) => Promise<NodeWithChildrenAllOf>, ) => Promise<void>; }; export type Store = State & Actions; export type NodeStoreAPI = ReturnType<typeof createNodeStore>; export const createNodeStore = (initState: State) => { return createStore<Store>()( immer((set, get) => { const simplePatch = (data: NodeMutableProps) => set((state) => { const newState = applyNodeChanges(state.draft, data); Object.assign(state.draft, newState); }); const commit = async ( callback: (draft: MutationSet) => Promise<NodeWithChildrenAllOf>, ) => { const current = get().original; const draft = get().draft; const mutation = deriveMutationFromDifference(current, draft); if (mutation.clean) { console.debug("skipping commit: no changes"); return; } console.debug(`applying commit: `, mutation); const updated = await handle( async () => { return await callback(mutation); }, { errorToast: true, }, ); if (updated) { set(() => ({ original: updated, draft: updated, })); } }; return { ...initState, // - // Simple mutations // - setName: (name) => simplePatch({ name }), setSlug: (slug) => simplePatch({ slug }), setContent: (content) => simplePatch({ content }), setTags: (tags) => simplePatch({ tags }), // - // Cover image // - setPrimaryImage: (coverConfig: CoverImageArgs) => { if (coverConfig.isReplacement) { set((state) => { state.draft.primary_image = coverConfig.asset; state.draft.meta = { ...state.draft.meta, coverImage: null, }; }); } else { set((state) => { state.draft.primary_image = coverConfig.asset; state.draft.meta = { ...state.draft.meta, coverImage: coverConfig.config, }; }); } }, removePrimaryImage: () => { set((state) => { state.draft.primary_image = undefined; state.draft.meta = { ...state.draft.meta, coverImage: null, }; }); }, setLink: (link: LinkReference) => { set((state) => { state.draft.link = link; }); }, removeLink: () => { set((state) => { state.draft.link = undefined; }); }, addAsset: (asset: Asset) => { set((state) => { state.draft.assets.push(asset); }); }, removeAsset: (asset: Asset) => { set((state) => { state.draft.assets = state.draft.assets.filter( (a) => a.id !== asset.id, ); }); }, // - // Property management // - addProperty: ( name: PropertyName, type: PropertyType, value?: string, ) => { set((state) => { const existingNames = new Set( state.draft.properties.map((f) => f.name), ); let newName = name; let counter = 1; while (existingNames.has(newName)) { newName = `${name} ${counter++}`; } state.draft.properties.push({ fid: uniqueId("new_field_"), name: newName, type, sort: "5", // TODO: refine later value: value ?? "", }); }); }, removePropertyByName: (name: PropertyName) => { set((state) => { state.draft.properties = state.draft.properties.filter( (f) => f.name !== name, ); }); }, removePropertyByID: (id: Identifier) => { set((state) => { state.draft.properties = state.draft.properties.filter( (f) => f.fid !== id, ); }); }, setPropertyName: (name: PropertyName, newName: PropertyName) => { set((state) => { const target = state.draft.properties.find((f) => f.name === name); if (target) { target.name = newName; } }); }, setPropertyValue: (name: PropertyName, value: string) => { set((state) => { const target = state.draft.properties.find((f) => f.name === name); if (target) { target.value = value; } }); }, // Child properties - used from parent perspective addChildProperty(newProperty: PropertySchema): void { set((state) => { const newColumn = { fid: newProperty.fid, hidden: false, }; const layout = (state.draft.meta.layout ??= structuredClone(DefaultLayout)); for (const block of layout.blocks) { if (block.type !== "directory") continue; // config might not be defined yet, it should be in all cases, but // typescript is unsure (so am i) so just to be safe, set default. if (!block.config) { block.config = { layout: "table", columns: [] }; } else if (!block.config.columns) { block.config.columns = []; } block.config.columns.push(newColumn); } state.draft.child_property_schema.push(newProperty); }); }, removeChildPropertyByID: (fid: Identifier) => { set((state) => { const layout = state.draft.meta.layout; if (!layout) return; for (const block of layout.blocks) { if (block.type !== "directory") continue; if (!block.config?.columns) continue; const columns = block.config.columns; const index = columns.findIndex((col) => col.fid === fid); if (index !== -1) { columns.splice(index, 1); } } const schema = state.draft.child_property_schema; const schemaIndex = schema.findIndex((p) => p.fid === fid); if (schemaIndex !== -1) { schema.splice(schemaIndex, 1); } }); }, setChildPropertyName: (fid: Identifier, newName: PropertyName) => { set((state) => { const target = state.draft.child_property_schema.find( (f) => f.fid === fid, ); if (target) { target.name = newName; } }); }, setChildPropertyValue: ( nodeID: Identifier, fid: string, value: string, ) => { set((state) => { const isFixed = fid.startsWith("fixed:"); const target = state.draft.children.find((f) => f.id === nodeID); if (!target) { console.warn( "Attempting to set property on non-existing child node", { nodeID, fid, value, }, ); return; } console.debug("Setting child property value", { nodeID, fid, value, isFixed, }); if (isFixed) { switch (fid) { case "fixed:name": { target.name = value; break; } case "fixed:description": { target.description = value; break; } case "fixed:link": { // NOTE: The actual value here is only worried about the URL. // see diff.ts projectNodeToMutableProps for why. Since this // is a mutation only, the other fields don't matter. target.link = { url: value, } as LinkReference; break; } } } else { const prop = target.properties.find((p) => p.fid === fid); if (!prop) { console.warn( "Attempting to set value on non-existing property", { nodeID, fid, value, }, ); return; } prop.value = value; } }); }, setChildPropertyHiddenState: (fid: string, hidden: boolean) => { set((state) => { const layout = (state.draft.meta.layout ??= DefaultLayout); const blocks = layout.blocks; for (const block of blocks) { if (block.type !== "directory") continue; if (!block.config?.columns) continue; for (const col of block.config.columns) { if (col.fid === fid) { col.hidden = hidden; return; } } } }); }, // - // Block management // - moveBlock: (type: LibraryPageBlockType, newIndex: number) => { set((state) => { const layout = (state.draft.meta.layout ??= DefaultLayout); const blocks = layout.blocks; const activeIndex = blocks.findIndex((b) => b.type === type); if ( activeIndex === -1 || newIndex === -1 || activeIndex === newIndex ) { return; } const [moved] = blocks.splice(activeIndex, 1); if (!moved) { return; } blocks.splice(newIndex, 0, moved); }); }, addBlock: (type: LibraryPageBlockType, index?: number) => { set((state) => { const layout = (state.draft.meta.layout ??= DefaultLayout); // check if the block already exists, if it does, return if (layout.blocks.some((b) => b.type === type)) { return; } if (index === undefined || index > layout.blocks.length) { layout.blocks.push({ type }); } else { layout.blocks.splice(index + 1, 0, { type }); } }); }, removeBlock: (type: LibraryPageBlockType) => { set((state) => { const layout = state.draft.meta.layout; if (!layout) { return; } const blocks = layout.blocks; // check if the block exists, if it does, remove it const index = blocks.findIndex((b) => b.type === type); if (index === -1) { return; } blocks.splice(index, 1); }); }, overwriteBlock: (block: LibraryPageBlock) => { set((state) => { if (state.draft.meta.layout === undefined) { state.draft.meta.layout = DefaultLayout; } // check if the block exists, if not, do nothing. const index = state.draft.meta.layout.blocks.findIndex( (b) => b.type === block.type, ); if (index === -1) { return; } state.draft.meta.layout.blocks[index] = block; }); }, commit, }; }), ); }; export function useWatch<T>(selector: (state: State) => T): T { const { store } = useLibraryPageContext(); return useStoreWithEqualityFn(store, selector, dequal); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server