From ac2333ee1c036e86df7cc74fdb509fec6d7546fc Mon Sep 17 00:00:00 2001
From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Tue, 24 Mar 2026 03:11:30 -0700
Subject: [PATCH 1/3] fix: separate nested interactive elements in roles
manager
Moves the RolePermissionsDialog trigger to wrap only the text content
area instead of the entire card. The dropdown menu is now a sibling,
not nested inside the dialog button. Removes stopPropagation hacks.
Fixes #921
---
.../components/settings/roles-manager.tsx | 160 +++++++++---------
1 file changed, 77 insertions(+), 83 deletions(-)
diff --git a/surfsense_web/components/settings/roles-manager.tsx b/surfsense_web/components/settings/roles-manager.tsx
index 23b9aa4b6..0ccbb077d 100644
--- a/surfsense_web/components/settings/roles-manager.tsx
+++ b/surfsense_web/components/settings/roles-manager.tsx
@@ -510,93 +510,87 @@ function RolesContent({
{roles.map((role) => (
-
-
-
-
-
{role.name}
- {role.is_system_role && (
-
- System
-
- )}
- {role.is_default && (
-
- Default
-
+
+
+
+
+
+ {role.name}
+ {role.is_system_role && (
+
+ System
+
+ )}
+ {role.is_default && (
+
+ Default
+
+ )}
+
+ {role.description && (
+
+ {role.description}
+
)}
- {role.description && (
-
- {role.description}
-
- )}
-
+
+
-
+
- {!role.is_system_role && (
-
e.stopPropagation()}
- onKeyDown={(e) => e.stopPropagation()}
- >
-
-
-
-
-
-
- e.preventDefault()}>
- {canUpdate && (
- setEditingRoleId(role.id)}>
-
- Edit Role
-
- )}
- {canDelete && (
- <>
-
-
-
- e.preventDefault()}>
-
- Delete Role
-
-
-
-
- Delete role?
-
- This will permanently delete the "{role.name}" role.
- Members with this role will lose their permissions.
-
-
-
- Cancel
- onDeleteRole(role.id)}
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
- >
- Delete
-
-
-
-
- >
- )}
-
-
-
- )}
-
-
+ {!role.is_system_role && (
+
+
+
+
+
+
+
+ e.preventDefault()}>
+ {canUpdate && (
+ setEditingRoleId(role.id)}>
+
+ Edit Role
+
+ )}
+ {canDelete && (
+ <>
+
+
+
+ e.preventDefault()}>
+
+ Delete Role
+
+
+
+
+ Delete role?
+
+ This will permanently delete the "{role.name}" role.
+ Members with this role will lose their permissions.
+
+
+
+ Cancel
+ onDeleteRole(role.id)}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ Delete
+
+
+
+
+ >
+ )}
+
+
+
+ )}
+
))}
From a3b3852452e40c83005ed27ba09feadb82e33095 Mon Sep 17 00:00:00 2001
From: likiosliu
Date: Tue, 24 Mar 2026 19:53:14 +0800
Subject: [PATCH 2/3] fix: add AbortController to fetch call in audio download
Abort in-flight download when the component unmounts or a new download
starts, preventing wasted bandwidth on navigation.
Closes #919
---
surfsense_web/components/tool-ui/audio.tsx | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/surfsense_web/components/tool-ui/audio.tsx b/surfsense_web/components/tool-ui/audio.tsx
index acf6054fb..0bda81bb8 100644
--- a/surfsense_web/components/tool-ui/audio.tsx
+++ b/surfsense_web/components/tool-ui/audio.tsx
@@ -27,6 +27,7 @@ function formatTime(seconds: number): string {
export function Audio({ id, src, title, description, artwork, durationMs, className }: AudioProps) {
const audioRef = useRef(null);
+ const downloadControllerRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(durationMs ? durationMs / 1000 : 0);
@@ -84,8 +85,12 @@ export function Audio({ id, src, title, description, artwork, durationMs, classN
// Handle download
const handleDownload = useCallback(async () => {
+ downloadControllerRef.current?.abort();
+ const controller = new AbortController();
+ downloadControllerRef.current = controller;
+
try {
- const response = await fetch(src);
+ const response = await fetch(src, { signal: controller.signal });
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
@@ -96,10 +101,16 @@ export function Audio({ id, src, title, description, artwork, durationMs, classN
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (err) {
+ if (err instanceof DOMException && err.name === "AbortError") return;
console.error("Error downloading audio:", err);
}
}, [src, title]);
+ // Abort in-flight download on unmount
+ useEffect(() => {
+ return () => downloadControllerRef.current?.abort();
+ }, []);
+
// Set up audio event listeners
useEffect(() => {
const audio = audioRef.current;
From 323788e9a3cf42c7786e82597d01921e1c3b1b97 Mon Sep 17 00:00:00 2001
From: likiosliu
Date: Tue, 24 Mar 2026 19:51:29 +0800
Subject: [PATCH 3/3] fix: replace `any` types with proper types in contact
form component
- Replace `any` props type with `React.ComponentProps<"svg">` plus explicit named props
- Replace `any` tuple type with `[number, number]` in squares mapper
Closes #912
---
surfsense_web/components/contact/contact-form.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/surfsense_web/components/contact/contact-form.tsx b/surfsense_web/components/contact/contact-form.tsx
index 967c1c524..03e4118de 100644
--- a/surfsense_web/components/contact/contact-form.tsx
+++ b/surfsense_web/components/contact/contact-form.tsx
@@ -161,7 +161,7 @@ export const FeatureIconContainer = ({
);
};
-export const Grid = ({ pattern, size }: { pattern?: number[][]; size?: number }) => {
+export const Grid = ({ pattern, size }: { pattern?: [number, number][]; size?: number }) => {
const p = pattern ?? [
[9, 3],
[8, 5],
@@ -185,7 +185,7 @@ export const Grid = ({ pattern, size }: { pattern?: number[][]; size?: number })
);
};
-export function GridPattern({ width, height, x, y, squares, ...props }: any) {
+export function GridPattern({ width, height, x, y, squares, ...props }: React.ComponentProps<"svg"> & { width: number; height: number; x: string | number; y: string | number; squares?: [number, number][] }) {
const patternId = useId();
return (
@@ -205,7 +205,7 @@ export function GridPattern({ width, height, x, y, squares, ...props }: any) {
{squares && (
- {squares.map(([x, y]: any, idx: number) => (
+ {squares.map(([x, y]: [number, number], idx: number) => (