1
10
11export const MAX_BUTTON_ACTIONS = 5;
12
13export type ButtonActionType =
14 | "set_property"
15 | "create_page"
16 | "update_page"
17 | "archive_page"
18 | "duplicate_page"
19 | "send_webhook"
20 | "trigger_automation";
21
22export const BUTTON_ACTION_TYPES: readonly ButtonActionType[] = [
23 "set_property",
24 "create_page",
25 "update_page",
26 "archive_page",
27 "duplicate_page",
28 "send_webhook",
29 "trigger_automation",
30];
31
32export interface ButtonConfig {
33 name: string;
34 icon?: string;
35 color?: string;
36 actions: unknown[];
37}
38
39export interface ButtonValidationError {
40 field: string;
41 message: string;
42}
43
44
45export function validateButtonConfig(config: unknown): ButtonValidationError[] {
46 const errors: ButtonValidationError[] = [];
47 if (config === null || typeof config !== "object" || Array.isArray(config)) {
48 return [{ field: "config", message: "button config must be an object" }];
49 }
50 const c = config as Record<string, unknown>;
51 if (typeof c["name"] !== "string" || c["name"].length < 1 || c["name"].length > 50) {
52 errors.push({ field: "name", message: "name must be 1–50 characters" });
53 }
54 if (c["icon"] !== undefined && typeof c["icon"] !== "string") {
55 errors.push({ field: "icon", message: "icon must be an emoji string" });
56 }
57 if (c["color"] !== undefined && typeof c["color"] !== "string") {
58 errors.push({ field: "color", message: "color must be a token string" });
59 }
60 if (
61 !Array.isArray(c["actions"]) ||
62 c["actions"].length < 1 ||
63 c["actions"].length > MAX_BUTTON_ACTIONS
64 ) {
65 errors.push({
66 field: "actions",
67 message: `actions must be an array of 1–${MAX_BUTTON_ACTIONS}`,
68 });
69 return errors;
70 }
71 c["actions"].forEach((a, i) => {
72 const p = `actions[${i}]`;
73 if (a === null || typeof a !== "object" || Array.isArray(a)) {
74 errors.push({ field: p, message: "action must be an object" });
75 return;
76 }
77 const o = a as Record<string, unknown>;
78 const type = o["type"];
79 if (typeof type !== "string" || !BUTTON_ACTION_TYPES.includes(type as ButtonActionType)) {
80 errors.push({
81 field: `${p}.type`,
82 message: `action type must be one of ${BUTTON_ACTION_TYPES.join(", ")}`,
83 });
84 return;
85 }
86 switch (type) {
87 case "set_property":
88 if (typeof o["propertyId"] !== "string")
89 errors.push({ field: `${p}.propertyId`, message: "required" });
90 if (o["value"] === undefined && o["setTo"] === undefined)
91 errors.push({ field: `${p}.value`, message: "value or setTo required" });
92 break;
93 case "create_page":
94 if (o["databaseId"] !== undefined && typeof o["databaseId"] !== "string")
95 errors.push({ field: `${p}.databaseId`, message: "must be a string" });
96 break;
97 case "update_page":
98 if (typeof o["patch"] !== "object" || o["patch"] === null)
99 errors.push({ field: `${p}.patch`, message: "required" });
100 break;
101 case "archive_page":
102 case "duplicate_page":
103 if (
104 o["target"] !== "self" &&
105 (typeof o["target"] !== "string" || o["target"].length === 0)
106 ) {
107 errors.push({ field: `${p}.target`, message: "'self' or a page id" });
108 }
109 break;
110 case "send_webhook":
111 if (typeof o["webhookId"] !== "string" || o["webhookId"].length === 0)
112 errors.push({ field: `${p}.webhookId`, message: "required" });
113 if (o["payloadTemplate"] !== undefined && typeof o["payloadTemplate"] !== "string") {
114 errors.push({ field: `${p}.payloadTemplate`, message: "must be a JSON string" });
115 }
116 break;
117 case "trigger_automation":
118 if (typeof o["automationId"] !== "string" || o["automationId"].length === 0)
119 errors.push({ field: `${p}.automationId`, message: "required" });
120 break;
121 }
122 });
123 return errors;
124}
125
126
131export function resolveTemplate(
132 template: string,
133 title: string | undefined,
134 values: Record<string, { display: string }>,
135): string {
136 const lookup = new Map<string, string>();
137 if (title !== undefined) lookup.set("title", title);
138 for (const [key, v] of Object.entries(values)) lookup.set(key.toLowerCase(), v.display);
139 return template.replace(
140 /\{([A-Za-z0-9_]+)\}/g,
141 (_m, name: string) => lookup.get(name.toLowerCase()) ?? "",
142 );
143}
144
145
146export function describeAction(
147 action: Record<string, unknown>,
148 propertyNames: Map<string, string>,
149): string {
150 const type = action["type"];
151 switch (type) {
152 case "set_property": {
153 const name = propertyNames.get(String(action["propertyId"])) ?? "property";
154 if (action["setTo"] === "empty") return `Clear ${name}`;
155 return `Set ${name}`;
156 }
157 case "create_page":
158 return action["databaseId"] ? "Create a record" : "Create a page";
159 case "update_page":
160 return "Update page";
161 case "archive_page":
162 return action["target"] === "self" ? "Archive this record" : "Archive page";
163 case "duplicate_page":
164 return action["target"] === "self" ? "Duplicate this record" : "Duplicate page";
165 case "send_webhook":
166 return "Send webhook";
167 case "trigger_automation":
168 return "Trigger automation";
169 default:
170 return String(type ?? "action");
171 }
172}
173