diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d598afd..f48c60aef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- [#82](https://github.com/alleslabs/celatone-frontend/pull/82) Add all codes page - [#83](https://github.com/alleslabs/celatone-frontend/pull/83) Add invalid code state - [#73](https://github.com/alleslabs/celatone-frontend/pull/73) Wireup migration table - [#77](https://github.com/alleslabs/celatone-frontend/pull/77) Wireup code info section in code details page diff --git a/src/lib/components/InputWithIcon.tsx b/src/lib/components/InputWithIcon.tsx index afd71f7fb..8d049aa9f 100644 --- a/src/lib/components/InputWithIcon.tsx +++ b/src/lib/components/InputWithIcon.tsx @@ -24,7 +24,7 @@ const InputWithIcon = ({ onChange={onChange} size={size} /> - + diff --git a/src/lib/components/modal/code/CodeDetailsTemplate.tsx b/src/lib/components/modal/code/CodeDetailsTemplate.tsx new file mode 100644 index 000000000..349c7f11c --- /dev/null +++ b/src/lib/components/modal/code/CodeDetailsTemplate.tsx @@ -0,0 +1,126 @@ +import { Flex, Icon, Text, useToast } from "@chakra-ui/react"; +import { useCallback, useEffect, useState } from "react"; +import { MdAddCircleOutline, MdCheckCircle } from "react-icons/md"; + +import { ActionModal } from ".."; +import { ExplorerLink } from "lib/components/ExplorerLink"; +import { TextInput } from "lib/components/forms"; +import { MAX_CODE_DESCRIPTION_LENGTH } from "lib/data"; +import { useCodeStore, useGetAddressType } from "lib/hooks"; +import type { CodeLocalInfo } from "lib/stores/code"; + +interface CodeDetailsTemplateProps { + title: string; + helperText?: string; + mainBtnTitle: string; + isNewCode: boolean; + codeLocalInfo: CodeLocalInfo; + triggerElement: JSX.Element; +} + +export const CodeDetailsTemplate = ({ + title, + helperText, + mainBtnTitle, + isNewCode, + codeLocalInfo, + triggerElement, +}: CodeDetailsTemplateProps) => { + const { saveNewCode, updateCodeInfo } = useCodeStore(); + const toast = useToast(); + const getAddressType = useGetAddressType(); + + const [description, setDescription] = useState( + codeLocalInfo.description ?? "" + ); + + const uploaderType = getAddressType(codeLocalInfo.uploader); + + const handleAction = useCallback(() => { + if (isNewCode) saveNewCode(codeLocalInfo.id); + + updateCodeInfo(codeLocalInfo.id, codeLocalInfo.uploader, description); + + // TODO: abstract toast to template later + toast({ + title, + status: "success", + duration: 5000, + isClosable: false, + position: "bottom-right", + icon: ( + + ), + }); + }, [ + codeLocalInfo.id, + codeLocalInfo.uploader, + description, + isNewCode, + saveNewCode, + title, + toast, + updateCodeInfo, + ]); + + // fix prefilling blank space problem (e.g. description saved as " ") + useEffect(() => { + setDescription(codeLocalInfo.description ?? ""); + }, [codeLocalInfo.description]); + + return ( + + {helperText && ( + + {helperText} + + )} + + + Code ID + + + + + + Uploader + + + + + } + > + + + ); +}; diff --git a/src/lib/components/modal/code/EditCodeDetails.tsx b/src/lib/components/modal/code/EditCodeDetails.tsx new file mode 100644 index 000000000..40f821f8a --- /dev/null +++ b/src/lib/components/modal/code/EditCodeDetails.tsx @@ -0,0 +1,20 @@ +import type { CodeLocalInfo } from "lib/stores/code"; + +import { CodeDetailsTemplate } from "./CodeDetailsTemplate"; + +interface EditCodeDetailsProps { + codeLocalInfo: CodeLocalInfo; + triggerElement: JSX.Element; +} +export const EditCodeDetails = ({ + codeLocalInfo, + triggerElement, +}: EditCodeDetailsProps) => ( + +); diff --git a/src/lib/components/modal/code/SaveCodeDetails.tsx b/src/lib/components/modal/code/SaveCodeDetails.tsx new file mode 100644 index 000000000..f99406054 --- /dev/null +++ b/src/lib/components/modal/code/SaveCodeDetails.tsx @@ -0,0 +1,21 @@ +import type { CodeLocalInfo } from "lib/stores/code"; + +import { CodeDetailsTemplate } from "./CodeDetailsTemplate"; + +interface SaveCodeDetailsProps { + codeLocalInfo: CodeLocalInfo; + triggerElement: JSX.Element; +} +export const SaveCodeDetails = ({ + codeLocalInfo, + triggerElement, +}: SaveCodeDetailsProps) => ( + +); diff --git a/src/lib/components/modal/code/SaveNewCode.tsx b/src/lib/components/modal/code/SaveNewCode.tsx index 5b44a288b..1a5262254 100644 --- a/src/lib/components/modal/code/SaveNewCode.tsx +++ b/src/lib/components/modal/code/SaveNewCode.tsx @@ -48,7 +48,7 @@ export function SaveNewCodeModal({ buttonProps }: ModalProps) { /* DEPENDENCY */ const toast = useToast(); - const { isCodeIdExist, saveNewCode, updateCodeInfo, getCodeLocalInfo } = + const { isCodeIdSaved, saveNewCode, updateCodeInfo, getCodeLocalInfo } = useCodeStore(); const endpoint = useEndpoint(); @@ -85,17 +85,7 @@ export function SaveNewCodeModal({ buttonProps }: ModalProps) { const id = Number(codeId); saveNewCode(id); - - if (description.trim().length) { - updateCodeInfo(id, { - description, - uploader, - }); - } else { - updateCodeInfo(id, { - uploader, - }); - } + updateCodeInfo(id, uploader, description); // TODO: abstract toast to template later toast({ @@ -134,7 +124,7 @@ export function SaveNewCodeModal({ buttonProps }: ModalProps) { } else { setCodeIdStatus({ state: "loading" }); - if (isCodeIdExist(Number(codeId))) { + if (isCodeIdSaved(Number(codeId))) { setCodeIdStatus({ state: "error", message: "You already added this Code ID", @@ -149,7 +139,7 @@ export function SaveNewCodeModal({ buttonProps }: ModalProps) { } return () => {}; - }, [isCodeIdExist, codeId, refetch]); + }, [isCodeIdSaved, codeId, refetch]); // update code description useEffect(() => { diff --git a/src/lib/components/modal/code/SaveOrEditCode.tsx b/src/lib/components/modal/code/SaveOrEditCode.tsx index b9b6316c6..589e337ec 100644 --- a/src/lib/components/modal/code/SaveOrEditCode.tsx +++ b/src/lib/components/modal/code/SaveOrEditCode.tsx @@ -1,23 +1,15 @@ -import { Button, chakra, Flex, Icon, Text, useToast } from "@chakra-ui/react"; +import { Button, chakra, Icon } from "@chakra-ui/react"; import { observer } from "mobx-react-lite"; -import { useCallback, useEffect, useState } from "react"; -import { - MdAddCircleOutline, - MdBookmark, - MdCheckCircle, - MdMode, -} from "react-icons/md"; +import { MdBookmark, MdMode } from "react-icons/md"; -import type { ActionModalProps } from ".."; -import { ActionModal } from ".."; -import { ExplorerLink } from "lib/components/ExplorerLink"; -import { TextInput } from "lib/components/forms"; -import { MAX_CODE_DESCRIPTION_LENGTH } from "lib/data"; -import { useCodeStore, useGetAddressType } from "lib/hooks"; -import type { CodeInfo } from "lib/types"; +import type { CodeLocalInfo } from "lib/stores/code"; -interface SaveOrEditCodeModalProps extends Omit { +import { EditCodeDetails } from "./EditCodeDetails"; +import { SaveCodeDetails } from "./SaveCodeDetails"; + +interface SaveOrEditCodeModalProps { mode: "save" | "edit"; + codeLocalInfo: CodeLocalInfo; } const StyledIcon = chakra(Icon, { @@ -27,143 +19,28 @@ const StyledIcon = chakra(Icon, { }); export const SaveOrEditCodeModal = observer( - ({ - mode, - id, - uploader, - description: savedDesc = "", - }: SaveOrEditCodeModalProps) => { - const { saveNewCode, updateCodeInfo } = useCodeStore(); - const toast = useToast(); - const getAddressType = useGetAddressType(); - - const [description, setDescription] = useState(savedDesc); - - const isSaveMode = mode === "save"; - const uploaderType = getAddressType(uploader); - - const handleAction = useCallback(() => { - if (isSaveMode) saveNewCode(id); - - updateCodeInfo(id, { - uploader, - description, - }); - - // TODO: abstract toast to template later - toast({ - title: isSaveMode - ? `Saved ’${description || id}’ to Saved Codes` - : "New Code Description saved", - status: "success", - duration: 5000, - isClosable: false, - position: "bottom-right", - icon: ( - - ), - }); - }, [ - description, - id, - isSaveMode, - saveNewCode, - toast, - updateCodeInfo, - uploader, - ]); - - const modeBoundProps: Pick< - ActionModalProps, - "title" | "trigger" | "mainBtnTitle" - > = isSaveMode - ? { - title: "Save New Code", - trigger: ( - - ), - mainBtnTitle: "Save New Code", + ({ mode, codeLocalInfo }: SaveOrEditCodeModalProps) => { + return mode === "save" ? ( + } + > + Save Code + } - : { - title: "Edit Code Description", - trigger: ( - - ), - mainBtnTitle: "Save", - }; - - // fix prefilling blank space problem (e.g. description saved as " ") - useEffect(() => { - setDescription(savedDesc); - }, [savedDesc]); - - return ( - - {isSaveMode && ( - - Save other stored codes to your "Saved Codes" list - - )} - - - Code ID - - - - - - Uploader - - {uploader ? ( - - ) : ( - - N/A - - )} - - + /> + ) : ( + }> + Edit + } - > - - + /> ); } ); diff --git a/src/lib/components/modal/code/SaveOrRemoveCode.tsx b/src/lib/components/modal/code/SaveOrRemoveCode.tsx new file mode 100644 index 000000000..f1d2790b7 --- /dev/null +++ b/src/lib/components/modal/code/SaveOrRemoveCode.tsx @@ -0,0 +1,47 @@ +import { chakra, IconButton } from "@chakra-ui/react"; +import { MdBookmark, MdBookmarkBorder } from "react-icons/md"; + +import type { CodeInfo } from "lib/types"; + +import { RemoveCode } from "./RemoveCode"; +import { SaveCodeDetails } from "./SaveCodeDetails"; + +const StyledIconButton = chakra(IconButton, { + baseStyle: { + display: "flex", + alignItems: "center", + fontSize: "22px", + borderRadius: "36px", + }, +}); + +interface SaveOrRemoveCodeModalProps { + codeInfo: CodeInfo; +} + +export function SaveOrRemoveCode({ codeInfo }: SaveOrRemoveCodeModalProps) { + return codeInfo.isSaved ? ( + } + variant="ghost-gray" + color="primary.main" + /> + } + /> + ) : ( + } + variant="ghost-gray" + color="gray.600" + /> + } + /> + ); +} diff --git a/src/lib/data/queries.ts b/src/lib/data/queries.ts index 35b730f2f..7ccfda54f 100644 --- a/src/lib/data/queries.ts +++ b/src/lib/data/queries.ts @@ -1,5 +1,23 @@ import { graphql } from "lib/gql"; +export const getCodeListQueryDocument = graphql(` + query getCodeListQuery { + codes(limit: 500, offset: 0, order_by: { id: desc }) { + id + contracts_aggregate { + aggregate { + count + } + } + account { + uploader: address + } + access_config_permission + access_config_addresses + } + } +`); + export const getCodeListByUserQueryDocument = graphql(` query getCodeListByUserQuery($walletAddr: String!) { codes( @@ -9,7 +27,11 @@ export const getCodeListByUserQueryDocument = graphql(` order_by: { id: desc } ) { id - instantiated: contract_instantiated + contracts_aggregate { + aggregate { + count + } + } account { uploader: address } @@ -23,7 +45,11 @@ export const getCodeListByIDsQueryDocument = graphql(` query getCodeListByIDsQuery($ids: [Int!]!) { codes(where: { id: { _in: $ids } }) { id - instantiated: contract_instantiated + contracts_aggregate { + aggregate { + count + } + } account { uploader: address } diff --git a/src/lib/gql/gql.ts b/src/lib/gql/gql.ts index 6e6456554..b6725c731 100644 --- a/src/lib/gql/gql.ts +++ b/src/lib/gql/gql.ts @@ -3,9 +3,11 @@ import * as types from "./graphql"; import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core"; const documents = { - "\n query getCodeListByUserQuery($walletAddr: String!) {\n codes(\n where: { account: { address: { _eq: $walletAddr } } }\n limit: 500\n offset: 0\n order_by: { id: desc }\n ) {\n id\n instantiated: contract_instantiated\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n": + "\n query getCodeListQuery {\n codes(limit: 500, offset: 0, order_by: { id: desc }) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n": + types.GetCodeListQueryDocument, + "\n query getCodeListByUserQuery($walletAddr: String!) {\n codes(\n where: { account: { address: { _eq: $walletAddr } } }\n limit: 500\n offset: 0\n order_by: { id: desc }\n ) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n": types.GetCodeListByUserQueryDocument, - "\n query getCodeListByIDsQuery($ids: [Int!]!) {\n codes(where: { id: { _in: $ids } }) {\n id\n instantiated: contract_instantiated\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n": + "\n query getCodeListByIDsQuery($ids: [Int!]!) {\n codes(where: { id: { _in: $ids } }) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n": types.GetCodeListByIDsQueryDocument, "\n query getInstantiatedCountByUserQueryDocument($walletAddr: String!) {\n contracts_aggregate(\n where: { transaction: { account: { address: { _eq: $walletAddr } } } }\n ) {\n aggregate {\n count\n }\n }\n }\n": types.GetInstantiatedCountByUserQueryDocumentDocument, @@ -30,11 +32,14 @@ const documents = { }; export function graphql( - source: "\n query getCodeListByUserQuery($walletAddr: String!) {\n codes(\n where: { account: { address: { _eq: $walletAddr } } }\n limit: 500\n offset: 0\n order_by: { id: desc }\n ) {\n id\n instantiated: contract_instantiated\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n" -): typeof documents["\n query getCodeListByUserQuery($walletAddr: String!) {\n codes(\n where: { account: { address: { _eq: $walletAddr } } }\n limit: 500\n offset: 0\n order_by: { id: desc }\n ) {\n id\n instantiated: contract_instantiated\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n"]; + source: "\n query getCodeListQuery {\n codes(limit: 500, offset: 0, order_by: { id: desc }) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n" +): typeof documents["\n query getCodeListQuery {\n codes(limit: 500, offset: 0, order_by: { id: desc }) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n"]; export function graphql( - source: "\n query getCodeListByIDsQuery($ids: [Int!]!) {\n codes(where: { id: { _in: $ids } }) {\n id\n instantiated: contract_instantiated\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n" -): typeof documents["\n query getCodeListByIDsQuery($ids: [Int!]!) {\n codes(where: { id: { _in: $ids } }) {\n id\n instantiated: contract_instantiated\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n"]; + source: "\n query getCodeListByUserQuery($walletAddr: String!) {\n codes(\n where: { account: { address: { _eq: $walletAddr } } }\n limit: 500\n offset: 0\n order_by: { id: desc }\n ) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n" +): typeof documents["\n query getCodeListByUserQuery($walletAddr: String!) {\n codes(\n where: { account: { address: { _eq: $walletAddr } } }\n limit: 500\n offset: 0\n order_by: { id: desc }\n ) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n"]; +export function graphql( + source: "\n query getCodeListByIDsQuery($ids: [Int!]!) {\n codes(where: { id: { _in: $ids } }) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n" +): typeof documents["\n query getCodeListByIDsQuery($ids: [Int!]!) {\n codes(where: { id: { _in: $ids } }) {\n id\n contracts_aggregate {\n aggregate {\n count\n }\n }\n account {\n uploader: address\n }\n access_config_permission\n access_config_addresses\n }\n }\n"]; export function graphql( source: "\n query getInstantiatedCountByUserQueryDocument($walletAddr: String!) {\n contracts_aggregate(\n where: { transaction: { account: { address: { _eq: $walletAddr } } } }\n ) {\n aggregate {\n count\n }\n }\n }\n" ): typeof documents["\n query getInstantiatedCountByUserQueryDocument($walletAddr: String!) {\n contracts_aggregate(\n where: { transaction: { account: { address: { _eq: $walletAddr } } } }\n ) {\n aggregate {\n count\n }\n }\n }\n"]; diff --git a/src/lib/gql/graphql.ts b/src/lib/gql/graphql.ts index 31442f753..2d85c0d5d 100644 --- a/src/lib/gql/graphql.ts +++ b/src/lib/gql/graphql.ts @@ -6247,6 +6247,26 @@ export type Transactions_Variance_Order_By = { sender?: InputMaybe; }; +export type GetCodeListQueryQueryVariables = Exact<{ [key: string]: never }>; + +export type GetCodeListQueryQuery = { + __typename?: "query_root"; + codes: Array<{ + __typename?: "codes"; + id: number; + access_config_permission: string; + access_config_addresses: any; + contracts_aggregate: { + __typename?: "contracts_aggregate"; + aggregate?: { + __typename?: "contracts_aggregate_fields"; + count: number; + } | null; + }; + account: { __typename?: "accounts"; uploader: string }; + }>; +}; + export type GetCodeListByUserQueryQueryVariables = Exact<{ walletAddr: Scalars["String"]; }>; @@ -6258,7 +6278,13 @@ export type GetCodeListByUserQueryQuery = { id: number; access_config_permission: string; access_config_addresses: any; - instantiated: number; + contracts_aggregate: { + __typename?: "contracts_aggregate"; + aggregate?: { + __typename?: "contracts_aggregate_fields"; + count: number; + } | null; + }; account: { __typename?: "accounts"; uploader: string }; }>; }; @@ -6274,7 +6300,13 @@ export type GetCodeListByIDsQueryQuery = { id: number; access_config_permission: string; access_config_addresses: any; - instantiated: number; + contracts_aggregate: { + __typename?: "contracts_aggregate"; + aggregate?: { + __typename?: "contracts_aggregate_fields"; + count: number; + } | null; + }; account: { __typename?: "accounts"; uploader: string }; }>; }; @@ -6452,6 +6484,104 @@ export type GetCodeInfoByCodeIdQuery = { } | null; }; +export const GetCodeListQueryDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "getCodeListQuery" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "codes" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "limit" }, + value: { kind: "IntValue", value: "500" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "offset" }, + value: { kind: "IntValue", value: "0" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "order_by" }, + value: { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "id" }, + value: { kind: "EnumValue", value: "desc" }, + }, + ], + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { + kind: "Field", + name: { kind: "Name", value: "contracts_aggregate" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "aggregate" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "count" }, + }, + ], + }, + }, + ], + }, + }, + { + kind: "Field", + name: { kind: "Name", value: "account" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + alias: { kind: "Name", value: "uploader" }, + name: { kind: "Name", value: "address" }, + }, + ], + }, + }, + { + kind: "Field", + name: { kind: "Name", value: "access_config_permission" }, + }, + { + kind: "Field", + name: { kind: "Name", value: "access_config_addresses" }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + GetCodeListQueryQuery, + GetCodeListQueryQueryVariables +>; export const GetCodeListByUserQueryDocument = { kind: "Document", definitions: [ @@ -6548,8 +6678,25 @@ export const GetCodeListByUserQueryDocument = { { kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", - alias: { kind: "Name", value: "instantiated" }, - name: { kind: "Name", value: "contract_instantiated" }, + name: { kind: "Name", value: "contracts_aggregate" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "aggregate" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "count" }, + }, + ], + }, + }, + ], + }, }, { kind: "Field", @@ -6650,8 +6797,25 @@ export const GetCodeListByIDsQueryDocument = { { kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", - alias: { kind: "Name", value: "instantiated" }, - name: { kind: "Name", value: "contract_instantiated" }, + name: { kind: "Name", value: "contracts_aggregate" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "aggregate" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "count" }, + }, + ], + }, + }, + ], + }, }, { kind: "Field", diff --git a/src/lib/layout/Navbar.tsx b/src/lib/layout/Navbar.tsx index fea5f7482..8c519a0d9 100644 --- a/src/lib/layout/Navbar.tsx +++ b/src/lib/layout/Navbar.tsx @@ -27,9 +27,9 @@ const Navbar = observer(() => { const { getContractLists } = useContractStore(); const { currentChainName } = useWallet(); - const getPublicCodeShortCut = () => + const getAllCodesShortCut = () => PERMISSIONED_CHAINS.includes(currentChainName) - ? [{ name: "Public Codes", slug: "/public-codes", icon: MdPublic }] + ? [{ name: "All Codes", slug: "/all-codes", icon: MdPublic }] : []; const navMenu = [ @@ -63,7 +63,7 @@ const Navbar = observer(() => { category: "Codes", submenu: [ { name: "My Codes", slug: "/codes", icon: MdCode }, - ...getPublicCodeShortCut(), + ...getAllCodesShortCut(), ], }, { diff --git a/src/lib/pages/all-codes/data.ts b/src/lib/pages/all-codes/data.ts new file mode 100644 index 000000000..b2f60dfe5 --- /dev/null +++ b/src/lib/pages/all-codes/data.ts @@ -0,0 +1,37 @@ +import { useMemo } from "react"; + +import { useCodeStore } from "lib/hooks"; +import { useCodeListQuery } from "lib/services/codeService"; +import type { CodeInfo } from "lib/types"; + +interface AllCodesData { + allCodes: CodeInfo[]; + isLoading: boolean; +} + +export const useAllCodesData = (keyword?: string): AllCodesData => { + const { getCodeLocalInfo, isCodeIdSaved } = useCodeStore(); + const { data: rawAllCodes = [], isLoading } = useCodeListQuery(); + + const allCodes = rawAllCodes.map((code) => ({ + ...code, + description: getCodeLocalInfo(code.id)?.description, + isSaved: isCodeIdSaved(code.id), + })); + + 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(filterFn), isLoading }; + }, [keyword, allCodes, isLoading]); +}; diff --git a/src/lib/pages/all-codes/index.tsx b/src/lib/pages/all-codes/index.tsx new file mode 100644 index 000000000..f4b55963b --- /dev/null +++ b/src/lib/pages/all-codes/index.tsx @@ -0,0 +1,49 @@ +import { Heading, Box } from "@chakra-ui/react"; +import { observer } from "mobx-react-lite"; +import type { ChangeEvent } from "react"; +import { useState } from "react"; + +import InputWithIcon from "lib/components/InputWithIcon"; +import { Loading } from "lib/components/Loading"; +import CodesTable from "lib/pages/codes/components/CodesTable"; + +import { useAllCodesData } from "./data"; + +const AllCodes = observer(() => { + const [keyword, setKeyword] = useState(""); + const { allCodes, isLoading } = useAllCodesData(keyword); + + const handleFilterChange = (e: ChangeEvent) => { + const inputValue = e.target.value; + setKeyword(inputValue); + }; + + return ( + + + + All Codes + + + + + {isLoading ? ( + + ) : ( + + )} + + ); +}); + +export default AllCodes; diff --git a/src/lib/pages/code-details/component/CTASection.tsx b/src/lib/pages/code-details/component/CTASection.tsx index 85af6b3c7..856602d3a 100644 --- a/src/lib/pages/code-details/component/CTASection.tsx +++ b/src/lib/pages/code-details/component/CTASection.tsx @@ -16,12 +16,17 @@ const StyledIcon = chakra(Icon, { export const CTASection = observer( ({ id, ...codeInfo }: Omit) => { - const { isCodeIdExist } = useCodeStore(); - const isSaved = isCodeIdExist(id); + const { isCodeIdSaved } = useCodeStore(); + const isSaved = isCodeIdSaved(id); return ( - {isSaved && } + {isSaved && ( + + )} ) : ( - + )} ); diff --git a/src/lib/pages/codes/components/CodeDescriptionCell.tsx b/src/lib/pages/codes/components/CodeDescriptionCell.tsx index 6dee5ae12..8c063bacf 100644 --- a/src/lib/pages/codes/components/CodeDescriptionCell.tsx +++ b/src/lib/pages/codes/components/CodeDescriptionCell.tsx @@ -4,21 +4,18 @@ import { MdCheckCircle } from "react-icons/md"; import { EditableCell } from "lib/components/table"; import { MAX_CODE_DESCRIPTION_LENGTH } from "lib/data"; import { useCodeStore } from "lib/hooks"; +import type { CodeLocalInfo } from "lib/stores/code"; interface CodeDescriptionCellProps { - codeId: number; - description?: string; + code: CodeLocalInfo; } -export const CodeDescriptionCell = ({ - codeId, - description, -}: CodeDescriptionCellProps) => { +export const CodeDescriptionCell = ({ code }: CodeDescriptionCellProps) => { const toast = useToast(); const { updateCodeInfo } = useCodeStore(); const onSave = (inputValue?: string) => { - updateCodeInfo(codeId, { description: inputValue }); + updateCodeInfo(code.id, code.uploader, inputValue); toast({ title: "New Code Description saved", status: "success", @@ -38,7 +35,7 @@ export const CodeDescriptionCell = ({ }; return ( { }; const Empty = ({ type }: OtherTBodyProps) => { + const renderEmptyText = () => { + switch (type) { + case "all": + return "All Code IDs will display here"; + case "saved": + return "Your saved Code IDs will display here. Saved Codes are stored in your device."; + case "stored": + return "Your uploaded Wasm files will display as My Stored Codes"; + default: + return ""; + } + }; return ( - {type === "saved" ? ( - - Your saved Code ID will display here. Saved Codes are stored in your - device. - - ) : ( - - Your uploaded Wasm files will display as My Stored Codes - - )} + {renderEmptyText()} ); }; @@ -98,6 +104,7 @@ const TableHead = () => { th": { padding: "16px", textTransform: "capitalize", @@ -126,10 +133,12 @@ const TableRow = ({ code, isRemovable }: CodesRowProps) => { return ( td": { padding: "16px" } }} - _hover={{ - bg: "gray.900", + sx={{ + "& td:first-of-type": { pl: "48px" }, + "& td:last-of-type": { pr: "48px" }, + "> td": { padding: "16px" }, }} + _hover={{ bg: "gray.900" }} cursor="pointer" onClick={goToCodeDetails} > @@ -141,7 +150,7 @@ const TableRow = ({ code, isRemovable }: CodesRowProps) => { /> - + { permissionAddresses={code.permissionAddresses} codeId={code.id} /> - {isRemovable && ( + {isRemovable ? ( + ) : ( + )} @@ -217,20 +228,8 @@ function CodesTable({ }: CodesTableProps) { const { address } = useWallet(); - const renderBodyStored = () => { - if (!address) return ; - if (codes.length === 0 && isSearching) return ; - if (codes.length === 0) return ; - return ( - - ); - }; - - const renderBodySaved = () => { + const renderBody = () => { + if (!address && type === "stored") return ; if (codes.length === 0 && isSearching) return ; if (codes.length === 0) return ; return ( @@ -244,13 +243,20 @@ function CodesTable({ return ( - - - {tableName} - + + {type !== "all" && ( + + {tableName} + + )} {action} - {type === "saved" ? renderBodySaved() : renderBodyStored()} + {renderBody()} ); } diff --git a/src/lib/pages/codes/data.ts b/src/lib/pages/codes/data.ts index a33f364ad..ab0149722 100644 --- a/src/lib/pages/codes/data.ts +++ b/src/lib/pages/codes/data.ts @@ -19,7 +19,8 @@ interface CodeListData { export const useCodeListData = (keyword?: string): CodeListData => { const { address } = useWallet(); - const { getCodeLocalInfo, lastSavedCodes, lastSavedCodeIds } = useCodeStore(); + const { getCodeLocalInfo, lastSavedCodes, lastSavedCodeIds, isCodeIdSaved } = + useCodeStore(); const { data: rawStoredCodes = [] } = useCodeListByUserQuery(address); @@ -36,13 +37,12 @@ export const useCodeListData = (keyword?: string): CodeListData => { ); return { ...localSavedCode, - uploader: - localSavedCode.uploader ?? querySavedCodeInfo?.uploader ?? "unknown", contracts: querySavedCodeInfo?.contracts ?? 0, instantiatePermission: querySavedCodeInfo?.instantiatePermission ?? InstantiatePermission.UNKNOWN, permissionAddresses: querySavedCodeInfo?.permissionAddresses ?? [], + isSaved: true, }; } ); @@ -50,10 +50,10 @@ export const useCodeListData = (keyword?: string): CodeListData => { const savedCodesCount = savedCodes?.length ?? 0; const storedCodes = rawStoredCodes.map((code) => { - const localInfo = getCodeLocalInfo(code.id); return { ...code, - description: localInfo?.description, + description: getCodeLocalInfo(code.id)?.description, + isSaved: isCodeIdSaved(code.id), }; }); diff --git a/src/lib/pages/codes/index.tsx b/src/lib/pages/codes/index.tsx index 42e1e200e..bcd96bfec 100644 --- a/src/lib/pages/codes/index.tsx +++ b/src/lib/pages/codes/index.tsx @@ -1,11 +1,17 @@ -import { Heading, Tabs, TabList, TabPanels, TabPanel } from "@chakra-ui/react"; +import { + Heading, + Tabs, + TabList, + TabPanels, + TabPanel, + Box, +} from "@chakra-ui/react"; import { observer } from "mobx-react-lite"; import type { ChangeEvent } from "react"; import { useState } from "react"; import { CustomTab } from "lib/components/CustomTab"; import InputWithIcon from "lib/components/InputWithIcon"; -import PageContainer from "lib/components/PageContainer"; import CodesTable from "lib/pages/codes/components/CodesTable"; import SaveCodeButton from "./components/SaveCodeButton"; @@ -29,23 +35,26 @@ const Codes = observer(() => { }; return ( - - - Code Lists - - + + + + Code Lists + + - - All Codes - My Stored Codes - My Saved Codes - - + + + All Codes + My Stored Codes + My Saved Codes + + + { - + ); }); diff --git a/src/lib/pages/pastTxs/components/FalseState.tsx b/src/lib/pages/past-txs/components/FalseState.tsx similarity index 100% rename from src/lib/pages/pastTxs/components/FalseState.tsx rename to src/lib/pages/past-txs/components/FalseState.tsx diff --git a/src/lib/pages/pastTxs/components/MsgDetail.tsx b/src/lib/pages/past-txs/components/MsgDetail.tsx similarity index 100% rename from src/lib/pages/pastTxs/components/MsgDetail.tsx rename to src/lib/pages/past-txs/components/MsgDetail.tsx diff --git a/src/lib/pages/pastTxs/components/MultipleMsg.tsx b/src/lib/pages/past-txs/components/MultipleMsg.tsx similarity index 100% rename from src/lib/pages/pastTxs/components/MultipleMsg.tsx rename to src/lib/pages/past-txs/components/MultipleMsg.tsx diff --git a/src/lib/pages/pastTxs/components/PastTxTable.tsx b/src/lib/pages/past-txs/components/PastTxTable.tsx similarity index 100% rename from src/lib/pages/pastTxs/components/PastTxTable.tsx rename to src/lib/pages/past-txs/components/PastTxTable.tsx diff --git a/src/lib/pages/pastTxs/components/SingleMsg.tsx b/src/lib/pages/past-txs/components/SingleMsg.tsx similarity index 100% rename from src/lib/pages/pastTxs/components/SingleMsg.tsx rename to src/lib/pages/past-txs/components/SingleMsg.tsx diff --git a/src/lib/pages/pastTxs/components/StepperItem.tsx b/src/lib/pages/past-txs/components/StepperItem.tsx similarity index 100% rename from src/lib/pages/pastTxs/components/StepperItem.tsx rename to src/lib/pages/past-txs/components/StepperItem.tsx diff --git a/src/lib/pages/pastTxs/index.tsx b/src/lib/pages/past-txs/index.tsx similarity index 100% rename from src/lib/pages/pastTxs/index.tsx rename to src/lib/pages/past-txs/index.tsx diff --git a/src/lib/pages/pastTxs/query/graphqlQuery.ts b/src/lib/pages/past-txs/query/graphqlQuery.ts similarity index 100% rename from src/lib/pages/pastTxs/query/graphqlQuery.ts rename to src/lib/pages/past-txs/query/graphqlQuery.ts diff --git a/src/lib/pages/pastTxs/query/useTxQuery.ts b/src/lib/pages/past-txs/query/useTxQuery.ts similarity index 100% rename from src/lib/pages/pastTxs/query/useTxQuery.ts rename to src/lib/pages/past-txs/query/useTxQuery.ts diff --git a/src/lib/pages/upload/index.tsx b/src/lib/pages/upload/index.tsx index 8155dab20..63e16acb8 100644 --- a/src/lib/pages/upload/index.tsx +++ b/src/lib/pages/upload/index.tsx @@ -70,15 +70,24 @@ const Upload = () => { const proceed = useCallback(async () => { const stream = await postUploadTx({ onTxSucceed: (codeId: number) => { - updateCodeInfo(codeId, { - description: codeDesc || `${wasmFile?.name}(${codeId})`, - }); + updateCodeInfo( + codeId, + address, + codeDesc || `${wasmFile?.name}(${codeId})` + ); }, codeDesc, }); if (stream) broadcast(stream); - }, [postUploadTx, broadcast, codeDesc, updateCodeInfo, wasmFile]); + }, [ + postUploadTx, + codeDesc, + broadcast, + updateCodeInfo, + address, + wasmFile?.name, + ]); useEffect(() => { (async () => { diff --git a/src/lib/services/codeService.ts b/src/lib/services/codeService.ts index 03d232d9a..9c89e5a13 100644 --- a/src/lib/services/codeService.ts +++ b/src/lib/services/codeService.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-identical-functions */ import type { UseQueryResult } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; import { useCallback } from "react"; @@ -7,6 +8,7 @@ import { getCodeInfoByCodeId, getCodeListByIDsQueryDocument, getCodeListByUserQueryDocument, + getCodeListQueryDocument, getContractListByCodeId, getContractListCountByCodeId, } from "lib/data/queries"; @@ -18,9 +20,33 @@ import type { Option, InstantiatePermission, PermissionAddresses, + HumanAddr, } from "lib/types"; import { parseDateDefault, parseTxHashOpt, unwrap } from "lib/utils"; +export const useCodeListQuery = (): UseQueryResult> => { + const queryFn = useCallback(async () => { + return indexerGraphClient + .request(getCodeListQueryDocument) + .then(({ codes }) => + codes.map((code) => ({ + id: code.id, + uploader: code.account.uploader as ContractAddr | HumanAddr, + contracts: code.contracts_aggregate.aggregate?.count ?? 0, + instantiatePermission: + code.access_config_permission as InstantiatePermission, + permissionAddresses: + code.access_config_addresses as PermissionAddresses, + })) + ); + }, []); + + // TODO: add query key later + return useQuery(["all_codes"], queryFn, { + keepPreviousData: true, + }); +}; + export const useCodeListByUserQuery = ( walletAddr: Option ): UseQueryResult> => { @@ -34,8 +60,8 @@ export const useCodeListByUserQuery = ( .then(({ codes }) => codes.map((code) => ({ id: code.id, - contracts: code.instantiated, - uploader: code.account.uploader, + uploader: code.account.uploader as ContractAddr | HumanAddr, + contracts: code.contracts_aggregate.aggregate?.count ?? 0, instantiatePermission: code.access_config_permission as InstantiatePermission, permissionAddresses: @@ -62,8 +88,8 @@ export const useCodeListByIDsQuery = (ids: Option) => { .then(({ codes }) => codes.map((code) => ({ id: code.id, - uploader: code.account.uploader, - contracts: code.instantiated, + uploader: code.account.uploader as ContractAddr | HumanAddr, + contracts: code.contracts_aggregate.aggregate?.count ?? 0, instantiatePermission: code.access_config_permission as InstantiatePermission, permissionAddresses: @@ -94,7 +120,7 @@ export const useCodeInfoByCodeId = ( return { codeId: codes_by_pk.id, - uploader: codes_by_pk.account.address, + uploader: codes_by_pk.account.address as ContractAddr | HumanAddr, hash: parseTxHashOpt(codes_by_pk.transaction?.hash), height: codes_by_pk.transaction?.block.height, created: parseDateDefault(codes_by_pk.transaction?.block?.timestamp), diff --git a/src/lib/stores/code.ts b/src/lib/stores/code.ts index e491754c8..65a54ef4d 100644 --- a/src/lib/stores/code.ts +++ b/src/lib/stores/code.ts @@ -4,14 +4,9 @@ import { makePersistable } from "mobx-persist-store"; import type { Dict } from "lib/types"; export interface CodeLocalInfo { - description?: string; - uploader?: string; -} - -interface SavedCodeInfo { id: number; + uploader: string; description?: string; - uploader?: string; } export class CodeStore { @@ -19,7 +14,7 @@ export class CodeStore { savedCodeIds: Dict; - codeInfo: Dict>; + codeInfo: Dict>; constructor() { this.savedCodeIds = {}; @@ -46,7 +41,7 @@ export class CodeStore { return this.codeInfo[this.userKey]?.[id]; } - isCodeIdExist(id: number): boolean { + isCodeIdSaved(id: number): boolean { return this.savedCodeIds[this.userKey]?.includes(id) ?? false; } @@ -54,7 +49,7 @@ export class CodeStore { return this.savedCodeIds[userKey]?.slice().reverse() ?? []; } - lastSavedCodes(userKey: string): SavedCodeInfo[] { + lastSavedCodes(userKey: string): CodeLocalInfo[] { const savedCodeIdsByUserKey = this.savedCodeIds[userKey]; if (!savedCodeIdsByUserKey) return []; @@ -62,8 +57,8 @@ export class CodeStore { return savedCodeIdsByUserKey .map((codeId) => ({ id: codeId, + uploader: this.codeInfo[userKey]?.[codeId]?.uploader ?? "TODO", description: this.codeInfo[userKey]?.[codeId]?.description, - uploader: this.codeInfo[userKey]?.[codeId]?.uploader, })) .reverse(); } @@ -82,16 +77,14 @@ export class CodeStore { ); } - updateCodeInfo(id: number, newCodeInfo: CodeLocalInfo): void { - const codeInfo = this.codeInfo[this.userKey]?.[id] || {}; + updateCodeInfo(id: number, uploader: string, description?: string): void { + const codeInfo = this.codeInfo[this.userKey]?.[id] || { id, uploader }; - if (newCodeInfo.description !== undefined) { - codeInfo.description = newCodeInfo.description.trim().length - ? newCodeInfo.description.trim() + if (description !== undefined) { + codeInfo.description = description.trim().length + ? description.trim() : undefined; } - if (newCodeInfo.uploader !== undefined) - codeInfo.uploader = newCodeInfo.uploader; this.codeInfo[this.userKey] = { ...this.codeInfo[this.userKey], diff --git a/src/lib/types/code.ts b/src/lib/types/code.ts index 4a69d7de1..031d9477a 100644 --- a/src/lib/types/code.ts +++ b/src/lib/types/code.ts @@ -1,3 +1,4 @@ +import type { CodeLocalInfo } from "lib/stores/code"; import type { HumanAddr, ContractAddr, Option } from "lib/types"; export enum InstantiatePermission { @@ -11,13 +12,11 @@ export enum InstantiatePermission { export type PermissionAddresses = (HumanAddr | ContractAddr)[]; -export interface CodeInfo { - id: number; - description?: string; +export interface CodeInfo extends CodeLocalInfo { contracts: number; - uploader: string; instantiatePermission: InstantiatePermission; permissionAddresses: PermissionAddresses; + isSaved?: boolean; } interface CodeProposal { diff --git a/src/pages/all-codes.tsx b/src/pages/all-codes.tsx new file mode 100644 index 000000000..00c7333be --- /dev/null +++ b/src/pages/all-codes.tsx @@ -0,0 +1,3 @@ +import AllCodes from "lib/pages/all-codes"; + +export default AllCodes; diff --git a/src/pages/past-txs.tsx b/src/pages/past-txs.tsx index f10a7a094..a1d23196f 100644 --- a/src/pages/past-txs.tsx +++ b/src/pages/past-txs.tsx @@ -1,3 +1,3 @@ -import PastTxs from "lib/pages/pastTxs"; +import PastTxs from "lib/pages/past-txs"; export default PastTxs;