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 7 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
- [#92](https://github.com/alleslabs/celatone-frontend/pull/92) Create select contract component for admin and migrate pages
- [#101](https://github.com/alleslabs/celatone-frontend/pull/101) Fix incorrect truncating of proposal id in contract detail's migration table
- [#100](https://github.com/alleslabs/celatone-frontend/pull/100) Fix contract instantiated time parsing
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
59 changes: 59 additions & 0 deletions src/lib/components/forms/FilterByPermission.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Grid } from "@chakra-ui/react";
import type { IconType } from "react-icons";
import { MdCheck, MdHowToVote, MdPerson } from "react-icons/md";

import { SelectInput } from "./SelectInput";

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

interface FilterByPermissionProps {
setPermissionValue: (newVal: string) => 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
formLabel="Filter by Instantiate Permission"
options={options}
onChange={setPermissionValue}
placeholder="Select"
initialSelected={initialSelected}
/>
</Grid>
);
};
59 changes: 46 additions & 13 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";

const ITEM_HEIGHT = 56;

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

interface SelectItemProps {
Expand All @@ -32,18 +43,20 @@ 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>
);
};

Expand All @@ -53,19 +66,27 @@ export const SelectInput = ({
onChange,
placeholder = "",
initialSelected,
hasDivider = false,
}: SelectInputProps) => {
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 ?? ""
);

const [inputRefWidth, setInputRefWidth] = useState<Option<number>>();
useOutsideClick({
ref: optionRef,
handler: () => isOpen && onClose(),
});
const selectedOption =
jennieramida marked this conversation as resolved.
Show resolved Hide resolved
options.find((item) => item.label === selected) || undefined;

useEffect(() => {
if (inputRef.current) {
setInputRefWidth(inputRef.current.clientWidth);
}
}, [inputRef]);
return (
<Popover placement="bottom-start" isOpen={isOpen}>
<PopoverTrigger>
Expand All @@ -92,13 +113,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 +141,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 +150,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 +165,7 @@ export const SelectInput = ({
}}
disabled={disabled}
>
{icon && <Icon as={icon} boxSize={5} color={iconColor} />}
{label}
</SelectItem>
))}
Expand Down
37 changes: 31 additions & 6 deletions src/lib/pages/all-codes/data.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { useWallet } from "@cosmos-kit/react";
import { useMemo } from "react";

import { useCodeStore } from "lib/hooks";
import { useCodeListQuery } from "lib/services/codeService";
import type { CodeInfo } from "lib/types";
import type { CodeInfo, HumanAddr } from "lib/types";
import { InstantiatePermission } from "lib/types";

interface AllCodesData {
allCodes: CodeInfo[];
isLoading: boolean;
}

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

Expand All @@ -18,11 +23,28 @@ export const useAllCodesData = (keyword?: string): AllCodesData => {
description: getCodeLocalInfo(code.id)?.description,
isSaved: isCodeIdSaved(code.id),
}));
const { address } = useWallet();

return useMemo(() => {
const filterFn = (code: CodeInfo) => {
if (keyword === undefined) return true;
const permissionFilter = (code: CodeInfo) => {
const isEveryBody =
code.instantiatePermission === InstantiatePermission.EVERYBODY;
const isNobody =
code.instantiatePermission === InstantiatePermission.NOBODY;
const isInclude = code.permissionAddresses.includes(address as HumanAddr);

switch (permissionValue) {
case "with-proposal":
return (!isEveryBody && !isInclude) || isNobody;
case "without-proposal":
return isInclude || isEveryBody;
case "all":
default:
return true;
}
};

const searchFilter = (code: CodeInfo) => {
const computedKeyword = keyword.trim();
if (computedKeyword.length === 0) return true;

Expand All @@ -32,6 +54,9 @@ export const useAllCodesData = (keyword?: string): AllCodesData => {
);
};

return { allCodes: allCodes.filter(filterFn), isLoading };
}, [keyword, allCodes, isLoading]);
return {
allCodes: allCodes.filter(permissionFilter).filter(searchFilter),
isLoading,
};
}, [keyword, allCodes, isLoading, permissionValue, address]);
};
48 changes: 35 additions & 13 deletions src/lib/pages/all-codes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { Heading, Box } from "@chakra-ui/react";
import { Heading, Box, Flex } from "@chakra-ui/react";
import { observer } from "mobx-react-lite";
import type { ChangeEvent } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";

import { FilterByPermission } from "lib/components/forms/FilterByPermission";
import InputWithIcon from "lib/components/InputWithIcon";
import { Loading } from "lib/components/Loading";
import CodesTable from "lib/pages/codes/components/CodesTable";

import { useAllCodesData } from "./data";

interface AllCodeState {
keyword: string;
permissionValue: string;
}

const AllCodes = observer(() => {
const [keyword, setKeyword] = useState("");
const { allCodes, isLoading } = useAllCodesData(keyword);
const { watch, setValue } = useForm<AllCodeState>({
defaultValues: {
permissionValue: "all",
keyword: "",
},
});
const states = watch();
jennieramida marked this conversation as resolved.
Show resolved Hide resolved
const { allCodes, isLoading } = useAllCodesData(
states.keyword,
states.permissionValue
);

const handleFilterChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
setKeyword(inputValue);
setValue("keyword", inputValue);
jennieramida marked this conversation as resolved.
Show resolved Hide resolved
};

return (
Expand All @@ -24,13 +39,20 @@ const AllCodes = observer(() => {
<Heading as="h1" size="lg" color="white" mb={4}>
All Codes
</Heading>

<InputWithIcon
placeholder="Search with code ID or code description"
value={keyword}
onChange={handleFilterChange}
size="lg"
/>
<Flex gap={2}>
<InputWithIcon
placeholder="Search with code ID or code description"
value={states.keyword}
onChange={handleFilterChange}
size="lg"
/>
<FilterByPermission
initialSelected="all"
setPermissionValue={(newVal: string) =>
setValue("permissionValue", newVal)
}
/>
</Flex>
</Box>
{isLoading ? (
<Loading />
Expand All @@ -39,7 +61,7 @@ const AllCodes = observer(() => {
type="all"
tableName="All Codes"
codes={allCodes}
isSearching={!!keyword}
isSearching={!!states.keyword}
/>
)}
</Box>
Expand Down
Loading