| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- import React, {
- createContext,
- useContext,
- useState,
- useCallback,
- useMemo,
- type ReactNode,
- } from "react";
- import {
- resolveAction,
- executeAction,
- type ActionBinding,
- type ActionHandler,
- type ActionConfirm,
- type ResolvedAction,
- } from "@json-render/core";
- import { useStateStore } from "./state";
- let idCounter = 0;
- function generateUniqueId(): string {
- idCounter += 1;
- return `${Date.now()}-${idCounter}`;
- }
- function deepResolveValue(
- value: unknown,
- get: (path: string) => unknown,
- ): unknown {
- if (value === null || value === undefined) return value;
- if (value === "$id") {
- return generateUniqueId();
- }
- if (typeof value === "object" && !Array.isArray(value)) {
- const obj = value as Record<string, unknown>;
- const keys = Object.keys(obj);
- if (keys.length === 1 && typeof obj.$state === "string") {
- return get(obj.$state as string);
- }
- if (keys.length === 1 && "$id" in obj) {
- return generateUniqueId();
- }
- }
- if (Array.isArray(value)) {
- return value.map((item) => deepResolveValue(item, get));
- }
- if (typeof value === "object") {
- const resolved: Record<string, unknown> = {};
- for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
- resolved[key] = deepResolveValue(val, get);
- }
- return resolved;
- }
- return value;
- }
- export interface PendingConfirmation {
- action: ResolvedAction;
- handler: ActionHandler;
- resolve: () => void;
- reject: () => void;
- }
- export interface ActionContextValue {
- handlers: Record<string, ActionHandler>;
- loadingActions: Set<string>;
- pendingConfirmation: PendingConfirmation | null;
- execute: (binding: ActionBinding) => Promise<void>;
- confirm: () => void;
- cancel: () => void;
- registerHandler: (name: string, handler: ActionHandler) => void;
- }
- const ActionContext = createContext<ActionContextValue | null>(null);
- export interface ActionProviderProps {
- handlers?: Record<string, ActionHandler>;
- navigate?: (path: string) => void;
- children: ReactNode;
- }
- export function ActionProvider({
- handlers: initialHandlers = {},
- navigate,
- children,
- }: ActionProviderProps) {
- const { state, get, set } = useStateStore();
- const [handlers, setHandlers] =
- useState<Record<string, ActionHandler>>(initialHandlers);
- const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());
- const [pendingConfirmation, setPendingConfirmation] =
- useState<PendingConfirmation | null>(null);
- const registerHandler = useCallback(
- (name: string, handler: ActionHandler) => {
- setHandlers((prev) => ({ ...prev, [name]: handler }));
- },
- [],
- );
- const execute = useCallback(
- async (binding: ActionBinding) => {
- const resolved = resolveAction(binding, state);
- if (resolved.action === "setState" && resolved.params) {
- const statePath = resolved.params.statePath as string;
- const value = resolved.params.value;
- if (statePath) {
- set(statePath, value);
- }
- return;
- }
- if (resolved.action === "pushState" && resolved.params) {
- const statePath = resolved.params.statePath as string;
- const rawValue = resolved.params.value;
- if (statePath) {
- const resolvedValue = deepResolveValue(rawValue, get);
- const arr = (get(statePath) as unknown[] | undefined) ?? [];
- set(statePath, [...arr, resolvedValue]);
- const clearStatePath = resolved.params.clearStatePath as
- | string
- | undefined;
- if (clearStatePath) {
- set(clearStatePath, "");
- }
- }
- return;
- }
- if (resolved.action === "removeState" && resolved.params) {
- const statePath = resolved.params.statePath as string;
- const index = resolved.params.index as number;
- if (statePath !== undefined && index !== undefined) {
- const arr = (get(statePath) as unknown[] | undefined) ?? [];
- set(
- statePath,
- arr.filter((_, i) => i !== index),
- );
- }
- return;
- }
- const handler = handlers[resolved.action];
- if (!handler) {
- console.warn(`No handler registered for action: ${resolved.action}`);
- return;
- }
- if (resolved.confirm) {
- return new Promise<void>((resolve, reject) => {
- setPendingConfirmation({
- action: resolved,
- handler,
- resolve: () => {
- setPendingConfirmation(null);
- resolve();
- },
- reject: () => {
- setPendingConfirmation(null);
- reject(new Error("Action cancelled"));
- },
- });
- }).then(async () => {
- setLoadingActions((prev) => new Set(prev).add(resolved.action));
- try {
- await executeAction({
- action: resolved,
- handler,
- setState: set,
- navigate,
- executeAction: async (name) => {
- const subBinding: ActionBinding = { action: name };
- await execute(subBinding);
- },
- });
- } finally {
- setLoadingActions((prev) => {
- const next = new Set(prev);
- next.delete(resolved.action);
- return next;
- });
- }
- });
- }
- setLoadingActions((prev) => new Set(prev).add(resolved.action));
- try {
- await executeAction({
- action: resolved,
- handler,
- setState: set,
- navigate,
- executeAction: async (name) => {
- const subBinding: ActionBinding = { action: name };
- await execute(subBinding);
- },
- });
- } finally {
- setLoadingActions((prev) => {
- const next = new Set(prev);
- next.delete(resolved.action);
- return next;
- });
- }
- },
- [state, handlers, get, set, navigate],
- );
- const confirm = useCallback(() => {
- pendingConfirmation?.resolve();
- }, [pendingConfirmation]);
- const cancel = useCallback(() => {
- pendingConfirmation?.reject();
- }, [pendingConfirmation]);
- const value = useMemo<ActionContextValue>(
- () => ({
- handlers,
- loadingActions,
- pendingConfirmation,
- execute,
- confirm,
- cancel,
- registerHandler,
- }),
- [
- handlers,
- loadingActions,
- pendingConfirmation,
- execute,
- confirm,
- cancel,
- registerHandler,
- ],
- );
- return (
- <ActionContext.Provider value={value}>{children}</ActionContext.Provider>
- );
- }
- export function useActions(): ActionContextValue {
- const ctx = useContext(ActionContext);
- if (!ctx) {
- throw new Error("useActions must be used within an ActionProvider");
- }
- return ctx;
- }
- export function useAction(binding: ActionBinding): {
- execute: () => Promise<void>;
- isLoading: boolean;
- } {
- const { execute, loadingActions } = useActions();
- const isLoading = loadingActions.has(binding.action);
- const executeAction = useCallback(() => execute(binding), [execute, binding]);
- return { execute: executeAction, isLoading };
- }
- export interface ConfirmDialogProps {
- confirm: ActionConfirm;
- onConfirm: () => void;
- onCancel: () => void;
- }
- /**
- * No-op confirm dialog for PDF context. PDFs are non-interactive,
- * so confirmations are not rendered.
- */
- export function ConfirmDialog(_props: ConfirmDialogProps) {
- return null;
- }
|