actions.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import { z } from "zod";
  2. import type { DynamicValue, StateModel } from "./types";
  3. import { DynamicValueSchema, resolveDynamicValue } from "./types";
  4. /**
  5. * Confirmation dialog configuration
  6. */
  7. export interface ActionConfirm {
  8. title: string;
  9. message: string;
  10. confirmLabel?: string;
  11. cancelLabel?: string;
  12. variant?: "default" | "danger";
  13. }
  14. /**
  15. * Action success handler
  16. */
  17. export type ActionOnSuccess =
  18. | { navigate: string }
  19. | { set: Record<string, unknown> }
  20. | { action: string };
  21. /**
  22. * Action error handler
  23. */
  24. export type ActionOnError =
  25. | { set: Record<string, unknown> }
  26. | { action: string };
  27. /**
  28. * Action binding — maps an event to an action invocation.
  29. *
  30. * Used inside the `on` field of a UIElement:
  31. * ```json
  32. * { "on": { "press": { "action": "setState", "params": { "path": "/x", "value": 1 } } } }
  33. * ```
  34. */
  35. export interface ActionBinding {
  36. /** Action name (must be in catalog) */
  37. action: string;
  38. /** Parameters to pass to the action handler */
  39. params?: Record<string, DynamicValue>;
  40. /** Confirmation dialog before execution */
  41. confirm?: ActionConfirm;
  42. /** Handler after successful execution */
  43. onSuccess?: ActionOnSuccess;
  44. /** Handler after failed execution */
  45. onError?: ActionOnError;
  46. }
  47. /**
  48. * @deprecated Use ActionBinding instead
  49. */
  50. export type Action = ActionBinding;
  51. /**
  52. * Schema for action confirmation
  53. */
  54. export const ActionConfirmSchema = z.object({
  55. title: z.string(),
  56. message: z.string(),
  57. confirmLabel: z.string().optional(),
  58. cancelLabel: z.string().optional(),
  59. variant: z.enum(["default", "danger"]).optional(),
  60. });
  61. /**
  62. * Schema for success handlers
  63. */
  64. export const ActionOnSuccessSchema = z.union([
  65. z.object({ navigate: z.string() }),
  66. z.object({ set: z.record(z.string(), z.unknown()) }),
  67. z.object({ action: z.string() }),
  68. ]);
  69. /**
  70. * Schema for error handlers
  71. */
  72. export const ActionOnErrorSchema = z.union([
  73. z.object({ set: z.record(z.string(), z.unknown()) }),
  74. z.object({ action: z.string() }),
  75. ]);
  76. /**
  77. * Full action binding schema
  78. */
  79. export const ActionBindingSchema = z.object({
  80. action: z.string(),
  81. params: z.record(z.string(), DynamicValueSchema).optional(),
  82. confirm: ActionConfirmSchema.optional(),
  83. onSuccess: ActionOnSuccessSchema.optional(),
  84. onError: ActionOnErrorSchema.optional(),
  85. });
  86. /**
  87. * @deprecated Use ActionBindingSchema instead
  88. */
  89. export const ActionSchema = ActionBindingSchema;
  90. /**
  91. * Action handler function signature
  92. */
  93. export type ActionHandler<
  94. TParams = Record<string, unknown>,
  95. TResult = unknown,
  96. > = (params: TParams) => Promise<TResult> | TResult;
  97. /**
  98. * Action definition in catalog
  99. */
  100. export interface ActionDefinition<TParams = Record<string, unknown>> {
  101. /** Zod schema for params validation */
  102. params?: z.ZodType<TParams>;
  103. /** Description for AI */
  104. description?: string;
  105. }
  106. /**
  107. * Resolved action with all dynamic values resolved
  108. */
  109. export interface ResolvedAction {
  110. action: string;
  111. params: Record<string, unknown>;
  112. confirm?: ActionConfirm;
  113. onSuccess?: ActionOnSuccess;
  114. onError?: ActionOnError;
  115. }
  116. /**
  117. * Resolve all dynamic values in an action binding
  118. */
  119. export function resolveAction(
  120. binding: ActionBinding,
  121. stateModel: StateModel,
  122. ): ResolvedAction {
  123. const resolvedParams: Record<string, unknown> = {};
  124. if (binding.params) {
  125. for (const [key, value] of Object.entries(binding.params)) {
  126. resolvedParams[key] = resolveDynamicValue(value, stateModel);
  127. }
  128. }
  129. // Interpolate confirmation message if present
  130. let confirm = binding.confirm;
  131. if (confirm) {
  132. confirm = {
  133. ...confirm,
  134. message: interpolateString(confirm.message, stateModel),
  135. title: interpolateString(confirm.title, stateModel),
  136. };
  137. }
  138. return {
  139. action: binding.action,
  140. params: resolvedParams,
  141. confirm,
  142. onSuccess: binding.onSuccess,
  143. onError: binding.onError,
  144. };
  145. }
  146. /**
  147. * Interpolate ${path} expressions in a string
  148. */
  149. export function interpolateString(
  150. template: string,
  151. stateModel: StateModel,
  152. ): string {
  153. return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
  154. const value = resolveDynamicValue({ path }, stateModel);
  155. return String(value ?? "");
  156. });
  157. }
  158. /**
  159. * Context for action execution
  160. */
  161. export interface ActionExecutionContext {
  162. /** The resolved action */
  163. action: ResolvedAction;
  164. /** The action handler from the host */
  165. handler: ActionHandler;
  166. /** Function to update state model */
  167. setState: (path: string, value: unknown) => void;
  168. /** Function to navigate */
  169. navigate?: (path: string) => void;
  170. /** Function to execute another action */
  171. executeAction?: (name: string) => Promise<void>;
  172. }
  173. /**
  174. * Execute an action with all callbacks
  175. */
  176. export async function executeAction(
  177. ctx: ActionExecutionContext,
  178. ): Promise<void> {
  179. const { action, handler, setState, navigate, executeAction } = ctx;
  180. try {
  181. await handler(action.params);
  182. // Handle success
  183. if (action.onSuccess) {
  184. if ("navigate" in action.onSuccess && navigate) {
  185. navigate(action.onSuccess.navigate);
  186. } else if ("set" in action.onSuccess) {
  187. for (const [path, value] of Object.entries(action.onSuccess.set)) {
  188. setState(path, value);
  189. }
  190. } else if ("action" in action.onSuccess && executeAction) {
  191. await executeAction(action.onSuccess.action);
  192. }
  193. }
  194. } catch (error) {
  195. // Handle error
  196. if (action.onError) {
  197. if ("set" in action.onError) {
  198. for (const [path, value] of Object.entries(action.onError.set)) {
  199. // Replace $error.message with actual error
  200. const resolvedValue =
  201. typeof value === "string" && value === "$error.message"
  202. ? (error as Error).message
  203. : value;
  204. setState(path, resolvedValue);
  205. }
  206. } else if ("action" in action.onError && executeAction) {
  207. await executeAction(action.onError.action);
  208. }
  209. } else {
  210. throw error;
  211. }
  212. }
  213. }
  214. /**
  215. * Helper to create action bindings
  216. */
  217. export const actionBinding = {
  218. /** Create a simple action binding */
  219. simple: (
  220. actionName: string,
  221. params?: Record<string, DynamicValue>,
  222. ): ActionBinding => ({
  223. action: actionName,
  224. params,
  225. }),
  226. /** Create an action binding with confirmation */
  227. withConfirm: (
  228. actionName: string,
  229. confirm: ActionConfirm,
  230. params?: Record<string, DynamicValue>,
  231. ): ActionBinding => ({
  232. action: actionName,
  233. params,
  234. confirm,
  235. }),
  236. /** Create an action binding with success handler */
  237. withSuccess: (
  238. actionName: string,
  239. onSuccess: ActionOnSuccess,
  240. params?: Record<string, DynamicValue>,
  241. ): ActionBinding => ({
  242. action: actionName,
  243. params,
  244. onSuccess,
  245. }),
  246. };
  247. /**
  248. * @deprecated Use actionBinding instead
  249. */
  250. export const action = actionBinding;