Skip to content

Commit 98a0f9e

Browse files
committed
feat: (zenflux-react-commander) - add useScopedCommand hook for enhanced command management
1 parent 6cc0357 commit 98a0f9e

2 files changed

Lines changed: 82 additions & 11 deletions

File tree

packages/zenflux-react-commander/src/use-commands.tsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { GET_INTERNAL_SYMBOL, GET_INTERNAL_MATCH_SYMBOL } from "./_internal/cons
77
import core from "./_internal/core";
88

99
import { ComponentIdContext } from "@zenflux/react-commander/commands-context";
10-
1110
import commandsManager from "@zenflux/react-commander/commands-manager";
1211

1312
import type { DCommandArgs, DCommandComponentContextProps, DCommandIdArgs } from "@zenflux/react-commander/definitions";
@@ -204,3 +203,76 @@ export function useCommandId( commandName: string, opts?: { match?: string; inde
204203
return id;
205204
}
206205

206+
export function useScopedCommand( commandName: string, opts?: { match?: string; index?: number } ) {
207+
const componentContext = React.useContext( ComponentIdContext );
208+
const fallbackId = React.useId();
209+
const ownerId = componentContext?.isSet ? componentContext.getNameUnique() : ( "GLOBAL-" + fallbackId );
210+
211+
const id = useCommandId( commandName, opts );
212+
213+
const run = React.useCallback( ( args: DCommandArgs, callback?: ( result: unknown ) => void ) => {
214+
if ( ! id ) return;
215+
return commandsManager.run( id, args, callback );
216+
}, [ id ] );
217+
218+
const hookScoped = React.useCallback( (
219+
callback: ( result?: unknown, args?: DCommandArgs ) => void,
220+
options?: { __ignoreDuplicatedHookError?: boolean }
221+
) => {
222+
if ( ! id ) return { dispose: () => void 0 } as any;
223+
return commandsManager.hookScoped( id, ownerId, callback, options );
224+
}, [ id, ownerId ] );
225+
226+
const unhookHandle = React.useCallback( ( handle: { dispose: () => void } ) => {
227+
commandsManager.unhookHandle( handle as any );
228+
}, [] );
229+
230+
const hook = React.useCallback( (
231+
callback: ( result?: unknown, args?: DCommandArgs ) => void,
232+
options?: { __ignoreDuplicatedHookError?: boolean }
233+
) => {
234+
if ( ! id ) return;
235+
return commandsManager.hook( id, callback, options );
236+
}, [ id ] );
237+
238+
const unhook = React.useCallback( () => {
239+
if ( ! id ) return;
240+
commandsManager.unhook( id );
241+
}, [ id ] );
242+
243+
return {
244+
id,
245+
run,
246+
hook,
247+
unhook,
248+
hookScoped,
249+
unhookHandle,
250+
} as const;
251+
}
252+
253+
function toAdapterKey( name: string ): string {
254+
const last = name.split( "/" ).pop() || name;
255+
return last.charAt( 0 ).toLowerCase() + last.slice( 1 );
256+
}
257+
258+
export function useCommands( input: string[] | Record<string, string> ) {
259+
const entries = React.useMemo( () => {
260+
if ( Array.isArray( input ) ) {
261+
return input.map( ( name ) => [ toAdapterKey( name ), name ] as const );
262+
}
263+
return Object.entries( input );
264+
}, [ input ] );
265+
266+
const adapters = entries.map( ( [ _key, name ] ) => useScopedCommand( name ) );
267+
268+
const result = React.useMemo( () => {
269+
const out: { [ k: string ]: ReturnType<typeof useScopedCommand> } = {};
270+
entries.forEach( ( [ key ], i ) => {
271+
out[ key ] = adapters[ i ];
272+
} );
273+
return out;
274+
}, [ entries, adapters ] );
275+
276+
return result;
277+
}
278+

zenflux-react-app-examples/budget-allocation/src/app.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { useEffect } from "react";
33
import { API } from "@zenflux/react-api/src";
44
import commandsManager from "@zenflux/react-commander/commands-manager";
55

6-
import { useCommandId } from "@zenflux/react-commander/use-commands";
6+
import { useScopedCommand } from "@zenflux/react-commander/use-commands";
77

88
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@zenflux/app-budget-allocation/src/components/ui/tabs";
99

@@ -45,36 +45,35 @@ function App() {
4545

4646
const [ selectedTab, setSelectedTab ] = React.useState( location.hash.replace( "#", "" ) );
4747

48-
const addChannelId = useCommandId( "App/AddChannel" );
48+
const addChannel = useScopedCommand( "App/AddChannel" );
4949

5050
useEffect( () => {
51-
if ( ! addChannelId ) return;
51+
if ( ! addChannel.id ) return;
5252

5353
if ( location.hash === "#allocation/add-channel" ) {
5454
location.hash = "#allocation";
5555
setSelectedTab( "allocation" );
5656

5757
setTimeout( () => {
58-
commandsManager.run( addChannelId, {} );
58+
addChannel.run( {} );
5959
}, 1000 );
6060
}
61-
}, [ location.hash, addChannelId ] );
61+
}, [ location.hash, addChannel.id?.componentNameUnique ] );
6262

6363
useEffect( () => {
64-
if ( ! addChannelId ) return;
64+
if ( ! addChannel.id ) return;
6565

66-
const ownerId = "App";
67-
const handle = commandsManager.hookScoped( addChannelId, ownerId, () => {
66+
const handle = addChannel.hookScoped( () => {
6867
location.hash = "#allocation/add-channel";
6968

7069
setSelectedTab( "allocation" );
7170
} );
7271

7372
return () => {
74-
commandsManager.unhookHandle( handle );
73+
addChannel.unhookHandle( handle );
7574
};
7675

77-
}, [ addChannelId ] );
76+
}, [ addChannel.id?.componentNameUnique ] );
7877

7978
const items = [
8079
{ id: "allocation", title: "Budget Allocation", content: <LazyLoader ContentComponent={ BudgetAllocation }/> },

0 commit comments

Comments
 (0)