Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/search code by permission #93

Merged
merged 18 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- [#93](https://github.com/alleslabs/celatone-frontend/pull/93) Add filter code by instantiate permission in all codes page
- [#141](https://github.com/alleslabs/celatone-frontend/pull/141) Add 404 not found page, catch network params error
- [#134](https://github.com/alleslabs/celatone-frontend/pull/134) Fix un-align sub-page with sidebar
- [#144](https://github.com/alleslabs/celatone-frontend/pull/144) Add `Assign me` for admin address on instantiate form
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/InputWithIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ const InputWithIcon = ({
onChange={onChange}
size={size}
/>
<InputRightElement h="full">
<SearchIcon color="input.main" />
<InputRightElement h="56px" alignItems="center">
<SearchIcon color="gray.600" />
</InputRightElement>
</InputGroup>
);
Expand Down
61 changes: 61 additions & 0 deletions src/lib/components/forms/FilterByPermission.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Grid } from "@chakra-ui/react";
import type { IconType } from "react-icons";
import { MdCheck, MdHowToVote, MdPerson } from "react-icons/md";

import type { PermissionFilterValue } from "lib/hooks";

import { SelectInput } from "./SelectInput";

interface PermissionOption {
label: string;
value: PermissionFilterValue;
disabled: boolean;
icon?: IconType;
iconColor: string;
jennieramida marked this conversation as resolved.
Show resolved Hide resolved
}

interface FilterByPermissionProps {
setPermissionValue: (newVal: PermissionFilterValue) => void;
initialSelected: string;
}

const options: PermissionOption[] = [
{
label: "All",
value: "all",
disabled: false,
icon: MdCheck,
iconColor: "gray.600",
},
{
label: "Can Instantiate without proposal",
value: "without-proposal",
disabled: false,
icon: MdPerson,
iconColor: "primary.main",
},
{
label: "Instantiate through proposal only",
value: "with-proposal",
disabled: false,
icon: MdHowToVote,
iconColor: "text.dark",
},
];

export const FilterByPermission = ({
setPermissionValue,
initialSelected,
}: FilterByPermissionProps) => {
return (
<Grid columnGap="16px" w="full" mb="16px" maxW="360px">
<SelectInput<PermissionFilterValue>
formLabel="Filter by Instantiate Permission"
options={options}
onChange={setPermissionValue}
placeholder="Select"
initialSelected={initialSelected}
/>
</Grid>
);
};
68 changes: 50 additions & 18 deletions src/lib/components/forms/SelectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,33 @@ import {
Popover,
PopoverTrigger,
PopoverContent,
Box,
useDisclosure,
useOutsideClick,
Flex,
InputLeftElement,
} from "@chakra-ui/react";
import type { MutableRefObject, ReactNode } from "react";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import type { IconType } from "react-icons/lib";
import { MdArrowDropDown } from "react-icons/md";

const ITEM_HEIGHT = 57;
import type { Option } from "lib/types";

interface SelectInputProps {
const ITEM_HEIGHT = 56;

interface SelectInputProps<T extends string> {
formLabel?: string;
options: { label: string; value: string; disabled: boolean }[];
onChange: (newVal: string) => void;
options: {
label: string;
value: T;
disabled: boolean;
icon?: IconType;
iconColor?: string;
}[];
onChange: (newVal: T) => void;
placeholder?: string;
initialSelected: string;
hasDivider?: boolean;
}

interface SelectItemProps {
Expand All @@ -32,40 +43,49 @@ interface SelectItemProps {

const SelectItem = ({ children, onSelect, disabled }: SelectItemProps) => {
return (
jennieramida marked this conversation as resolved.
Show resolved Hide resolved
<Box
p={4}
<Flex
px={4}
py={2}
onClick={onSelect}
color="text.main"
transition="all .2s"
cursor="pointer"
gap={2}
aria-disabled={disabled}
_hover={{ bg: "gray.800" }}
_disabled={{ opacity: 0.4, pointerEvents: "none" }}
>
{children}
</Box>
</Flex>
);
};

export const SelectInput = ({
export const SelectInput = <T extends string>({
formLabel,
options,
onChange,
placeholder = "",
initialSelected,
}: SelectInputProps) => {
hasDivider = false,
}: SelectInputProps<T>) => {
const optionRef = useRef() as MutableRefObject<HTMLElement>;
const { isOpen, onClose, onOpen } = useDisclosure();

const inputRef = useRef() as MutableRefObject<HTMLInputElement>;
const [selected, setSelected] = useState(
() => options.find((asset) => asset.value === initialSelected)?.label ?? ""
() => options.find((item) => item.value === initialSelected)?.label ?? ""
);

const [inputRefWidth, setInputRefWidth] = useState<Option<number>>();
useOutsideClick({
ref: optionRef,
handler: () => isOpen && onClose(),
});
const selectedOption = options.find((item) => item.label === selected);

useEffect(() => {
if (inputRef.current) {
setInputRefWidth(inputRef.current.clientWidth);
}
}, [inputRef]);
return (
<Popover placement="bottom-start" isOpen={isOpen}>
<PopoverTrigger>
Expand All @@ -92,13 +112,24 @@ export const SelectInput = ({
}}
>
<div className="form-label">{formLabel}</div>
{selectedOption?.icon && (
<InputLeftElement pointerEvents="none" h="full">
<Icon
as={selectedOption.icon}
color={selectedOption.iconColor}
fontSize="20px"
/>
</InputLeftElement>
)}
<Input
size="lg"
textAlign="start"
type="button"
value={selected || placeholder}
fontSize="14px"
color={selected ? "text.main" : "text.dark"}
ref={inputRef}
pl={selectedOption?.icon ? 9 : 4}
/>
<InputRightElement pointerEvents="none" h="full">
<Icon as={MdArrowDropDown} color="text.dark" fontSize="24px" />
Expand All @@ -109,7 +140,7 @@ export const SelectInput = ({
ref={optionRef}
border="unset"
bg="gray.900"
w="200px"
w={inputRefWidth}
maxH={`${ITEM_HEIGHT * 4}px`}
overflow="scroll"
borderRadius="4px"
Expand All @@ -118,12 +149,12 @@ export const SelectInput = ({
}}
sx={{
"> div:not(:last-of-type)": {
borderBottom: "1px solid",
borderBottomColor: "divider.main",
borderBottom: hasDivider && "1px solid",
borderBottomColor: hasDivider && "divider.main",
},
}}
>
{options.map(({ label, value, disabled }) => (
{options.map(({ label, value, disabled, icon, iconColor }) => (
<SelectItem
key={value}
onSelect={() => {
Expand All @@ -133,6 +164,7 @@ export const SelectInput = ({
}}
disabled={disabled}
>
{icon && <Icon as={icon} boxSize={5} color={iconColor} />}
{label}
</SelectItem>
))}
Expand Down
1 change: 1 addition & 0 deletions src/lib/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./useLCDEndpoint";
export * from "./useUserKey";
export * from "./useDummyWallet";
export * from "./useAddress";
export * from "./useCodeFilter";
46 changes: 46 additions & 0 deletions src/lib/hooks/useCodeFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useWallet } from "@cosmos-kit/react";
import { useCallback } from "react";

import type { CodeInfo, HumanAddr } from "lib/types";
import { InstantiatePermission } from "lib/types";

export type PermissionFilterValue =
| "all"
| "without-proposal"
| "with-proposal";

export const usePermissionFilter = (filterValue?: PermissionFilterValue) => {
const { address } = useWallet();
return useCallback(
({ instantiatePermission, permissionAddresses }: CodeInfo) => {
const isAllowed =
permissionAddresses.includes(address as HumanAddr) ||
instantiatePermission === InstantiatePermission.EVERYBODY;

switch (filterValue) {
case "with-proposal":
return !isAllowed;
case "without-proposal":
return isAllowed;
case "all":
default:
return true;
}
},
[address, filterValue]
);
};

export const useSearchFilter = (keyword = "") => {
return useCallback(
(code: CodeInfo) => {
const computedKeyword = keyword.trim();
if (!computedKeyword.length) return true;
return (
code.id.toString().startsWith(computedKeyword) ||
code.description?.toLowerCase().includes(computedKeyword.toLowerCase())
);
},
[keyword]
);
};
27 changes: 12 additions & 15 deletions src/lib/pages/all-codes/data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo } from "react";

import { useCodeStore } from "lib/hooks";
import type { PermissionFilterValue } from "lib/hooks";
import { useCodeStore, usePermissionFilter, useSearchFilter } from "lib/hooks";
import { useCodeListQuery } from "lib/services/codeService";
import type { CodeInfo } from "lib/types";

Expand All @@ -9,9 +10,14 @@ interface AllCodesData {
isLoading: boolean;
}

export const useAllCodesData = (keyword?: string): AllCodesData => {
export const useAllCodesData = (
keyword: string,
permissionValue: PermissionFilterValue
): AllCodesData => {
const { getCodeLocalInfo, isCodeIdSaved } = useCodeStore();
const { data: rawAllCodes = [], isLoading } = useCodeListQuery();
const permissionFilterFn = usePermissionFilter(permissionValue);
const searchFilterFn = useSearchFilter(keyword);

const allCodes = rawAllCodes.map<CodeInfo>((code) => ({
...code,
Expand All @@ -20,18 +26,9 @@ export const useAllCodesData = (keyword?: string): AllCodesData => {
}));

return useMemo(() => {
const filterFn = (code: CodeInfo) => {
if (keyword === undefined) return true;

const computedKeyword = keyword.trim();
if (computedKeyword.length === 0) return true;

return (
code.id.toString().startsWith(computedKeyword) ||
code.description?.toLowerCase().includes(computedKeyword.toLowerCase())
);
return {
allCodes: allCodes.filter(permissionFilterFn).filter(searchFilterFn),
isLoading,
};

return { allCodes: allCodes.filter(filterFn), isLoading };
}, [keyword, allCodes, isLoading]);
}, [allCodes, isLoading, permissionFilterFn, searchFilterFn]);
};
Loading