Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/packages/operators/src/Panel/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as usePanelClientEvent } from "./usePanelClientEvent";
55 changes: 55 additions & 0 deletions app/packages/operators/src/Panel/hooks/usePanelClientEvent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { usePanelId } from "@fiftyone/spaces";

const events: PanelsEvents = {};

export default function usePanelEvents(panelId?: string) {
const id = usePanelId();
const computedPanelId = panelId ?? id;

function register(
event: string,
callback: PanelEventHandler,
panelId?: string
) {
const registerPanelId = panelId ?? computedPanelId;
assertPanelId(registerPanelId);
if (!events[registerPanelId]) {
events[registerPanelId] = {};
}
events[registerPanelId][event] = callback;
}
Comment on lines +16 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Allow multiple handlers per event and return a boolean from trigger

Currently, registering the same event twice overwrites the previous handler. Support a Set of handlers per event and have trigger return whether anything ran.

Apply these diffs:

-    if (!events[registerPanelId]) {
-      events[registerPanelId] = {};
-    }
-    events[registerPanelId][event] = callback;
+    if (!events[registerPanelId]) {
+      events[registerPanelId] = {};
+    }
+    if (!events[registerPanelId][event]) {
+      events[registerPanelId][event] = new Set<PanelEventHandler>();
+    }
+    events[registerPanelId][event].add(callback);
-  function trigger(event: string, params: unknown, panelId?: string) {
+  function trigger(event: string, params: unknown, panelId?: string): boolean {
     const triggerPanelId = panelId ?? computedPanelId;
     assertPanelId(triggerPanelId);
-    const callback = events[triggerPanelId]?.[event];
-    if (callback) {
-      callback(params);
-    }
+    const callbacks = events[triggerPanelId]?.[event];
+    if (callbacks?.size) {
+      for (const cb of callbacks) cb(params);
+      return true;
+    }
+    return false;
   }
 type PanelEventHandler = (params: unknown) => void;
-type PanelEvents = {
-  [key: string]: PanelEventHandler;
-};
-type PanelsEvents = {
-  [key: string]: PanelEvents;
-};
+type PanelEvents = {
+  [event: string]: Set<PanelEventHandler>;
+};
+type PanelsEvents = {
+  [panelId: string]: PanelEvents;
+};

Also applies to: 22-29, 49-55

🤖 Prompt for AI Agents
In app/packages/operators/src/Panel/hooks/usePanelClientEvent.tsx around lines
16-20, 22-29 and 49-55, event registration currently stores a single callback
per event which overwrites previous handlers and trigger does not return whether
any handlers ran; change the event storage to use a Set of handlers for each
event (initialize events[registerPanelId][event] as a Set and add callbacks to
the Set instead of replacing), update removal to delete from the Set, update
trigger to iterate over the Set invoking each handler (catching errors if
desired) and have trigger return a boolean indicating whether at least one
handler was invoked.


function trigger(event: string, params: unknown, panelId?: string) {
const triggerPanelId = panelId ?? computedPanelId;
assertPanelId(triggerPanelId);
const callback = events[triggerPanelId]?.[event];
if (callback) {
callback(params);
}
}

function unregister(event: string, panelId?: string) {
const unregisterPanelId = panelId ?? computedPanelId;
assertPanelId(unregisterPanelId);
if (events[unregisterPanelId]) {
delete events[unregisterPanelId][event];
}
}

return { register, trigger, unregister };
}
Comment on lines +39 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Expose a non-hook trigger for non-React call sites (operators, services)

Triggers don’t require React state; calling this hook from non-React code would violate Rules of Hooks. Provide a pure helper and prefer it in operator code paths.

Add this helper near the top-level (outside the hook):

export function triggerPanelEvent(panelId: string, event: string, params?: unknown): boolean {
  if (!panelId) return false;
  const bucket = events[panelId]?.[event];
  if (!bucket) return false;
  if (bucket instanceof Set) {
    if (!bucket.size) return false;
    for (const cb of bucket) cb(params);
    return true;
  }
  // Back-compat if single-handler shape remains somewhere
  (bucket as unknown as (p: unknown) => void)(params);
  return true;
}

Verification (scan for hook usage outside components/hooks):


🏁 Script executed:

#!/bin/bash
# Find direct invocations of the hook; inspect contexts for non-React usage
rg -n -C2 -S '\b(usePanelClientEvent|usePanelEvents)\s*\(' app/packages

Length of output: 940


Extract trigger logic into a standalone helper and update operator call sites

• In app/packages/operators/src/Panel/hooks/usePanelClientEvent.tsx, add above the hook definition:

export function triggerPanelEvent(
  panelId: string,
  event: string,
  params?: unknown
): boolean {
  if (!panelId) return false;
  const bucket = events[panelId]?.[event];
  if (!bucket) return false;
  if (bucket instanceof Set) {
    if (!bucket.size) return false;
    for (const cb of bucket) cb(params);
    return true;
  }
  (bucket as unknown as (p: unknown) => void)(params);
  return true;
}

• In app/packages/operators/src/built-in-operators.ts (line 1513), replace:

const { trigger } = usePanelClientEvent();

with:

import { triggerPanelEvent } from './Panel/hooks/usePanelClientEvent';

const trigger = (params) => triggerPanelEvent(panelId, eventName, params);
🤖 Prompt for AI Agents
In app/packages/operators/src/Panel/hooks/usePanelClientEvent.tsx around lines
39-40, extract the trigger logic into a standalone exported helper named
triggerPanelEvent that accepts (panelId: string, event: string, params?:
unknown) and performs the same null checks, bucket lookup, Set iteration or
single-callback invocation, and boolean return as in the suggested snippet;
place this exported function above the hook definition. Then in
app/packages/operators/src/built-in-operators.ts at ~line 1513, replace the
hook-derived trigger by importing triggerPanelEvent from the Panel/hooks file
and define const trigger = (params) => triggerPanelEvent(panelId, eventName,
params) so operator call sites use the new helper.


function assertPanelId(panelId?: string) {
if (!panelId) {
throw new Error("Panel ID is required");
}
return panelId;
}

type PanelEventHandler = (params: unknown) => void;
type PanelEvents = {
[key: string]: PanelEventHandler;
};
type PanelsEvents = {
[key: string]: PanelEvents;
};
27 changes: 25 additions & 2 deletions app/packages/operators/src/built-in-operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
useRecoilValue,
useSetRecoilState,
} from "recoil";
import { useOperatorExecutor } from ".";
import { useOperatorExecutor, usePanelClientEvent } from ".";
import useRefetchableSavedViews from "../../core/src/hooks/useRefetchableSavedViews";
import registerPanel from "./Panel/register";
import {
Expand All @@ -37,7 +37,6 @@
} from "./operators";
import { useShowOperatorIO } from "./state";
import usePanelEvent from "./usePanelEvent";
import { Clear } from "@mui/icons-material";

