From ae8f7c642498bc895ecfe4b7f89ba77659f478fc Mon Sep 17 00:00:00 2001
From: Jason Guo <33064781+Xaroz@users.noreply.github.com>
Date: Thu, 27 Feb 2025 13:58:35 -0400
Subject: [PATCH] feat(widgets): allow chains to be disabled in ChainSearchMenu
(#5581)
### Description
Updates `` to disable chains when its status is
`disabled`
- Add `showDisabledChains` boolean to props. When false it will make the
chain items unable to be selected. Defaults to `true`
- Add disabled to the chain metadata because its required by
`SearchMenu` component
- Now disabled items will be shown at the bottom
- New storybook for this use case
### Drive-by changes
### Related issues
### Backward compatibility
Yes
### Testing
Manual and visual testing with storybook
---
.changeset/witty-houses-wash.md | 5 ++
.../widgets/src/chains/ChainSearchMenu.tsx | 55 +++++++++++++++++--
.../widgets/src/components/SearchMenu.tsx | 1 +
.../src/stories/ChainSearchMenu.stories.tsx | 35 ++++++++++++
4 files changed, 90 insertions(+), 6 deletions(-)
create mode 100644 .changeset/witty-houses-wash.md
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;