diff --git a/packages/shared/package.json b/packages/shared/package.json index b00161c5c..048c6b0a9 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@hats.finance/shared", - "version": "1.1.119", + "version": "1.1.120", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/shared/src/types/payout.ts b/packages/shared/src/types/payout.ts index e2ca01aab..ac9407c7a 100644 --- a/packages/shared/src/types/payout.ts +++ b/packages/shared/src/types/payout.ts @@ -42,6 +42,18 @@ export interface IPayoutResponse { updatedAt?: Date; } +export type GithubIssue = { + id: number; + number: number; + title: string; + createdBy: number; + labels: string[]; + validLabels: string[]; + createdAt: string; + body: string; + txHash?: string; +}; + export type IPayoutData = ISinglePayoutData | ISplitPayoutData; interface IPayoutDataBase { @@ -64,6 +76,7 @@ export interface ISinglePayoutData extends IPayoutDataBase { nftUrl: string; submissionData?: { id: string; subId: string; idx: number }; decryptedSubmission?: Omit; // Omit: workaround to avoid circular dependency; + ghIssue?: GithubIssue; } // Only for v2 vaults @@ -89,6 +102,7 @@ export interface ISplitPayoutBeneficiary { nftUrl: string; submissionData?: { id: string; subId: string; idx: number }; decryptedSubmission?: Omit; // Omit: workaround to avoid circular dependency; + ghIssue?: GithubIssue; } export interface IPayoutSignature { diff --git a/packages/shared/src/utils/vaultEditor.utils.ts b/packages/shared/src/utils/vaultEditor.utils.ts index b553b0c20..7ab474adc 100644 --- a/packages/shared/src/utils/vaultEditor.utils.ts +++ b/packages/shared/src/utils/vaultEditor.utils.ts @@ -447,9 +447,9 @@ export function editedFormToCreateVaultOnChainCall( descriptionHash, bountyGovernanceHAT: formatPercentage(getHatsFee()), bountyHackerHATVested: formatPercentage(editedVaultDescription.parameters.fixedHatsRewardPercetange ?? 0), - arbitratorCanChangeBeneficiary: true, - arbitratorCanChangeBounty: true, - arbitratorCanSubmitClaims: true, + arbitratorCanChangeBeneficiary: false, + arbitratorCanChangeBounty: false, + arbitratorCanSubmitClaims: false, isTokenLockRevocable: false, arbitrator: ChainsConfig[+(editedVaultDescription.committee.chainId ?? "1")].arbitratorContract, } as ICreateVaultOnChainCall; diff --git a/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx b/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx index d34f06a87..beac7bd49 100644 --- a/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx +++ b/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx @@ -18,6 +18,7 @@ export interface FormSelectInputOption { interface FormSelectInputProps { name?: string; label?: string; + helper?: string; placeholder?: string; emptyState?: string; multiple?: boolean; @@ -52,6 +53,7 @@ export function FormSelectInputComponent( noSelectedMark = false, emptyState, error, + helper, placeholder, label, }: FormSelectInputProps, @@ -145,6 +147,7 @@ export function FormSelectInputComponent( })} )} + {!error && helper && {helper}} ); } diff --git a/packages/web/src/components/FormControls/FormSelectInput/styles.ts b/packages/web/src/components/FormControls/FormSelectInput/styles.ts index 533a2b700..79df730a5 100644 --- a/packages/web/src/components/FormControls/FormSelectInput/styles.ts +++ b/packages/web/src/components/FormControls/FormSelectInput/styles.ts @@ -24,6 +24,14 @@ export const StyledFormSelectInput = styled.div<{ noMargin: boolean; flexExpand: margin-left: ${getSpacing(1)}; font-size: var(--xxsmall); } + + span.helper { + display: block; + color: var(--secondary-light); + margin-top: ${getSpacing(0.5)}; + margin-left: ${getSpacing(1)}; + font-size: var(--xxsmall); + } ` ); diff --git a/packages/web/src/constants/constants.ts b/packages/web/src/constants/constants.ts index dceeca576..beece24fb 100644 --- a/packages/web/src/constants/constants.ts +++ b/packages/web/src/constants/constants.ts @@ -9,6 +9,7 @@ export enum LocalStorage { ShowedWhereverCTA = "HATS_SHOWED_WHEREVER_CTA", Submissions = "HATS_SUBMISSIONS", SelectedSubmissions = "HATS_USER_SELECTED_SUBMISSIONS", + SelectedVaultInSubmissions = "HATS_USER_SELECTED_VAULT_IN_SUBMISSIONS", CoingeckoPrices = "HATS_COINGECKO_PRICES", CompetitionStreak = "HATS_HAS_SEEN_COMPETITION_STREAK", AirdropModalSeen = "HATS_HAS_SEEN_AIRDROP_MODAL", diff --git a/packages/web/src/hooks/vaults/useUserVaults.tsx b/packages/web/src/hooks/vaults/useUserVaults.tsx index f0467840f..51676121d 100644 --- a/packages/web/src/hooks/vaults/useUserVaults.tsx +++ b/packages/web/src/hooks/vaults/useUserVaults.tsx @@ -73,6 +73,9 @@ export const useUserVaults = (versions: UserVaultsVersion[] = ["all"]) => { } } + foundVaults.sort( + (a, b) => (b.description?.["project-metadata"].starttime ?? 0) - (a.description?.["project-metadata"].starttime ?? 0) + ); setUserVaults(foundVaults); setIsLoading(false); }; diff --git a/packages/web/src/languages/en.json b/packages/web/src/languages/en.json index fa196c616..1ba27ca65 100644 --- a/packages/web/src/languages/en.json +++ b/packages/web/src/languages/en.json @@ -730,6 +730,11 @@ "roles": "Roles", "competitionsRewards": "Competitions rewards", "earnedFees": "Earned fees", + "loadingGithubIssues": "Loading GitHub issues", + "submittedAs": "Submitted as", + "labeledAs": "Labeled as", + "onlyShowLabeledIssues": "Show only labeled issues", + "ghIssue": "GH issue", "MyWallet": { "overview": "Overview", "pointValue": "Point value", @@ -1125,7 +1130,10 @@ "notAllowedToCreatePayout": "You are not allowed to create a payout for this vault. Please verify you are connected with a committee wallet.", "filterBySeverity": "Filter by severity", "filterByTitle": "Filter by title", - "title": "Title" + "filterByVault": "Filter by vault", + "title": "Title", + "showAllVaults": "Show all vaults", + "goToVaultGithubIssues": "Go to vault GitHub issues" }, "Leaderboard": { "ifYouAreAWinner": "If you are one of the winners and you want to claim your rewards, please execute the claim by clicking on the button:", diff --git a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/forms/SinglePayout/SinglePayoutForm.tsx b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/forms/SinglePayout/SinglePayoutForm.tsx index ffe0219d7..82485fbc5 100644 --- a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/forms/SinglePayout/SinglePayoutForm.tsx +++ b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/forms/SinglePayout/SinglePayoutForm.tsx @@ -1,17 +1,19 @@ import { DefaultIndexArray, + GithubIssue, IPayoutData, ISinglePayoutData, IVulnerabilitySeverityV1, IVulnerabilitySeverityV2, } from "@hats.finance/shared"; -import { FormInput, FormSelectInput, Spinner } from "components"; +import { FormInput, FormSelectInput, Loading, Spinner } from "components"; import { getCustomIsDirty, useEnhancedFormContext } from "hooks/form"; import { useOnChange } from "hooks/usePrevious"; import { hasSubmissionData } from "pages/CommitteeTools/PayoutsTool/utils/hasSubmissionData"; import { SubmissionCard } from "pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard"; +import { getGhIssueFromSubmission, getGithubIssuesFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api"; import { useVaultSubmissionsByKeystore } from "pages/CommitteeTools/SubmissionsTool/submissionsService.hooks"; -import { useContext } from "react"; +import { useContext, useEffect, useState } from "react"; import { Controller, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { SinglePayoutAllocation } from "../../../components"; @@ -81,6 +83,23 @@ export const SinglePayoutForm = () => { } }); + const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined); + const [isLoadingGH, setIsLoadingGH] = useState(false); + + // Get information from github + useEffect(() => { + if (!beneficiarySubmission || !vault) return; + if (vaultGithubIssues !== undefined) return; + + const loadGhIssues = async () => { + setIsLoadingGH(true); + const ghIssues = await getGithubIssuesFromVault(vault); + setVaultGithubIssues(ghIssues); + setIsLoadingGH(false); + }; + loadGhIssues(); + }, [vault, vaultGithubIssues, beneficiarySubmission]); + return (
@@ -100,6 +119,10 @@ export const SinglePayoutForm = () => {
) : ( @@ -203,6 +226,7 @@ export const SinglePayoutForm = () => { colorable /> */} + {isLoadingGH && }
); }; diff --git a/packages/web/src/pages/CommitteeTools/PayoutsTool/components/PayoutAllocation/SplitPayoutAllocation/components/SplitPayoutBeneficiaryForm.tsx b/packages/web/src/pages/CommitteeTools/PayoutsTool/components/PayoutAllocation/SplitPayoutAllocation/components/SplitPayoutBeneficiaryForm.tsx index b7bf9ab1c..a40e2c697 100644 --- a/packages/web/src/pages/CommitteeTools/PayoutsTool/components/PayoutAllocation/SplitPayoutAllocation/components/SplitPayoutBeneficiaryForm.tsx +++ b/packages/web/src/pages/CommitteeTools/PayoutsTool/components/PayoutAllocation/SplitPayoutAllocation/components/SplitPayoutBeneficiaryForm.tsx @@ -1,15 +1,16 @@ -import { IPayoutResponse, ISplitPayoutData, IVault } from "@hats.finance/shared"; +import { GithubIssue, IPayoutResponse, ISplitPayoutData, IVault } from "@hats.finance/shared"; import DeleteIcon from "@mui/icons-material/DeleteOutlineOutlined"; import InfoIcon from "@mui/icons-material/InfoOutlined"; import MoreIcon from "@mui/icons-material/MoreVertOutlined"; -import { DropdownSelector, FormInput, FormSelectInput, FormSelectInputOption, Modal, Spinner } from "components"; +import { DropdownSelector, FormInput, FormSelectInput, FormSelectInputOption, Loading, Modal, Spinner } from "components"; import { getCustomIsDirty, useEnhancedFormContext } from "hooks/form"; import useModal from "hooks/useModal"; import { useOnChange } from "hooks/usePrevious"; import { hasSubmissionData } from "pages/CommitteeTools/PayoutsTool/utils/hasSubmissionData"; import { SubmissionCard } from "pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard"; +import { getGhIssueFromSubmission, getGithubIssuesFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api"; import { useVaultSubmissionsByKeystore } from "pages/CommitteeTools/SubmissionsTool/submissionsService.hooks"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Controller, UseFieldArrayRemove, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { SinglePayoutAllocation } from "../../SinglePayoutAllocation/SinglePayoutAllocation"; @@ -97,6 +98,23 @@ export const SplitPayoutBeneficiaryForm = ({ setValue(`beneficiaries.${index}.percentageOfPayout` as any, defaultPoints, { shouldValidate: true }); }); + const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined); + const [isLoadingGH, setIsLoadingGH] = useState(false); + + // Get information from github + useEffect(() => { + if (!beneficiarySubmission || !vault) return; + if (vaultGithubIssues !== undefined) return; + + const loadGhIssues = async () => { + setIsLoadingGH(true); + const ghIssues = await getGithubIssuesFromVault(vault); + setVaultGithubIssues(ghIssues); + setIsLoadingGH(false); + }; + loadGhIssues(); + }, [vault, vaultGithubIssues, beneficiarySubmission]); + const getMoreOptions = () => { if (beneficiariesCount === undefined) return []; if (beneficiariesCount > 1 && !readOnly && !isPayoutCreated) { @@ -148,6 +166,10 @@ export const SplitPayoutBeneficiaryForm = ({ submission={ isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission! } + ghIssue={getGhIssueFromSubmission( + isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission!, + vaultGithubIssues + )} /> ) : ( @@ -241,6 +263,8 @@ export const SplitPayoutBeneficiaryForm = ({ /> + + {isLoadingGH && } ); }; diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx index cf8cbdd18..6553d0978 100644 --- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx +++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx @@ -1,17 +1,19 @@ -import { allowedElementsMarkdown } from "@hats.finance/shared"; +import { GithubIssue, allowedElementsMarkdown } from "@hats.finance/shared"; import ArrowBackIcon from "@mui/icons-material/ArrowBackIosNewOutlined"; import LinkIcon from "@mui/icons-material/InsertLinkOutlined"; import MDEditor from "@uiw/react-md-editor"; -import { Alert, Button, HatSpinner, WalletButton } from "components"; +import { Alert, Button, HatSpinner, Loading, WalletButton } from "components"; import { useKeystore } from "components/Keystore"; import { IPFS_PREFIX } from "constants/constants"; +import { useVaults } from "hooks/subgraph/vaults/useVaults"; import { RoutePaths } from "navigation"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { appChains } from "settings"; import { useAccount } from "wagmi"; import { SubmissionCard } from "../SubmissionsListPage/SubmissionCard"; +import { getGhIssueFromSubmission, getGithubIssuesFromVault } from "../submissionsService.api"; import { useVaultSubmissionsByKeystore } from "../submissionsService.hooks"; import { StyledSubmissionDetailsPage } from "./styles"; @@ -20,15 +22,36 @@ export const SubmissionDetailsPage = () => { const navigate = useNavigate(); const { address } = useAccount(); const { keystore, initKeystore } = useKeystore(); + const { allVaults, vaultsReadyAllChains } = useVaults(); const { subId } = useParams(); const { data: committeeSubmissions, isLoading } = useVaultSubmissionsByKeystore(); const submission = committeeSubmissions?.find((sub) => sub.subId === subId); + const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined); + const [isLoadingGH, setIsLoadingGH] = useState(false); + useEffect(() => { if (!keystore) setTimeout(() => initKeystore(), 600); }, [keystore, initKeystore]); + // Get information from github + useEffect(() => { + if (!submission) return; + + const vault = allVaults?.find((vault) => vault.id.toLowerCase() === submission.linkedVault?.id.toLowerCase()); + if (!vault) return; + if (vaultGithubIssues !== undefined) return; + + const loadGhIssues = async () => { + setIsLoadingGH(true); + const ghIssues = await getGithubIssuesFromVault(vault); + setVaultGithubIssues(ghIssues); + setIsLoadingGH(false); + }; + loadGhIssues(); + }, [allVaults, vaultGithubIssues, submission]); + const openSubmissionData = () => { window.open(`${IPFS_PREFIX}/${submission?.submissionHash}`, "_blank"); }; @@ -82,7 +105,11 @@ export const SubmissionDetailsPage = () => {

- + { )} )} + + {!vaultsReadyAllChains && } + {isLoadingGH && } ); }; diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx index 49f522c08..2bd4a89d2 100644 --- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx +++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx @@ -1,4 +1,4 @@ -import { ISubmittedSubmission, IVulnerabilitySeverity, parseSeverityName } from "@hats.finance/shared"; +import { GithubIssue, ISubmittedSubmission, IVulnerabilitySeverity, parseSeverityName } from "@hats.finance/shared"; import ArrowIcon from "@mui/icons-material/ArrowForwardOutlined"; import BoxUnselected from "@mui/icons-material/CheckBoxOutlineBlankOutlined"; import BoxSelected from "@mui/icons-material/CheckBoxOutlined"; @@ -18,11 +18,13 @@ type SubmissionCardProps = { inPayout?: boolean; isChecked?: boolean; onCheckChange?: (submission: ISubmittedSubmission) => void; + ghIssue?: GithubIssue; }; export const SubmissionCard = ({ submission, onCheckChange, + ghIssue, noActions = false, isChecked = false, inPayout = false, @@ -65,11 +67,27 @@ export const SubmissionCard = ({
{submissionData?.severity && ( + {ghIssue && ( + + {t("ghIssue")} #{ghIssue.number} - + + )} + {t("submittedAs")}: + {ghIssue && ghIssue?.validLabels.length > 0 && ( + <> + {t("labeledAs")}: + + + )} )}

{submissionData?.title}

diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx index 1f38e9bd1..e4dfb205c 100644 --- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx +++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx @@ -1,8 +1,10 @@ import { + GithubIssue, IPayoutData, ISinglePayoutData, ISplitPayoutData, ISubmittedSubmission, + IVault, IVaultDescriptionV2, IVulnerabilitySeverity, PayoutType, @@ -18,14 +20,25 @@ import CalendarIcon from "@mui/icons-material/CalendarTodayOutlined"; import BoxUnselected from "@mui/icons-material/CheckBoxOutlineBlankOutlined"; import BoxSelected from "@mui/icons-material/CheckBoxOutlined"; import ClearIcon from "@mui/icons-material/ClearOutlined"; -import DownloadIcon from "@mui/icons-material/FileDownloadOutlined"; import KeyIcon from "@mui/icons-material/KeyOutlined"; +import OpenIcon from "@mui/icons-material/OpenInNewOutlined"; import RescanIcon from "@mui/icons-material/ReplayOutlined"; import SearchIcon from "@mui/icons-material/SearchOutlined"; import SyncIcon from "@mui/icons-material/SyncOutlined"; import PayoutIcon from "@mui/icons-material/TollOutlined"; import { AxiosError } from "axios"; -import { Alert, Button, FormDateInput, FormInput, FormSelectInput, HatSpinner, Loading, Modal, WalletButton } from "components"; +import { + Alert, + Button, + FormDateInput, + FormInput, + FormSelectInput, + FormSelectInputOption, + HatSpinner, + Loading, + Modal, + WalletButton, +} from "components"; import { useKeystore } from "components/Keystore"; import { IndexedDBs } from "config/DBConfig"; import { LocalStorage } from "constants/constants"; @@ -34,12 +47,16 @@ import { useVaults } from "hooks/subgraph/vaults/useVaults"; import useConfirm from "hooks/useConfirm"; import moment from "moment"; import { RoutePaths } from "navigation"; +import { useVaultRepoName } from "pages/Honeypots/VaultDetailsPage/hooks"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useIndexedDB } from "react-indexed-db-hook"; import { useLocation, useNavigate } from "react-router-dom"; +import { appChains } from "settings"; +import { shortenIfAddress } from "utils/addresses.utils"; import { getVaultCurator } from "utils/curator.utils"; import { useAccount } from "wagmi"; +import { getGhIssueFromSubmission, getGithubIssuesFromVault } from "../submissionsService.api"; import { useCreatePayoutFromSubmissions, useVaultSubmissionsByKeystore } from "../submissionsService.hooks"; import { SubmissionCard } from "./SubmissionCard"; import { StyledSubmissionsListPage } from "./styles"; @@ -59,8 +76,33 @@ export const SubmissionsListPage = () => { const [openDateFilter, setOpenDateFilter] = useState(false); const [dateFilter, setDateFilter] = useState({ from: 0, to: 0, active: false }); - const [severityFilter, setSeverityFilter] = useState(); + const [severityFilter, setSeverityFilter] = useState("all"); const [titleFilter, setTitleFilter] = useState(""); + const [vaultFilter, setVaultFilter] = useState(localStorage.getItem(LocalStorage.SelectedVaultInSubmissions) ?? ""); + const [onlyShowLabeled, setOnlyShowLabeled] = useState(false); + + const filteredVault = allVaults?.find((vault) => vault.id.toLowerCase() === vaultFilter.toLowerCase()); + const { data: filteredVaultRepoName } = useVaultRepoName(filteredVault); + + const goToFilteredVaultGithubIssues = async () => { + if (!filteredVaultRepoName) return; + + const githubLink = `https://github.com/hats-finance/${filteredVaultRepoName}/issues`; + + const wantToGo = await confirm({ + title: t("openGithub"), + titleIcon: , + description: t("doYouWantToSeeSubmissionsOnGithub"), + cancelText: t("no"), + confirmText: t("yesGo"), + }); + + if (!wantToGo) return; + window.open(githubLink, "_blank"); + }; + + const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined); + const [isLoadingGH, setIsLoadingGH] = useState(false); const { data: committeeSubmissions, isLoading, loadingProgress } = useVaultSubmissionsByKeystore(); @@ -82,12 +124,23 @@ export const SubmissionsListPage = () => { if (titleFilter) { filteredSubmissions = filteredSubmissions.filter((submission) => { if (!submission.submissionDataStructure?.title) return false; - console.log(titleFilter); return submission.submissionDataStructure.title.toLowerCase().includes(titleFilter.toLowerCase()); }); } + if (vaultFilter && vaultFilter !== "all") { + filteredSubmissions = filteredSubmissions.filter((submission) => { + if (!submission.linkedVault?.id) return false; + return submission.linkedVault?.id.toLowerCase() === vaultFilter.toLowerCase(); + }); + } + if (vaultFilter && vaultFilter !== "all" && onlyShowLabeled && vaultGithubIssues && vaultGithubIssues.length > 0) { + filteredSubmissions = filteredSubmissions.filter((submission) => { + const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues); + return ghIssue && ghIssue.validLabels.length > 0; + }); + } return filteredSubmissions; - }, [committeeSubmissions, dateFilter, severityFilter, titleFilter]); + }, [committeeSubmissions, dateFilter, severityFilter, titleFilter, vaultFilter, onlyShowLabeled, vaultGithubIssues]); const allSeveritiesOptions = useMemo(() => { if (!committeeSubmissions) return []; @@ -103,6 +156,44 @@ export const SubmissionsListPage = () => { return options; }, [committeeSubmissions, t]); + const allVaultsOptions = useMemo(() => { + if (!committeeSubmissions || committeeSubmissions.length === 0) return undefined; + const vaults = committeeSubmissions.reduce((prev, submission) => { + if (!submission.linkedVault) return prev; + const vault = submission.linkedVault; + if (vault && !prev.some((v) => v.id === vault.id)) prev.push(vault); + return prev; + }, []); + + vaults.sort( + (a, b) => (b.description?.["project-metadata"].starttime ?? 0) - (a.description?.["project-metadata"].starttime ?? 0) + ); + + const options: FormSelectInputOption[] = + vaults?.map((vault) => ({ + value: vault.id, + label: vault.description?.["project-metadata"].name ?? vault.name, + icon: vault.description?.["project-metadata"].icon, + onHoverText: `${vault.version} - ${appChains[vault.chainId as number].chain.name}`, + helper: ( +
+ {vault.version === "v1" + ? `${shortenIfAddress(vault.master.address, { startLength: 6, endLength: 6 })} (PID: ${vault.pid})` + : shortenIfAddress(vault.id, { startLength: 6, endLength: 6 })} + +
+ ), + onHelperClick: () => + window.open( + appChains[vault.chainId as number].chain.blockExplorers?.default.url + "/address/" + vault.id ?? vault.master.address, + "_blank" + ), + })) ?? []; + options.push({ label: t("all"), value: "all" }); + + return options; + }, [committeeSubmissions, t]); + const createPayoutFromSubmissions = useCreatePayoutFromSubmissions(); const [page, setPage] = useState(1); @@ -159,6 +250,15 @@ export const SubmissionsListPage = () => { sessionStorage.setItem(LocalStorage.SelectedSubmissions, JSON.stringify(selectedSubmissions)); }, [selectedSubmissions]); + // Set by default first vault as vaultFilter + useEffect(() => { + if (!allVaultsOptions) return; + if (vaultFilter) return; + + if (allVaultsOptions.length === 0) return setVaultFilter("all"); + setVaultFilter(allVaultsOptions[0].value); + }, [allVaultsOptions, vaultFilter]); + // Get selected submissions from navigation state useEffect(() => { const navigationState = location.state as { selectedSubmissions?: string[] }; @@ -168,33 +268,52 @@ export const SubmissionsListPage = () => { navigate(location.pathname, { replace: true }); }, [location, navigate]); - const handleDownloadAsCsv = () => { - if (!filteredSubmissions) return; - if (filteredSubmissions.length === 0) return; - - const submissionsToDownload = - selectedSubmissions.length === 0 - ? filteredSubmissions - : filteredSubmissions.filter((submission) => selectedSubmissions.includes(submission.subId)); - - const csvString = [ - ["beneficiary", "severity", "title"], - ...submissionsToDownload.map((submission, idx) => [ - submission.submissionDataStructure?.beneficiary, - submission.submissionDataStructure?.severity?.toLowerCase(), - submission.submissionDataStructure?.title.replaceAll(",", "."), - ]), - ] - .map((e) => e.join(",")) - .join("\n"); - - const blob = new Blob([csvString], { type: "text/csv" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.setAttribute("href", url); - a.setAttribute("download", `hats-submissions-${new Date().getTime()}.csv`); - a.click(); - }; + // Get information from github + useEffect(() => { + if (!vaultFilter || vaultFilter === "all") return; + + const vault = allVaults?.find((vault) => vault.id.toLowerCase() === vaultFilter.toLowerCase()); + if (!vault) return; + if (vaultGithubIssues !== undefined) return; + + const loadGhIssues = async () => { + setIsLoadingGH(true); + const ghIssues = await getGithubIssuesFromVault(vault); + setVaultGithubIssues(ghIssues); + setIsLoadingGH(false); + }; + loadGhIssues(); + + console.log(filteredSubmissions); + }, [vaultFilter, filteredSubmissions, allVaults, vaultGithubIssues]); + + // const handleDownloadAsCsv = () => { + // if (!filteredSubmissions) return; + // if (filteredSubmissions.length === 0) return; + + // const submissionsToDownload = + // selectedSubmissions.length === 0 + // ? filteredSubmissions + // : filteredSubmissions.filter((submission) => selectedSubmissions.includes(submission.subId)); + + // const csvString = [ + // ["beneficiary", "severity", "title"], + // ...submissionsToDownload.map((submission, idx) => [ + // submission.submissionDataStructure?.beneficiary, + // submission.submissionDataStructure?.severity?.toLowerCase(), + // submission.submissionDataStructure?.title.replaceAll(",", "."), + // ]), + // ] + // .map((e) => e.join(",")) + // .join("\n"); + + // const blob = new Blob([csvString], { type: "text/csv" }); + // const url = window.URL.createObjectURL(blob); + // const a = document.createElement("a"); + // a.setAttribute("href", url); + // a.setAttribute("download", `hats-submissions-${new Date().getTime()}.csv`); + // a.click(); + // }; const handleChangePage = (direction: number) => () => { if (direction === -1) { @@ -278,16 +397,20 @@ export const SubmissionsListPage = () => { .find((sev) => submission?.submissionDataStructure?.severity?.includes(sev.name.toLowerCase())) ?.name.toLowerCase() ?? submission?.submissionDataStructure?.severity; + const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues); + payoutData = { ...(createNewPayoutData("single") as ISinglePayoutData), beneficiary: submission?.submissionDataStructure?.beneficiary, - severity: severity ?? "", + severity: ghIssue ? ghIssue?.validLabels[0] ?? "" : severity ?? "", submissionData: { id: submission?.id, subId: submission?.subId, idx: submission?.submissionIdx }, depositors: getVaultDepositors(vault), curator: await getVaultCurator(vault), + ghIssue, } as ISinglePayoutData; } else { const submissions = committeeSubmissions.filter((sub) => selectedSubmissions.includes(sub.subId)); + payoutData = { ...(createNewPayoutData("split") as ISplitPayoutData), beneficiaries: submissions.map((submission) => { @@ -296,11 +419,14 @@ export const SubmissionsListPage = () => { .find((sev) => submission?.submissionDataStructure?.severity?.includes(sev.name.toLowerCase())) ?.name.toLowerCase() ?? submission?.submissionDataStructure?.severity; + const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues); + return { ...createNewSplitPayoutBeneficiary(), beneficiary: submission?.submissionDataStructure?.beneficiary, - severity: severity ?? "", + severity: ghIssue ? ghIssue?.validLabels[0] ?? "" : severity ?? "", submissionData: { id: submission?.id, subId: submission?.subId, idx: submission?.submissionIdx }, + ghIssue, }; }), usingPointingSystem: (vault.description as IVaultDescriptionV2).usingPointingSystem, @@ -370,7 +496,7 @@ export const SubmissionsListPage = () => {
-
+
{allPageSelected ? ( ) : ( @@ -394,11 +520,41 @@ export const SubmissionsListPage = () => { colorable options={allSeveritiesOptions} noMargin - onChange={(severity) => setSeverityFilter(severity as string)} + onChange={(severity) => { + setSeverityFilter(severity as string); + setPage(1); + }} />
+
+

+ {(page - 1) * ITEMS_PER_PAGE + 1}-{(page - 1) * ITEMS_PER_PAGE + quantityInPage} + of {filteredSubmissions?.length ?? 0} +

+
+ + +
+
+
+ { + setVaultFilter(vaultId as string); + localStorage.setItem(LocalStorage.SelectedVaultInSubmissions, vaultId as string); + setVaultGithubIssues(undefined); + setSelectedSubmissions([]); + setPage(1); + }} + /> +
{ placeholder={t("SubmissionsTool.title")} colorable noMargin - onChange={(e) => setTitleFilter(e.target.value as string)} + onChange={(e) => { + setTitleFilter(e.target.value as string); + setPage(1); + }} />
-
-
-

- {(page - 1) * ITEMS_PER_PAGE + 1}-{(page - 1) * ITEMS_PER_PAGE + quantityInPage} - of {filteredSubmissions?.length ?? 0} -

-
- - +
+ + +
+ {vaultFilter !== "all" && vaultGithubIssues && vaultGithubIssues.length > 0 && ( + setOnlyShowLabeled(e.target.checked)} + type="toggle" + label={t("onlyShowLabeledIssues")} + noMargin + /> + )} + {filteredVault && ( + + )} +
@@ -495,6 +667,7 @@ export const SubmissionsListPage = () => { isChecked={selectedSubmissions.includes(submission.subId)} key={submission.subId} submission={submission} + ghIssue={getGhIssueFromSubmission(submission, vaultGithubIssues)} /> ); })} @@ -503,24 +676,72 @@ export const SubmissionsListPage = () => {
- {Array.from( + + {(() => { + const totalPages = filteredSubmissions ? Math.ceil(filteredSubmissions?.length / ITEMS_PER_PAGE) : 1; + + if (totalPages <= 20) { + return Array.from(Array(totalPages).keys()).map((pageIdx) => ( +

setPage(pageIdx + 1)} + className={`number ${page === pageIdx + 1 && "current"}`} + > + {pageIdx + 1} +

+ )); + } else { + return ( + <> + {page > 1 && ( + <> +

setPage(1)} className={`number ${page === 1 && "current"}`}> + 1 +

+

...

+ + )} + + {Array.from( + { length: 20 }, + (_, i) => i + (totalPages - page > 20 ? page : totalPages - 20) + ).map((pageIdx) => ( +

setPage(pageIdx)} + className={`number ${page === pageIdx && "current"}`} + > + {pageIdx} +

+ ))} + + {totalPages - page > 20 &&

...

} +

setPage(totalPages)} className={`number ${page === totalPages && "current"}`}> + {totalPages} +

+ + ); + } + })()} + + {/* {Array.from( { length: filteredSubmissions ? Math.ceil(filteredSubmissions?.length / ITEMS_PER_PAGE) : 1 }, (_, i) => i + 1 ).map((pageIdx) => (

setPage(pageIdx)} className={`${page === pageIdx && "current"}`}> {pageIdx}

- ))} + ))} */}
- + */} {selectedSubmissions.length >= 1 && (