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.

fix: prevent leaving the page while creating or udpating an automation

Hugo d4dc0abc 15673e99

+60 -10
+12
app/islands/AutomationForm.css.ts
··· 88 88 display: "flex", 89 89 flexDirection: "column", 90 90 gap: space[1], 91 + selectors: { 92 + "& + &": { 93 + paddingBlockStart: space[3], 94 + borderBlockStart: `1px solid ${vars.color.borderSubtle}`, 95 + }, 96 + }, 91 97 }); 92 98 93 99 export const conditionRow = style({ ··· 169 175 cursor: "not-allowed", 170 176 }, 171 177 }, 178 + }); 179 + 180 + export const stickySubmit = style({ 181 + position: "sticky", 182 + insetBlockEnd: space[4], 183 + zIndex: 10, 172 184 }); 173 185 174 186 export const alertError = style({
+48 -10
app/islands/AutomationForm.tsx
··· 1 - import { useState, useCallback, useRef, useMemo } from "hono/jsx"; 1 + import { useState, useCallback, useRef, useMemo, useEffect } from "hono/jsx"; 2 2 import type { RecordSchema } from "../../lib/lexicons/schema-tree.js"; 3 3 import type { Action, FetchStep } from "../../lib/db/schema.js"; 4 4 import RecordFormBuilder from "./RecordFormBuilder.js"; ··· 318 318 const [nsidSuggestions, setNsidSuggestions] = useState<string[]>([]); 319 319 const [submitting, setSubmitting] = useState(false); 320 320 const [error, setError] = useState(""); 321 + const savedRef = useRef(false); 321 322 const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); 322 323 const suggestDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); 323 324 const lastSuggestPrefix = useRef(""); 324 325 const initialFetched = useRef(false); 325 326 327 + const isDirty = useMemo(() => { 328 + if (isEdit) { 329 + if (name !== (initial.name ?? "")) return true; 330 + if (description !== (initial.description ?? "")) return true; 331 + if (JSON.stringify(conditions) !== JSON.stringify(toConditionDrafts(initial.conditions))) 332 + return true; 333 + if (JSON.stringify(fetches) !== JSON.stringify(toFetchDrafts(initial.fetches))) return true; 334 + if (JSON.stringify(actions) !== JSON.stringify(toActionDrafts(initial.actions))) return true; 335 + return false; 336 + } 337 + return !!( 338 + name || 339 + description || 340 + lexicon || 341 + conditions.length || 342 + fetches.length || 343 + actions.length 344 + ); 345 + }, [name, description, lexicon, conditions, fetches, actions, isEdit]); 346 + 347 + useEffect(() => { 348 + if (!isDirty) return; 349 + const handler = (e: BeforeUnloadEvent) => { 350 + if (savedRef.current) return; 351 + e.preventDefault(); 352 + }; 353 + window.addEventListener("beforeunload", handler); 354 + return () => window.removeEventListener("beforeunload", handler); 355 + }, [isDirty]); 356 + 326 357 const fetchFields = useCallback((nsid: string, updateUrl = true) => { 327 358 if (debounceRef.current) clearTimeout(debounceRef.current); 328 359 if (!nsid) { ··· 486 517 if (!res.ok) { 487 518 setError(data.error || `Failed to ${isEdit ? "update" : "create"} automation`); 488 519 } else { 520 + savedRef.current = true; 489 521 const rkey = isEdit ? initial.rkey : data.rkey; 490 522 window.location.href = `/dashboard/automations/${rkey}`; 491 523 } ··· 848 880 849 881 {error && <div class={s.alertError}>{error}</div>} 850 882 851 - <button type="submit" class={s.submitBtn} disabled={!name.trim() || actions.length === 0}> 852 - {submitting 853 - ? isEdit 854 - ? "Updating..." 855 - : "Creating..." 856 - : isEdit 857 - ? "Update automation" 858 - : "Create automation"} 859 - </button> 883 + <div class={isEdit ? s.stickySubmit : undefined}> 884 + <button 885 + type="submit" 886 + class={s.submitBtn} 887 + disabled={!name.trim() || actions.length === 0} 888 + > 889 + {submitting 890 + ? isEdit 891 + ? "Updating..." 892 + : "Creating..." 893 + : isEdit 894 + ? "Update automation" 895 + : "Create automation"} 896 + </button> 897 + </div> 860 898 861 899 <datalist id="nsid-suggestions"> 862 900 {nsidSuggestions.map((nsid) => (