//
// BUILT-IN OPERATORS
Expand Down Expand Up @@ -818,7 +817,7 @@
unlisted: true,
});
}
useHooks(ctx: ExecutionContext): {} {

Check warning on line 820 in app/packages/operators/src/built-in-operators.ts

View workflow job for this annotation

GitHub Actions / lint / eslint

'ctx' is defined but never used. Allowed unused args must match /^_/u
return { updatePanelState: useUpdatePanelStatePartial() };
}
async execute(ctx: ExecutionContext): Promise<void> {
Expand Down Expand Up @@ -1502,6 +1501,29 @@
}
}

class TriggerPanelClientEvent extends Operator {
_builtIn = true;
get config() {
return new OperatorConfig({
name: "trigger_panel_client_event",
label: "Trigger panel client event",
});
}
useHooks() {
const { trigger } = usePanelClientEvent();
return {
trigger: (params) => {
const panelId = params.panel_id;
trigger(params.event, params?.params, panelId);
},
};
}
async execute(ctx: ExecutionContext) {
const { hooks, params } = ctx;
hooks.trigger(params);
}
}

export function registerBuiltInOperators() {
try {
_registerBuiltInOperator(CopyViewAsJSON);
Expand Down Expand Up @@ -1559,6 +1581,7 @@
_registerBuiltInOperator(HideSidebar);
_registerBuiltInOperator(ToggleSidebar);
_registerBuiltInOperator(ClearActiveFields);
_registerBuiltInOperator(TriggerPanelClientEvent);
} catch (e) {
console.error("Error registering built-in operators");
console.error(e);
Expand Down
1 change: 1 addition & 0 deletions app/packages/operators/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ export {
} from "./state";
export * as types from "./types";
export { default as usePanelEvent } from "./usePanelEvent";
export * from "./Panel/hooks";
13 changes: 13 additions & 0 deletions fiftyone/operators/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,19 @@ def toggle_sidebar(self):
"""Toggle the visibility of the App's sidebar."""
return self._ctx.trigger("toggle_sidebar")

def trigger_panel_client_event(self, id, event, params=None):
"""Triggers a panel client-side event.

Args:
id: the ID of the panel
event: the event to trigger
params: the parameters to pass to the event
"""
return self._ctx.trigger(
"trigger_panel_client_event",
{"panel_id": id, "event": event, "params": params},
)


def _serialize_view(view):
return json.loads(json_util.dumps(view._serialize()))
11 changes: 11 additions & 0 deletions fiftyone/operators/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,14 @@ def set_title(self, title):
if title is None:
raise ValueError("title cannot be None")
self._ctx.ops.set_panel_title(id=self.id, title=title)

def trigger(self, event, params=None):
"""Triggers a client-side event of a panel.

Args:
event: name of client-side event to trigger
params: parameters to pass to the event
"""
self._ctx.ops.trigger_panel_client_event(
id=self.id, event=event, params=params
)
Loading