diff --git a/app/packages/operators/src/Panel/hooks/index.ts b/app/packages/operators/src/Panel/hooks/index.ts new file mode 100644 index 00000000000..872417bd4c1 --- /dev/null +++ b/app/packages/operators/src/Panel/hooks/index.ts @@ -0,0 +1 @@ +export { default as usePanelClientEvent } from "./usePanelClientEvent"; diff --git a/app/packages/operators/src/Panel/hooks/usePanelClientEvent.tsx b/app/packages/operators/src/Panel/hooks/usePanelClientEvent.tsx new file mode 100644 index 00000000000..05ca1e8cb34 --- /dev/null +++ b/app/packages/operators/src/Panel/hooks/usePanelClientEvent.tsx @@ -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; + } + + 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 }; +} + +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; +}; diff --git a/app/packages/operators/src/built-in-operators.ts b/app/packages/operators/src/built-in-operators.ts index 164ecdea5fe..651d5c8c076 100644 --- a/app/packages/operators/src/built-in-operators.ts +++ b/app/packages/operators/src/built-in-operators.ts @@ -23,7 +23,7 @@ import { useRecoilValue, useSetRecoilState, } from "recoil"; -import { useOperatorExecutor } from "."; +import { useOperatorExecutor, usePanelClientEvent } from "."; import useRefetchableSavedViews from "../../core/src/hooks/useRefetchableSavedViews"; import registerPanel from "./Panel/register"; import { @@ -37,7 +37,6 @@ import { } from "./operators"; import { useShowOperatorIO } from "./state"; import usePanelEvent from "./usePanelEvent"; -import { Clear } from "@mui/icons-material"; // // BUILT-IN OPERATORS @@ -1502,6 +1501,29 @@ class ToggleSidebar extends Operator { } } +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); @@ -1559,6 +1581,7 @@ export function registerBuiltInOperators() { _registerBuiltInOperator(HideSidebar); _registerBuiltInOperator(ToggleSidebar); _registerBuiltInOperator(ClearActiveFields); + _registerBuiltInOperator(TriggerPanelClientEvent); } catch (e) { console.error("Error registering built-in operators"); console.error(e); diff --git a/app/packages/operators/src/index.ts b/app/packages/operators/src/index.ts index dd5ac9187ed..30bd280b125 100644 --- a/app/packages/operators/src/index.ts +++ b/app/packages/operators/src/index.ts @@ -29,3 +29,4 @@ export { } from "./state"; export * as types from "./types"; export { default as usePanelEvent } from "./usePanelEvent"; +export * from "./Panel/hooks"; diff --git a/fiftyone/operators/operations.py b/fiftyone/operators/operations.py index b9599bdfe5e..1ca3f7f0b0c 100644 --- a/fiftyone/operators/operations.py +++ b/fiftyone/operators/operations.py @@ -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())) diff --git a/fiftyone/operators/panel.py b/fiftyone/operators/panel.py index a5d03cd9439..0340b18d39c 100644 --- a/fiftyone/operators/panel.py +++ b/fiftyone/operators/panel.py @@ -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 + )