@hyperlane-xyz/widgets
Version:
Common react components for Hyperlane projects
166 lines • 9.91 kB
JavaScript
import React, { useCallback, useMemo, useState } from 'react';
import { ChainStatus, mergeChainMetadataMap, } from '@hyperlane-xyz/sdk';
import { ProtocolType, objMap } from '@hyperlane-xyz/utils';
import { SearchMenu, SortOrderOption, } from '../components/SearchMenu.js';
import { SegmentedControl } from '../components/SegmentedControl.js';
import { ChainAddMenu } from './ChainAddMenu.js';
import { ChainDetailsMenu } from './ChainDetailsMenu.js';
import { ChainLogo } from './ChainLogo.js';
export var ChainSortByOption;
(function (ChainSortByOption) {
ChainSortByOption["Name"] = "name";
ChainSortByOption["ChainId"] = "chain id";
ChainSortByOption["Protocol"] = "protocol";
})(ChainSortByOption || (ChainSortByOption = {}));
var FilterTestnetOption;
(function (FilterTestnetOption) {
FilterTestnetOption["Testnet"] = "testnet";
FilterTestnetOption["Mainnet"] = "mainnet";
})(FilterTestnetOption || (FilterTestnetOption = {}));
const defaultFilterState = {
type: undefined,
protocol: undefined,
};
export function ChainSearchMenu({ chainMetadata, onChangeOverrideMetadata, overrideChainMetadata, onClickChain, customListItemField, showChainDetails, showAddChainButton, showAddChainMenu, defaultSortField, shouldDisableChains = false, }) {
const [drilldownChain, setDrilldownChain] = useState(showChainDetails);
const [addChain, setAddChain] = useState(showAddChainMenu || false);
const { listData, mergedMetadata } = useMemo(() => {
const mergedMetadata = mergeChainMetadataMap(chainMetadata, overrideChainMetadata);
const disabledChainMetadata = getDisabledChains(mergedMetadata, shouldDisableChains);
return {
mergedMetadata: disabledChainMetadata,
listData: Object.values(disabledChainMetadata),
};
}, [chainMetadata, overrideChainMetadata, shouldDisableChains]);
const { ListComponent, searchFn, sortOptions, defaultSortState } = useCustomizedListItems(customListItemField, shouldDisableChains, defaultSortField);
if (drilldownChain && mergedMetadata[drilldownChain]) {
const isLocalOverrideChain = !chainMetadata[drilldownChain];
const onRemoveChain = () => {
const newOverrides = { ...overrideChainMetadata };
delete newOverrides[drilldownChain];
onChangeOverrideMetadata(newOverrides);
};
return (React.createElement(ChainDetailsMenu, { chainMetadata: chainMetadata[drilldownChain], overrideChainMetadata: overrideChainMetadata?.[drilldownChain], onChangeOverrideMetadata: (o) => onChangeOverrideMetadata({
...overrideChainMetadata,
[drilldownChain]: o,
}), onClickBack: () => setDrilldownChain(undefined), onRemoveChain: isLocalOverrideChain ? onRemoveChain : undefined }));
}
if (addChain) {
return (React.createElement(ChainAddMenu, { chainMetadata: chainMetadata, overrideChainMetadata: overrideChainMetadata, onChangeOverrideMetadata: onChangeOverrideMetadata, onClickBack: () => setAddChain(false) }));
}
return (React.createElement(SearchMenu, { data: listData, ListComponent: ListComponent, searchFn: searchFn, onClickItem: onClickChain, onClickEditItem: (chain) => setDrilldownChain(chain.name), sortOptions: sortOptions, defaultSortState: defaultSortState, FilterComponent: ChainFilters, defaultFilterState: defaultFilterState, placeholder: "Chain Name or ID", onClickAddItem: showAddChainButton ? () => setAddChain(true) : undefined }));
}
function ChainListItem({ data: chain, customField, }) {
return (React.createElement(React.Fragment, null,
React.createElement("div", { className: "htw-flex htw-items-center" },
React.createElement("div", { className: "htw-shrink-0" },
React.createElement(ChainLogo, { chainName: chain.name, logoUri: chain.logoURI, size: 32 })),
React.createElement("div", { className: "htw-ml-3 htw-text-left htw-overflow-hidden" },
React.createElement("div", { className: "htw-text-sm htw-font-medium truncate" }, chain.displayName),
React.createElement("div", { className: "htw-text-[0.7rem] htw-text-gray-500" }, chain.isTestnet ? 'Testnet' : 'Mainnet'))),
customField !== null && (React.createElement("div", { className: "htw-text-left htw-overflow-hidden" },
React.createElement("div", { className: "htw-text-sm truncate" }, customField
? customField.data[chain.name].display || 'Unknown'
: chain.deployer?.name || 'Unknown deployer'),
React.createElement("div", { className: "htw-text-[0.7rem] htw-text-gray-500" }, customField ? customField.header : 'Deployer')))));
}
function ChainFilters({ value, onChange, }) {
return (React.createElement("div", { className: "htw-py-3 htw-px-2.5 htw-space-y-4" },
React.createElement("div", { className: "htw-flex htw-flex-col htw-items-start htw-gap-2" },
React.createElement("label", { className: "htw-text-sm htw-text-gray-600 htw-pl-px" }, "Type"),
React.createElement(SegmentedControl, { options: Object.values(FilterTestnetOption), onChange: (selected) => onChange({ ...value, type: selected }), allowEmpty: true })),
React.createElement("div", { className: "htw-flex htw-flex-col htw-items-start htw-gap-2" },
React.createElement("label", { className: "htw-text-sm htw-text-gray-600 htw-pl-px" }, "Protocol"),
React.createElement(SegmentedControl, { options: Object.values(ProtocolType), onChange: (selected) => onChange({ ...value, protocol: selected }), allowEmpty: true }))));
}
function chainSearch({ data, query, sort, filter, customListItemField, shouldDisableChains, }) {
const queryFormatted = query.trim().toLowerCase();
return (data
// Query search
.filter((chain) => chain.name.includes(queryFormatted) ||
chain.displayName?.toLowerCase().includes(queryFormatted) ||
chain.chainId.toString().includes(queryFormatted) ||
chain.domainId.toString().includes(queryFormatted))
// Filter options
.filter((chain) => {
let included = true;
if (filter.type) {
included &&=
!!chain.isTestnet === (filter.type === FilterTestnetOption.Testnet);
}
if (filter.protocol) {
included &&= chain.protocol === filter.protocol;
}
return included;
})
// 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) {
const result = customListItemField.data[c1.name].sortValue -
customListItemField.data[c2.name].sortValue;
return sort.sortOrder === SortOrderOption.Asc ? result : -result;
}
// Otherwise sort by the default options
let sortValue1 = c1.name;
let sortValue2 = c2.name;
if (sort.sortBy === ChainSortByOption.ChainId) {
sortValue1 = c1.chainId.toString();
sortValue2 = c2.chainId.toString();
}
else if (sort.sortBy === ChainSortByOption.Protocol) {
sortValue1 = c1.protocol;
sortValue2 = c2.protocol;
}
return sort.sortOrder === SortOrderOption.Asc
? sortValue1.localeCompare(sortValue2)
: sortValue2.localeCompare(sortValue1);
}));
}
/**
* This hook creates closures around the provided customListItemField data
* This is useful because SearchMenu will do handle the list item rendering and
* management but the custom data is more or a chain-search-specific concern
*/
function useCustomizedListItems(customListItemField, shouldDisableChains, defaultSortField) {
// Create closure of ChainListItem but with customField pre-bound
const ListComponent = useCallback(({ data }) => (React.createElement(ChainListItem, { data: data, customField: customListItemField })), [customListItemField]);
// Bind the custom field to the search function
const searchFn = useCallback((args) => chainSearch({ ...args, shouldDisableChains, customListItemField }), [customListItemField, shouldDisableChains]);
// Merge the custom field into the sort options if a custom field exists
const sortOptions = useMemo(() => [
...(customListItemField ? [customListItemField.header] : []),
...Object.values(ChainSortByOption),
], [customListItemField]);
// Sort by defaultSortField initially, if value is "custom", sort using custom field by default
const defaultSortState = useMemo(() => defaultSortField
? {
sortBy: defaultSortField === 'custom' && customListItemField
? customListItemField.header
: defaultSortField,
sortOrder: SortOrderOption.Desc,
}
: undefined, [defaultSortField, customListItemField]);
return { ListComponent, searchFn, sortOptions, defaultSortState };
}
function getDisabledChains(chainMetadata, shouldDisableChains) {
if (!shouldDisableChains)
return chainMetadata;
return objMap(chainMetadata, (_, chain) => {
if (chain.availability?.status === ChainStatus.Disabled) {
return { ...chain, disabled: true };
}
return chain;
});
}
//# sourceMappingURL=ChainSearchMenu.js.map