diff --git a/.changeset/witty-houses-wash.md b/.changeset/witty-houses-wash.md new file mode 100644 index 0000000000..4afe177bcc --- /dev/null +++ b/.changeset/witty-houses-wash.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/widgets': minor +--- + +Allow chains to be disabled in ChainSearchMenu diff --git a/typescript/widgets/src/chains/ChainSearchMenu.tsx b/typescript/widgets/src/chains/ChainSearchMenu.tsx index a945e4bd93..d26f90c91f 100644 --- a/typescript/widgets/src/chains/ChainSearchMenu.tsx +++ b/typescript/widgets/src/chains/ChainSearchMenu.tsx @@ -4,9 +4,10 @@ import { ChainMap, ChainMetadata, ChainName, + ChainStatus, mergeChainMetadataMap, } from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ProtocolType, objMap } from '@hyperlane-xyz/utils'; import { SearchMenu, @@ -64,6 +65,10 @@ export interface ChainSearchMenuProps { showAddChainButton?: boolean; // Field by which data will be sorted by default defaultSortField?: DefaultSortField; + /** + * Allow chains to be shown as disabled. Defaults to `false` + */ + shouldDisableChains?: boolean; } export function ChainSearchMenu({ @@ -76,6 +81,7 @@ export function ChainSearchMenu({ showAddChainButton, showAddChainMenu, defaultSortField, + shouldDisableChains = false, }: ChainSearchMenuProps) { const [drilldownChain, setDrilldownChain] = useState( showChainDetails, @@ -88,11 +94,22 @@ export function ChainSearchMenu({ chainMetadata, overrideChainMetadata, ); - return { mergedMetadata, listData: Object.values(mergedMetadata) }; - }, [chainMetadata, overrideChainMetadata]); + const disabledChainMetadata = getDisabledChains( + mergedMetadata, + shouldDisableChains, + ); + return { + mergedMetadata: disabledChainMetadata, + listData: Object.values(disabledChainMetadata), + }; + }, [chainMetadata, overrideChainMetadata, shouldDisableChains]); const { ListComponent, searchFn, sortOptions, defaultSortState } = - useCustomizedListItems(customListItemField, defaultSortField); + useCustomizedListItems( + customListItemField, + shouldDisableChains, + defaultSortField, + ); if (drilldownChain && mergedMetadata[drilldownChain]) { const isLocalOverrideChain = !chainMetadata[drilldownChain]; @@ -225,12 +242,14 @@ function chainSearch({ sort, filter, customListItemField, + shouldDisableChains, }: { data: ChainMetadata[]; query: string; sort: SortState; filter: ChainFilterState; customListItemField?: CustomListItemField; + shouldDisableChains?: boolean; }) { const queryFormatted = query.trim().toLowerCase(); return ( @@ -257,6 +276,14 @@ function chainSearch({ }) // Sort options .sort((c1, c2) => { + if (shouldDisableChains) { + // If one chain is disabled and the other is not, place the disabled chain at the bottom + const c1Disabled = c1.availability?.status === ChainStatus.Disabled; + const c2Disabled = c2.availability?.status === ChainStatus.Disabled; + if (c1Disabled && !c2Disabled) return 1; + if (!c1Disabled && c2Disabled) return -1; + } + // Special case handling for if the chains are being sorted by the // custom field provided to ChainSearchMenu if (customListItemField && sort.sortBy === customListItemField.header) { @@ -290,6 +317,7 @@ function chainSearch({ */ function useCustomizedListItems( customListItemField, + shouldDisableChains: boolean, defaultSortField?: DefaultSortField, ) { // Create closure of ChainListItem but with customField pre-bound @@ -303,8 +331,8 @@ function useCustomizedListItems( // Bind the custom field to the search function const searchFn = useCallback( (args: Parameters[0]) => - chainSearch({ ...args, customListItemField }), - [customListItemField], + chainSearch({ ...args, shouldDisableChains, customListItemField }), + [customListItemField, shouldDisableChains], ); // Merge the custom field into the sort options if a custom field exists @@ -333,3 +361,18 @@ function useCustomizedListItems( return { ListComponent, searchFn, sortOptions, defaultSortState }; } + +function getDisabledChains( + chainMetadata: ChainMap, + shouldDisableChains: boolean, +) { + if (!shouldDisableChains) return chainMetadata; + + return objMap(chainMetadata, (_, chain) => { + if (chain.availability?.status === ChainStatus.Disabled) { + return { ...chain, disabled: true }; + } + + return chain; + }); +} diff --git a/typescript/widgets/src/components/SearchMenu.tsx b/typescript/widgets/src/components/SearchMenu.tsx index 750d2bfb3d..ee703d282b 100644 --- a/typescript/widgets/src/components/SearchMenu.tsx +++ b/typescript/widgets/src/components/SearchMenu.tsx @@ -106,6 +106,7 @@ export function SearchMenu< e.stopPropagation(); if (results.length === 1) { const item = results[0]; + if (item.disabled) return; isEditMode ? onClickEditItem(item) : onClickItem(item); } }, diff --git a/typescript/widgets/src/stories/ChainSearchMenu.stories.tsx b/typescript/widgets/src/stories/ChainSearchMenu.stories.tsx index fa6adff950..7a3e1a4b39 100644 --- a/typescript/widgets/src/stories/ChainSearchMenu.stories.tsx +++ b/typescript/widgets/src/stories/ChainSearchMenu.stories.tsx @@ -1,6 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { chainMetadata } from '@hyperlane-xyz/registry'; +import { ChainDisabledReason, ChainStatus } from '@hyperlane-xyz/sdk'; import { pick } from '@hyperlane-xyz/utils'; import { @@ -84,3 +85,37 @@ export const WithOverrideChain = { showAddChainButton: true, }, } satisfies Story; + +export const WithDisabledChains = { + args: { + chainMetadata: pick(chainMetadata, ['alfajores', 'base']), + overrideChainMetadata: { + arbitrum: { + ...chainMetadata['arbitrum'], + availability: { + status: ChainStatus.Disabled, + reasons: [ChainDisabledReason.Deprecated], + }, + }, + ethereum: { + ...chainMetadata['ethereum'], + availability: { + status: ChainStatus.Disabled, + }, + }, + }, + onChangeOverrideMetadata: () => {}, + showAddChainButton: true, + defaultSortField: 'custom', + shouldDisableChains: true, + customListItemField: { + header: 'Warp Routes', + data: { + alfajores: { display: '1 token', sortValue: 1 }, + arbitrum: { display: '2 tokens', sortValue: 2 }, + ethereum: { display: '1 token', sortValue: 1 }, + base: { display: '2 tokens', sortValue: 2 }, + }, + }, + }, +} satisfies Story;