Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: feature request duplication

Hugo 53242dd5 bd3b3f8a

+121 -17
+1
app/components/CodeBlock/styles.css.ts
··· 25 25 paddingInline: "6px", 26 26 paddingBlock: "2px", 27 27 borderRadius: radii.sm, 28 + overflowWrap: "anywhere", 28 29 });
-1
app/components/Layout/Container/styles.css.ts
··· 6 6 maxInlineSize: "72rem", 7 7 marginInline: "auto", 8 8 paddingInline: space[4], 9 - inlineSize: "100%", 10 9 "@media": { 11 10 [mq.md]: { 12 11 paddingInline: space[6],
+3
app/icons.ts
··· 6 6 // @ts-expect-error deep import 7 7 import ActivityIcon from "lucide-preact/icons/activity.js"; 8 8 // @ts-expect-error deep import 9 + import CopyIcon from "lucide-preact/icons/copy.js"; 10 + // @ts-expect-error deep import 9 11 import ChevronDownIcon from "lucide-preact/icons/chevron-down.js"; 10 12 // @ts-expect-error deep import 11 13 import ChevronRightIcon from "lucide-preact/icons/chevron-right.js"; ··· 47 49 48 50 export const Activity = cast(ActivityIcon); 49 51 export const ArrowLeft = cast(ArrowLeftIcon); 52 + export const Copy = cast(CopyIcon); 50 53 export const ChevronDown = cast(ChevronDownIcon); 51 54 export const ChevronRight = cast(ChevronRightIcon); 52 55 export const CircleAlert = cast(CircleAlertIcon);
+7 -5
app/islands/AutomationForm.css.ts
··· 98 98 99 99 export const conditionRow = style({ 100 100 display: "flex", 101 + flexWrap: "wrap", 101 102 gap: space[2], 102 103 alignItems: "flex-start", 103 104 }); 104 105 105 106 export const conditionField = style({ 106 - flex: 1, 107 + flex: "1 1 140px", 107 108 }); 108 109 109 110 export const conditionOperator = style({ 110 - flex: 1, 111 + flex: "1 1 140px", 111 112 }); 112 113 113 114 export const conditionValue = style({ 114 - flex: 1, 115 + flex: "1 1 140px", 115 116 }); 116 117 117 118 export const removeBtn = style({ ··· 258 259 259 260 export const fetchRow = style({ 260 261 display: "flex", 262 + flexWrap: "wrap", 261 263 gap: space[2], 262 264 alignItems: "flex-start", 263 265 }); 264 266 265 267 export const fetchName = style({ 266 - flex: 1, 268 + flex: "1 1 140px", 267 269 }); 268 270 269 271 export const fetchUri = style({ 270 - flex: 2, 272 + flex: "2 1 200px", 271 273 }); 272 274 273 275 export const collapsibleDetails = style({
+4 -4
app/islands/AutomationForm.tsx
··· 29 29 type ActionDraft = WebhookDraft | RecordDraft; 30 30 31 31 export type AutomationInitial = { 32 - rkey: string; 32 + rkey?: string; 33 33 name: string; 34 34 description: string | null; 35 35 lexicon: string; ··· 294 294 } 295 295 296 296 export default function AutomationForm({ initial }: { initial?: AutomationInitial }) { 297 - const isEdit = !!initial; 297 + const isEdit = !!initial?.rkey; 298 298 const initialLexicon = initial?.lexicon ?? getInitialParam("lexicon"); 299 299 const [name, setName] = useState(initial?.name ?? ""); 300 300 const [description, setDescription] = useState(initial?.description ?? ""); ··· 505 505 setError(""); 506 506 setSubmitting(true); 507 507 try { 508 - const url = isEdit ? `/api/automations/${initial.rkey}` : "/api/automations"; 508 + const url = isEdit ? `/api/automations/${initial!.rkey}` : "/api/automations"; 509 509 const res = await fetch(url, { 510 510 method: isEdit ? "PATCH" : "POST", 511 511 headers: { "Content-Type": "application/json" }, ··· 516 516 setError(data.error || `Failed to ${isEdit ? "update" : "create"} automation`); 517 517 } else { 518 518 savedRef.current = true; 519 - const rkey = isEdit ? initial.rkey : data.rkey; 519 + const rkey = isEdit ? initial!.rkey : data.rkey; 520 520 window.location.href = `/dashboard/automations/${rkey}`; 521 521 } 522 522 } catch {
+26 -5
app/islands/DeliveryLog.tsx
··· 1 1 import { useState, useCallback, useRef } from "hono/jsx"; 2 - import { Power, FlaskConical, Trash2, RefreshCw, ChevronRight, ChevronDown, CircleAlert } from "../icons.js"; 2 + import { 3 + Power, 4 + FlaskConical, 5 + Trash2, 6 + RefreshCw, 7 + ChevronRight, 8 + ChevronDown, 9 + CircleAlert, 10 + } from "../icons.js"; 3 11 import * as s from "./DeliveryLog.css.ts"; 4 12 import { variant as badgeVariant } from "../components/Badge/styles.css.ts"; 5 13 ··· 207 215 <> 208 216 <tr 209 217 key={log.id} 210 - class={`${log.dryRun ? s.dryRunRow : ""} ${hasDetails ? s.clickableRow : ""} ${expanded ? s.expandedRow : ""}`.trim() || undefined} 218 + class={ 219 + `${log.dryRun ? s.dryRunRow : ""} ${hasDetails ? s.clickableRow : ""} ${expanded ? s.expandedRow : ""}`.trim() || 220 + undefined 221 + } 211 222 onClick={hasDetails ? toggleExpand : undefined} 212 223 > 213 224 <td class={s.td}> ··· 227 238 </span> 228 239 </td> 229 240 <td class={s.td}>{log.actionIndex + 1}</td> 230 - <td class={s.td}>{log.dryRun ? "dry run" : (log.statusCode ?? "\u2014")}</td> 241 + <td class={s.td}> 242 + {log.dryRun ? "dry run" : (log.statusCode ?? "\u2014")} 243 + </td> 231 244 <td class={s.td}>{log.attempt}</td> 232 245 </tr> 233 246 {expanded && ( 234 247 <tr key={`${log.id}-detail`} class={s.detailRow}> 235 248 <td class={s.detailCell} colSpan={4}> 236 - {log.message && <p><strong>Message:</strong> {log.message}</p>} 237 - {log.error && <p class={s.detailError}><strong>Error:</strong> {log.error}</p>} 249 + {log.message && ( 250 + <p> 251 + <strong>Message:</strong> {log.message} 252 + </p> 253 + )} 254 + {log.error && ( 255 + <p class={s.detailError}> 256 + <strong>Error:</strong> {log.error} 257 + </p> 258 + )} 238 259 </td> 239 260 </tr> 240 261 )}
+4 -1
app/routes/dashboard/automations/[rkey].tsx
··· 1 1 import { createRoute } from "honox/factory"; 2 2 import { eq, and, desc } from "drizzle-orm"; 3 - import { ArrowLeft, Pencil, Filter, Database, Zap } from "../../../icons.js"; 3 + import { ArrowLeft, Copy, Pencil, Filter, Database, Zap } from "../../../icons.js"; 4 4 import { db } from "@/db/index.js"; 5 5 import { automations, deliveryLogs } from "@/db/schema.js"; 6 6 import { AppShell } from "../../../components/Layout/AppShell/index.js"; ··· 67 67 </span> 68 68 <Button href={`/dashboard/automations/${rkey}/edit`} variant="secondary" size="sm"> 69 69 <Pencil size={14} /> Edit 70 + </Button> 71 + <Button href={`/dashboard/automations/${rkey}/duplicate`} variant="ghost" size="sm"> 72 + <Copy size={14} /> Duplicate 70 73 </Button> 71 74 <Button href="/dashboard" variant="ghost" size="sm"> 72 75 <ArrowLeft size={14} /> Back
+72
app/routes/dashboard/automations/[rkey]/duplicate.tsx
··· 1 + import { createRoute } from "honox/factory"; 2 + import { eq, and } from "drizzle-orm"; 3 + import { ArrowLeft } from "../../../../icons.js"; 4 + import { db } from "@/db/index.js"; 5 + import { automations } from "@/db/schema.js"; 6 + import { AppShell } from "../../../../components/Layout/AppShell/index.js"; 7 + import { Header } from "../../../../components/Layout/Header/index.js"; 8 + import { Container } from "../../../../components/Layout/Container/index.js"; 9 + import { PageHeader } from "../../../../components/Layout/PageHeader/index.js"; 10 + import { Card } from "../../../../components/Card/index.js"; 11 + import { Button } from "../../../../components/Button/index.js"; 12 + import ThemeToggle from "../../../../islands/ThemeToggle.js"; 13 + import AutomationForm from "../../../../islands/AutomationForm.js"; 14 + 15 + export default createRoute(async (c) => { 16 + const user = c.get("user"); 17 + const rkey = c.req.param("rkey")!; 18 + 19 + const auto = await db.query.automations.findFirst({ 20 + where: and(eq(automations.did, user.did), eq(automations.rkey, rkey)), 21 + }); 22 + 23 + if (!auto) { 24 + c.status(404); 25 + return c.render( 26 + <AppShell header={<Header user={user} actions={<ThemeToggle />} />}> 27 + <Container> 28 + <PageHeader 29 + title="Not Found" 30 + actions={ 31 + <Button href="/dashboard" variant="ghost" size="sm"> 32 + <ArrowLeft size={14} /> Back 33 + </Button> 34 + } 35 + /> 36 + <p>This automation does not exist.</p> 37 + </Container> 38 + </AppShell>, 39 + { title: "Not Found — Airglow" }, 40 + ); 41 + } 42 + 43 + return c.render( 44 + <AppShell header={<Header user={user} actions={<ThemeToggle />} />}> 45 + <Container> 46 + <PageHeader 47 + title="Duplicate Automation" 48 + actions={ 49 + <Button href={`/dashboard/automations/${rkey}`} variant="ghost" size="sm"> 50 + <ArrowLeft size={14} /> Back 51 + </Button> 52 + } 53 + /> 54 + <Card variant="flat"> 55 + <AutomationForm 56 + initial={{ 57 + name: `${auto.name} (copy)`, 58 + description: auto.description, 59 + lexicon: auto.lexicon, 60 + operation: auto.operation, 61 + actions: auto.actions, 62 + fetches: auto.fetches, 63 + conditions: auto.conditions, 64 + active: false, 65 + }} 66 + /> 67 + </Card> 68 + </Container> 69 + </AppShell>, 70 + { title: "Duplicate Automation — Airglow" }, 71 + ); 72 + });
+4 -1
vite.config.ts
··· 192 192 "bun:sqlite": "/lib/db/sqlite-compat.ts", 193 193 "better-sqlite3": "/lib/db/sqlite-compat.ts", 194 194 "drizzle-orm/bun-sqlite": "drizzle-orm/better-sqlite3", 195 - "lucide-preact/icons": resolve(import.meta.dirname, "node_modules/lucide-preact/dist/esm/icons"), 195 + "lucide-preact/icons": resolve( 196 + import.meta.dirname, 197 + "node_modules/lucide-preact/dist/esm/icons", 198 + ), 196 199 "preact/hooks": resolve(import.meta.dirname, "lib/shims/preact-hooks.ts"), 197 200 preact: resolve(import.meta.dirname, "lib/shims/preact.ts"), 198 201 },