From ddf233e4aee63f60387b5d5fc63cd46c3b5ea572 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Fri, 25 Jul 2025 12:32:07 +0545 Subject: [PATCH 01/21] Add Pinata ipfs upload API --- govtool/backend/example-config.json | 1 + govtool/backend/src/VVA/API.hs | 27 ++++++++-- govtool/backend/src/VVA/API/Types.hs | 19 +++++++ govtool/backend/src/VVA/Config.hs | 19 ++++--- govtool/backend/src/VVA/Ipfs.hs | 77 ++++++++++++++++++++++++++++ govtool/backend/stack.yaml | 1 + govtool/backend/stack.yaml.lock | 9 +++- govtool/backend/vva-be.cabal | 9 ++-- 8 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 govtool/backend/src/VVA/Ipfs.hs diff --git a/govtool/backend/example-config.json b/govtool/backend/example-config.json index fe6a47420..800f7dde1 100644 --- a/govtool/backend/example-config.json +++ b/govtool/backend/example-config.json @@ -6,6 +6,7 @@ "password" : "postgres", "port" : 5432 }, + "pinataapijwt": "", "port" : 9999, "host" : "localhost", "cachedurationseconds": 20, diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index ca7660358..c17f25d7b 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -13,7 +13,7 @@ import Control.Exception (throw, throwIO) import Control.Monad.Except (runExceptT, throwError) import Control.Monad.Reader -import Data.Aeson (Value(..), Array, decode, encode, ToJSON, toJSON) +import Data.Aeson (Value(..), Array, decode, ToJSON, toJSON) import Data.Bool (Bool) import Data.List (sortOn, sort) import qualified Data.Map as Map @@ -21,6 +21,8 @@ import Data.Maybe (Maybe (Nothing), catMaybes, fromMaybe import Data.Ord (Down (..)) import Data.Text hiding (any, drop, elem, filter, length, map, null, take) import qualified Data.Text as Text +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Vector as V import Data.Time.LocalTime (TimeZone, getCurrentTimeZone) @@ -48,9 +50,14 @@ import VVA.Types (App, AppEnv (..), AppError (CriticalError, InternalError, ValidationError), CacheEnv (..)) import Data.Time (TimeZone, localTimeToUTC) +import qualified VVA.Ipfs as Ipfs +import Data.ByteString.Lazy (ByteString) +import qualified Data.ByteString.Lazy as BSL type VVAApi = - "drep" :> "list" + "ipfs" + :> "upload" :> QueryParam "fileName" Text :> ReqBody '[PlainText] Text :> Post '[JSON] UploadResponse + :<|> "drep" :> "list" :> QueryParam "search" Text :> QueryParams "status" DRepStatus :> QueryParam "sort" DRepSortMode @@ -89,7 +96,8 @@ type VVAApi = :<|> "account" :> Capture "stakeKey" HexText :> Get '[JSON] GetAccountInfoResponse server :: App m => ServerT VVAApi m -server = drepList +server = upload + :<|> drepList :<|> getVotingPower :<|> getVotes :<|> drepInfo @@ -107,6 +115,19 @@ server = drepList :<|> getNetworkTotalStake :<|> getAccountInfo +upload :: App m => Maybe Text -> Text -> m UploadResponse +upload mFileName fileContentText = do + AppEnv {vvaConfig} <- ask + let fileContent = TL.encodeUtf8 $ TL.fromStrict fileContentText + vvaPinataJwt = pinataApiJwt vvaConfig + fileName = fromMaybe "data.txt" mFileName -- Default to data.txt if no filename is provided + when (BSL.length fileContent > 1024 * 512) $ + throwError $ ValidationError "The uploaded file is larger than 500Kb" + eIpfsHash <- liftIO $ Ipfs.ipfsUpload vvaPinataJwt fileName fileContent + case eIpfsHash of + Left err -> throwError $ InternalError $ "IPFS upload failed: " <> pack err + Right ipfsHash -> return $ UploadResponse ipfsHash + mapDRepType :: Types.DRepType -> DRepType mapDRepType Types.DRep = NormalDRep mapDRepType Types.SoleVoter = SoleVoter diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index d14fd54cb..360f9be64 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -1112,6 +1112,14 @@ data GetAccountInfoResponse } deriving (Generic, Show) deriveJSON (jsonOptions "getAccountInfoResponse") ''GetAccountInfoResponse + +data UploadResponse + = UploadResponse + { uploadResponseIpfsCid :: Text + } + deriving (Generic, Show) +deriveJSON (jsonOptions "uploadResponse") ''UploadResponse + exampleGetAccountInfoResponse :: Text exampleGetAccountInfoResponse = "{\"stakeKey\": \"stake1u9\"," @@ -1125,3 +1133,14 @@ instance ToSchema GetAccountInfoResponse where & description ?~ "GetAccountInfoResponse" & example ?~ toJSON exampleGetAccountInfoResponse + +exampleUploadResponse :: Text +exampleUploadResponse = + "{\"ipfsHash\": \"QmZKLGf2D3Z3F2J2K5J2L5J2L5J2L5J2L5J2L5J2L5J2L5\"}" + +instance ToSchema UploadResponse where + declareNamedSchema _ = pure $ NamedSchema (Just "UploadResponse") $ mempty + & type_ ?~ OpenApiObject + & description ?~ "UploadResponse" + & example + ?~ toJSON exampleUploadResponse diff --git a/govtool/backend/src/VVA/Config.hs b/govtool/backend/src/VVA/Config.hs index cea9e3eb8..49151d630 100644 --- a/govtool/backend/src/VVA/Config.hs +++ b/govtool/backend/src/VVA/Config.hs @@ -32,7 +32,7 @@ import qualified Conferer.Source.Env as Env import Control.Monad.Reader -import Data.Aeson +import Data.Aeson as Aeson import qualified Data.Aeson.Encode.Pretty as AP import Data.ByteString (ByteString, toStrict) import Data.Has (Has, getter) @@ -58,7 +58,7 @@ data DBConfig -- | Port , dBConfigPort :: Int } - deriving (FromConfig, Generic, Show) + deriving (FromConfig, FromJSON, Generic, Show) instance DefaultConfig DBConfig where configDef = DBConfig "localhost" "cexplorer" "postgres" "test" 9903 @@ -79,9 +79,12 @@ data VVAConfigInternal , vVAConfigInternalSentrydsn :: String -- | Sentry environment , vVAConfigInternalSentryEnv :: String + -- | Pinata API JWT + , vVAConfigInternalPinataApiJwt :: Maybe Text } deriving (FromConfig, Generic, Show) + instance DefaultConfig VVAConfigInternal where configDef = VVAConfigInternal @@ -90,7 +93,8 @@ instance DefaultConfig VVAConfigInternal where vVAConfigInternalHost = "localhost", vVaConfigInternalCacheDurationSeconds = 20, vVAConfigInternalSentrydsn = "https://username:password@senty.host/id", - vVAConfigInternalSentryEnv = "development" + vVAConfigInternalSentryEnv = "development", + vVAConfigInternalPinataApiJwt = Nothing } -- | DEX configuration. @@ -108,6 +112,8 @@ data VVAConfig , sentryDSN :: String -- | Sentry environment , sentryEnv :: String + -- | Pinata API JWT + , pinataApiJwt :: Maybe Text } deriving (Generic, Show, ToJSON) @@ -148,7 +154,8 @@ convertConfig VVAConfigInternal {..} = serverHost = vVAConfigInternalHost, cacheDurationSeconds = vVaConfigInternalCacheDurationSeconds, sentryDSN = vVAConfigInternalSentrydsn, - sentryEnv = vVAConfigInternalSentryEnv + sentryEnv = vVAConfigInternalSentryEnv, + pinataApiJwt = vVAConfigInternalPinataApiJwt } -- | Load configuration from a file specified on the command line. Load from @@ -163,7 +170,7 @@ loadVVAConfig configFile = do where buildConfig :: IO Config buildConfig = - Conferer.mkConfig' + mkConfig' [] [ Env.fromConfig "vva", JSON.fromFilePath (fromMaybe "example-config.json" configFile) @@ -185,4 +192,4 @@ getServerPort = asks (serverPort . getter) getServerHost :: (Has VVAConfig r, MonadReader r m) => m Text -getServerHost = asks (serverHost . getter) \ No newline at end of file +getServerHost = asks (serverHost . getter) diff --git a/govtool/backend/src/VVA/Ipfs.hs b/govtool/backend/src/VVA/Ipfs.hs new file mode 100644 index 000000000..2f99414f0 --- /dev/null +++ b/govtool/backend/src/VVA/Ipfs.hs @@ -0,0 +1,77 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module VVA.Ipfs (ipfsUpload) where + +import Control.Exception (SomeException, try) +import Control.Monad.IO.Class (liftIO) +import Data.Aeson (FromJSON(parseJSON), withObject, (.:), eitherDecode) +import qualified Data.ByteString.Lazy as LBS +import Data.Text (Text) +import qualified Data.Text.Encoding as TE +import GHC.Generics (Generic) +import Network.HTTP.Client (newManager, parseRequest, httpLbs, method, requestHeaders, RequestBody(..), Request, responseBody, responseStatus) +import Network.HTTP.Client.TLS (tlsManagerSettings) +import Network.HTTP.Client.MultipartFormData (formDataBody, partBS, partFileRequestBody) +import Network.HTTP.Types.Status (statusIsSuccessful) +import qualified Data.ByteString.Lazy.Char8 as LBS8 +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TL +import qualified Data.Text as T + + +data PinataData = PinataData + { cid :: Text + , size :: Int + , created_at :: Text + , isDuplicate :: Maybe Bool + } deriving (Show, Generic) + +instance FromJSON PinataData + +data PinataSuccessResponse = PinataSuccessResponse + { pinataData :: PinataData + } deriving (Show) + +instance FromJSON PinataSuccessResponse where + parseJSON = withObject "PinataSuccessResponse" $ \v -> PinataSuccessResponse + <$> v .: "data" + +ipfsUpload :: Maybe Text -> Text -> LBS.ByteString -> IO (Either String Text) +ipfsUpload maybeJwt fileName fileContent = + case maybeJwt of + Nothing -> pure $ Left "Backend is not configured to support ipfs upload" + Just "" -> pure $ Left "Backend is not configured to support ipfs upload" + Just jwt -> do + manager <- newManager tlsManagerSettings + initialRequest <- parseRequest "https://uploads.pinata.cloud/v3/files" + let req = initialRequest + { method = "POST" + , requestHeaders = [("Authorization", "Bearer " <> TE.encodeUtf8 jwt)] + } + result <- try $ flip httpLbs manager =<< formDataBody + [ partBS "network" "public" + , partFileRequestBody "file" (T.unpack fileName) $ RequestBodyLBS fileContent + ] + req + + case result of + Left (e :: SomeException) -> do + let errMsg = show e + liftIO $ putStrLn errMsg + pure $ Left errMsg + Right response -> do + let body = responseBody response + let status = responseStatus response + if statusIsSuccessful status + then case eitherDecode body of + Left err -> do + let errMsg = "Failed to decode Pinata API reponse: " <> err <> "\nResponse body: " <> LBS8.unpack body + liftIO $ putStrLn errMsg + pure $ Left errMsg + Right (res :: PinataSuccessResponse) -> pure $ Right $ cid $ pinataData res + else do + let errMsg = "Pinata API request failed with status: " <> show status <> "\nResponse body: " <> LBS8.unpack body + liftIO $ putStrLn errMsg + pure $ Left errMsg diff --git a/govtool/backend/stack.yaml b/govtool/backend/stack.yaml index b6238c401..8de57aa42 100644 --- a/govtool/backend/stack.yaml +++ b/govtool/backend/stack.yaml @@ -4,3 +4,4 @@ packages: extra-deps: - raven-haskell-0.1.4.1@sha256:9187272adc064197528645b5ad9b89163b668f386f34016d97fa646d5c790784 +- http-client-multipart-0.3.0.0@sha256:d675f10cba69c98233467dd533ba46e64f34798fc2ea528efe662ad2ea6c89bf,554 \ No newline at end of file diff --git a/govtool/backend/stack.yaml.lock b/govtool/backend/stack.yaml.lock index 985ab39a4..d03add71d 100644 --- a/govtool/backend/stack.yaml.lock +++ b/govtool/backend/stack.yaml.lock @@ -1,7 +1,7 @@ # This file was autogenerated by Stack. # You should not edit this file by hand. # For more information, please see the documentation at: -# https://docs.haskellstack.org/en/stable/lock_files +# https://docs.haskellstack.org/en/stable/topics/lock_files packages: - completed: @@ -11,6 +11,13 @@ packages: size: 632 original: hackage: raven-haskell-0.1.4.1@sha256:9187272adc064197528645b5ad9b89163b668f386f34016d97fa646d5c790784 +- completed: + hackage: http-client-multipart-0.3.0.0@sha256:d675f10cba69c98233467dd533ba46e64f34798fc2ea528efe662ad2ea6c89bf,554 + pantry-tree: + sha256: a35e249bf5a162c18e5fa2309c5cfcdaaead1d8fc914be029f3f1239102bd648 + size: 164 + original: + hackage: http-client-multipart-0.3.0.0@sha256:d675f10cba69c98233467dd533ba46e64f34798fc2ea528efe662ad2ea6c89bf,554 snapshots: - completed: sha256: e019cd29e3f7f9dbad500225829a3f7a50f73c674614f2f452e21bb8bf5d99ea diff --git a/govtool/backend/vva-be.cabal b/govtool/backend/vva-be.cabal index b644dad9a..37cbf1062 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -45,8 +45,8 @@ executable vva-be -- other-modules: -- LANGUAGE extensions used by modules in this package. - -- other-extensions: - build-depends: base >=4.16 && <4.18 + -- other-extensions9 + build-depends: base >=4.16 && <4.19 , vva-be , optparse-applicative , text @@ -80,7 +80,7 @@ executable vva-be library hs-source-dirs: src - build-depends: base >=4.16 && <4.18 + build-depends: base >=4.16 && <4.19 , servant-server , conferer , mtl @@ -107,9 +107,11 @@ library , swagger2 , http-client , http-client-tls + , http-client-multipart , vector , async , random + , http-types exposed-modules: VVA.Config , VVA.CommandLine @@ -126,4 +128,5 @@ library , VVA.Types , VVA.Network , VVA.Account + , VVA.Ipfs ghc-options: -threaded From 01af374c963180f66555df65bdf054242ae69c3d Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Fri, 25 Jul 2025 12:41:17 +0545 Subject: [PATCH 02/21] Add dummy flow for saving metadata with govtool --- .../VoteContext/VoteContextChoice.tsx | 68 ++++++++++ .../VoteContext/VoteContextGovTool.tsx | 93 +++++++++++++ .../VoteContext/VoteContextModal.tsx | 123 +++++++++++++++--- .../VoteContext/VoteContextTerms.tsx | 2 +- .../VoteContext/VoteContextWrapper.tsx | 45 ++++--- .../src/components/organisms/index.ts | 2 + .../src/hooks/forms/useVoteContextForm.tsx | 3 +- .../frontend/src/services/requests/index.ts | 1 + .../src/services/requests/postIpfs.ts | 10 ++ 9 files changed, 309 insertions(+), 38 deletions(-) create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx create mode 100644 govtool/frontend/src/services/requests/postIpfs.ts diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx new file mode 100644 index 000000000..9ad563265 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -0,0 +1,68 @@ +import { Dispatch, SetStateAction } from "react"; +import { Box, Button } from "@mui/material"; + +import { Spacer, Typography } from "@atoms"; +import { useScreenDimension, useTranslation } from "@hooks"; +import { VoteContextWrapper } from "@organisms"; +import { NodeObject } from "jsonld"; + +type VoteContextChoiceProps = { + setStep: Dispatch>; + setStoreDataYourself: Dispatch>; + setJsonldContent: Dispatch>; + setMetadataHash: Dispatch>; + generateMetadata: () => Promise<{ jsonld: NodeObject; jsonHash: string }>; + onCancel: () => void; +}; + +export const VoteContextChoice = ({ + setStep, + setStoreDataYourself, + setJsonldContent, + setMetadataHash, + generateMetadata, + onCancel, +}: VoteContextChoiceProps) => { + const { t } = useTranslation(); + const { isMobile } = useScreenDimension(); + + const handleStoreItMyself = () => { + setStoreDataYourself(true); + setStep(3); + }; + + const handleLetGovToolStore = async () => { + setStoreDataYourself(false); + const { jsonld, jsonHash } = await generateMetadata(); + setJsonldContent(jsonld); + setMetadataHash(jsonHash); + setStep(3); + }; + + return ( + + + {t("createGovernanceAction.storeDataTitle")} + + + + + + + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx new file mode 100644 index 000000000..e79dde8f8 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx @@ -0,0 +1,93 @@ +import { useEffect, Dispatch, SetStateAction, useState } from "react"; +import { Box, Button, CircularProgress, Link, Typography } from "@mui/material"; +import { useMutation } from "react-query"; + +import { Spacer } from "@atoms"; +import { useTranslation } from "@hooks"; +import { VoteContextWrapper } from "@organisms"; +import { postIpfs } from "@services"; +import { downloadTextFile } from "@utils"; +import { NodeObject } from "jsonld"; +import { UseFormSetValue } from "react-hook-form"; +import { VoteContextFormValues } from "@hooks"; + +interface PostIpfsResponse { + ipfsHash: string; +} + +type VoteContextGovToolProps = { + setStep: Dispatch>; + setSavedHash: Dispatch>; + onCancel: () => void; + submitVoteContext: () => void; + jsonldContent: NodeObject | null; + metadataHash: string | null; + setValue: UseFormSetValue; +}; + +export const VoteContextGovTool = ({ + setStep, + setSavedHash, + onCancel, + submitVoteContext, + jsonldContent, + metadataHash, + setValue, +}: VoteContextGovToolProps) => { + const [apiResponse, setApiResponse] = useState(null); + const { t } = useTranslation(); + + const { mutate, isLoading } = useMutation({ + mutationFn: postIpfs, + onSuccess: (data) => { + const ipfsUrl = `ipfs://${data.ipfsHash}`; + setValue("storingURL", ipfsUrl); + setSavedHash(metadataHash); // Set savedHash to metadataHash + setApiResponse(JSON.stringify(data, null, 2)); + }, + }); + + useEffect(() => { + if (jsonldContent) { + mutate({ content: JSON.stringify(jsonldContent, null, 2) }); + } + }, [jsonldContent, mutate]); + + const handleDownload = () => { + if (jsonldContent) { + downloadTextFile(JSON.stringify(jsonldContent, null, 2), "voteContext.jsonld"); + } + }; + + return ( + + + {t("createGovernanceAction.letGovToolStore")} + + + {isLoading ? ( + + + + ) : apiResponse ? ( + <> + + {apiResponse} + + + + {t("createGovernanceAction.downloadJsonLd")} + + + ) : ( + + {t("createGovernanceAction.uploadingToIPFS")} + + )} + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx index 4ceca049a..878a19d42 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; import { useForm, FormProvider } from "react-hook-form"; import { ModalWrapper } from "@atoms"; @@ -8,8 +8,12 @@ import { VoteContextCheckResult, VoteContextTerms, VoteContextText, + VoteContextChoice, + VoteContextGovTool, } from "@organisms"; -import { VoteContextFormValues } from "@hooks"; +import { NodeObject } from "jsonld"; +import { VoteContextFormValues, useVoteContextForm } from "@hooks"; +import { UseFormReturn } from "react-hook-form"; export type VoteContextModalState = { onSubmit: (url: string, hash: string | null, voteContextText: string) => void; @@ -17,10 +21,13 @@ export type VoteContextModalState = { export const VoteContextModal = () => { const [step, setStep] = useState(1); + const [storeDataYourself, setStoreDataYourself] = useState(true); const [savedHash, setSavedHash] = useState(""); const [errorMessage, setErrorMessage] = useState( undefined, ); + const [jsonldContent, setJsonldContent] = useState(null); + const [metadataHash, setMetadataHash] = useState(null); const { state, closeModal } = useModal(); @@ -47,29 +54,109 @@ export const VoteContextModal = () => { }} > - {step === 1 && ( - - )} - {step === 2 && ( - - )} - {step === 3 && ( - - )} - {step === 4 && ( - )} + {step === 1 && ( + + )} ); }; + +// New component to encapsulate the flow that uses useVoteContextForm +const VoteContextFlow = ({ + step, + setStep, + storeDataYourself, + setStoreDataYourself, + setSavedHash, + setErrorMessage, + jsonldContent, + setJsonldContent, + metadataHash, + setMetadataHash, + submitVoteContext, + onCancel, + errorMessage, + methods, // Accept methods +}: { + step: number; + setStep: Dispatch>; + storeDataYourself: boolean; + setStoreDataYourself: Dispatch>; + setSavedHash: Dispatch>; + setErrorMessage: Dispatch>; + jsonldContent: NodeObject | null; + setJsonldContent: Dispatch>; + metadataHash: string | null; + setMetadataHash: Dispatch>; + submitVoteContext: () => void; + onCancel: () => void; + errorMessage: string | undefined; + methods: UseFormReturn; // Type for methods +}) => { + const { generateMetadata } = useVoteContextForm(); + + return ( + <> + {step === 2 && ( + + )} + {step === 3 && storeDataYourself && ( + + )} + {step === 3 && !storeDataYourself && ( + + )} + {step === 4 && ( + + )} + {step === 5 && ( + + )} + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx index d59d1af38..ce92076d3 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -23,7 +23,7 @@ export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { return ( setStep(3)} + onContinue={() => setStep(4)} isContinueDisabled={isContinueDisabled} onCancel={onCancel} > diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx index 91ceb8ed5..97a421a73 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx @@ -5,14 +5,21 @@ import { useScreenDimension, useTranslation } from "@hooks"; import { Button } from "@atoms"; type VoteContextWrapperProps = { - onContinue: () => void; + onContinue?: () => void; isContinueDisabled?: boolean; onCancel: () => void; + showContinueButton?: boolean; }; export const VoteContextWrapper: FC< PropsWithChildren -> = ({ onContinue, isContinueDisabled, onCancel, children }) => { +> = ({ + onContinue, + isContinueDisabled, + onCancel, + children, + showContinueButton = true, +}) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -43,22 +50,24 @@ export const VoteContextWrapper: FC< width: isMobile ? "100%" : "154px", }} variant="outlined" - > - {t("cancel")} - - - +> + {t("cancel")} + +{showContinueButton && ( + +)} + ); }; diff --git a/govtool/frontend/src/components/organisms/index.ts b/govtool/frontend/src/components/organisms/index.ts index add7263b4..9ef017f4e 100644 --- a/govtool/frontend/src/components/organisms/index.ts +++ b/govtool/frontend/src/components/organisms/index.ts @@ -28,5 +28,7 @@ export * from "./UncontrolledImageInput"; export * from "./ValidatedGovernanceActionCard"; export * from "./ValidatedGovernanceVotedOnCard"; export * from "./VoteContext"; +export * from "./VoteContext/VoteContextChoice"; +export * from "./VoteContext/VoteContextGovTool"; export * from "./WrongRouteInfo"; export * from "./MaintenanceEndingBanner"; diff --git a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx index e7f8c0144..acd8b0a96 100644 --- a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx @@ -51,7 +51,7 @@ export const useVoteContextForm = ( setHash(jsonHash); setJson(jsonld); - return jsonld; + return { jsonld, jsonHash }; }, [getValues]); const onClickDownloadJson = () => { @@ -102,5 +102,6 @@ export const useVoteContextForm = ( setValue, watch, hash, + json, }; }; diff --git a/govtool/frontend/src/services/requests/index.ts b/govtool/frontend/src/services/requests/index.ts index 598fe76d3..5887c0ee9 100644 --- a/govtool/frontend/src/services/requests/index.ts +++ b/govtool/frontend/src/services/requests/index.ts @@ -21,5 +21,6 @@ export * from "./postDRepRegister"; export * from "./postDRepRemoveVote"; export * from "./postDRepRetire"; export * from "./postDRepVote"; +export * from "./postIpfs"; export * from "./getDRepVotingPowerList"; export * from "./getAccount"; diff --git a/govtool/frontend/src/services/requests/postIpfs.ts b/govtool/frontend/src/services/requests/postIpfs.ts new file mode 100644 index 000000000..105065ed2 --- /dev/null +++ b/govtool/frontend/src/services/requests/postIpfs.ts @@ -0,0 +1,10 @@ +import { API } from "../API"; + +export const postIpfs = async ({ content }: { content: string }) => { + const response = await API.post("/ipfs/upload", content,{ + headers: { + "Content-Type": "text/plain;charset=utf-8" + } + }); + return response.data; +}; From 7a33b9db411e01182792a9df770b6375346a69f0 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Fri, 25 Jul 2025 13:13:47 +0545 Subject: [PATCH 03/21] Update "govtool-pins-metadata" flow to match design --- .../VoteContext/VoteContextChoice.tsx | 56 ++++++++++-- .../VoteContext/VoteContextGovTool.tsx | 89 ++++++++++++++++--- govtool/frontend/src/i18n/locales/en.json | 14 +++ govtool/frontend/src/utils/clipboard.ts | 3 + govtool/frontend/src/utils/index.ts | 1 + 5 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 govtool/frontend/src/utils/clipboard.ts diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx index 9ad563265..03893daa7 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -1,10 +1,12 @@ import { Dispatch, SetStateAction } from "react"; -import { Box, Button } from "@mui/material"; +import { Box, Button, Link } from "@mui/material"; import { Spacer, Typography } from "@atoms"; import { useScreenDimension, useTranslation } from "@hooks"; import { VoteContextWrapper } from "@organisms"; import { NodeObject } from "jsonld"; +import { openInNewTab } from "@utils"; +import { LINKS } from "@/consts/links"; type VoteContextChoiceProps = { setStep: Dispatch>; @@ -26,6 +28,8 @@ export const VoteContextChoice = ({ const { t } = useTranslation(); const { isMobile } = useScreenDimension(); + const openLink = () => openInNewTab(LINKS.STORING_INFORMATION_OFFLINE); + const handleStoreItMyself = () => { setStoreDataYourself(true); setStep(3); @@ -42,23 +46,57 @@ export const VoteContextChoice = ({ return ( - {t("createGovernanceAction.storeDataTitle")} + {t("createGovernanceAction.storeAndMaintainDataTitle")} + + + {t("createGovernanceAction.learnMoreAboutStoringInformation")} + + + {t("createGovernanceAction.govToolProvidesOptions")} + + +
    +
  • + + {t("createGovernanceAction.govToolCanPinToIPFS")} + +
  • +
  • + + {t("createGovernanceAction.storeYourselfInRepo")} + +
  • +
+
+ + {t("createGovernanceAction.chooseDataStorageOption")} - + diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx index e79dde8f8..c8214e4c3 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx @@ -1,18 +1,23 @@ import { useEffect, Dispatch, SetStateAction, useState } from "react"; import { Box, Button, CircularProgress, Link, Typography } from "@mui/material"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { useMutation } from "react-query"; import { Spacer } from "@atoms"; import { useTranslation } from "@hooks"; import { VoteContextWrapper } from "@organisms"; import { postIpfs } from "@services"; -import { downloadTextFile } from "@utils"; +import { downloadTextFile, openInNewTab } from "@utils"; import { NodeObject } from "jsonld"; import { UseFormSetValue } from "react-hook-form"; import { VoteContextFormValues } from "@hooks"; +import { LINKS } from "@/consts/links"; +import { ICONS } from "@/consts/icons"; +import { useSnackbar } from "@context"; +import { copyToClipboard } from "@utils"; interface PostIpfsResponse { - ipfsHash: string; + ipfsCid: string; } type VoteContextGovToolProps = { @@ -34,24 +39,29 @@ export const VoteContextGovTool = ({ metadataHash, setValue, }: VoteContextGovToolProps) => { - const [apiResponse, setApiResponse] = useState(null); + const [apiResponse, setApiResponse] = useState(null); + const [uploadInitiated, setUploadInitiated] = useState(false); // New state to track upload const { t } = useTranslation(); + const { addSuccessAlert } = useSnackbar(); + + const openLink = () => openInNewTab(LINKS.STORING_INFORMATION_OFFLINE); const { mutate, isLoading } = useMutation({ mutationFn: postIpfs, onSuccess: (data) => { - const ipfsUrl = `ipfs://${data.ipfsHash}`; + const ipfsUrl = `ipfs://${data.ipfsCid}`; setValue("storingURL", ipfsUrl); setSavedHash(metadataHash); // Set savedHash to metadataHash - setApiResponse(JSON.stringify(data, null, 2)); + setApiResponse(data); }, }); useEffect(() => { - if (jsonldContent) { + if (jsonldContent && !uploadInitiated) { mutate({ content: JSON.stringify(jsonldContent, null, 2) }); + setUploadInitiated(true); // Set flag after initiating upload } - }, [jsonldContent, mutate]); + }, [jsonldContent, mutate, uploadInitiated]); const handleDownload = () => { if (jsonldContent) { @@ -66,7 +76,33 @@ export const VoteContextGovTool = ({ onCancel={onCancel} > - {t("createGovernanceAction.letGovToolStore")} + {t("createGovernanceAction.rationalePinnedToIPFS")} + + + {t("createGovernanceAction.readFullGuide")} + + + + {t("createGovernanceAction.recommendations")} {isLoading ? ( @@ -75,13 +111,38 @@ export const VoteContextGovTool = ({ ) : apiResponse ? ( <> - - {apiResponse} + + {t("createGovernanceAction.downloadAndStoreMetadataFile")} + + + + {t("createGovernanceAction.rePinYourFile")} - - - {t("createGovernanceAction.downloadJsonLd")} - + + + {apiResponse.ipfsCid ? `ipfs://${apiResponse.ipfsCid}` : "[URI]"} + + {apiResponse.ipfsCid && ( + { + copyToClipboard(`ipfs://${apiResponse.ipfsCid}`); + addSuccessAlert(t("alerts.copiedToClipboard")); + }} + sx={{ cursor: "pointer", display: "flex", alignItems: "center" }} + > + copy + + )} + ) : ( diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 73836a465..99c192a7c 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -246,6 +246,20 @@ "storingInformationURLPlaceholder": "URL", "supportingLinks": "Supporting links", "title": "Create a Governance Action", + "storeAndMaintainDataTitle": "Store and Maintain the Data Yourself", + "learnMoreAboutStoringInformation": "Learn more about storing information", + "govToolProvidesOptions": "GovTool currently provides two options for storing your rationale:", + "govToolCanPinToIPFS": "GovTool can pin it to IPFS", + "storeYourselfInRepo": "Store it yourself in a repository such as GitHub", + "chooseDataStorageOption": "Choose a data storage option:", + "govToolPinsDataToIPFS": "GovTool pins data to IPFS", + "downloadAndStoreYourself": "Download and store yourself", + "uploadingToIPFS": "Uploading to IPFS...", + "rationalePinnedToIPFS": "Your rationale will be pinned to IPFS", + "readFullGuide": "Read full guide", + "recommendations": "Recommendations", + "downloadAndStoreMetadataFile": "Download and store your metadata file\n(if you needed in the future you can re-pin it on IPFS)", + "rePinYourFile": "Re-pin your file", "modals": { "submitTransactionSuccess": { "message": "Your Governance Action may take a little time to submit to the chain.", diff --git a/govtool/frontend/src/utils/clipboard.ts b/govtool/frontend/src/utils/clipboard.ts new file mode 100644 index 000000000..63e04ad3d --- /dev/null +++ b/govtool/frontend/src/utils/clipboard.ts @@ -0,0 +1,3 @@ +export const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); +}; diff --git a/govtool/frontend/src/utils/index.ts b/govtool/frontend/src/utils/index.ts index f331202b3..9ab32b8f0 100644 --- a/govtool/frontend/src/utils/index.ts +++ b/govtool/frontend/src/utils/index.ts @@ -39,3 +39,4 @@ export * from "./getBase64ImageDetails"; export * from "./parseBoolean"; export * from "./validateSignature"; export * from "./cip8verification"; +export * from "./clipboard"; From bcd8875c1bd6054611849c67288ede30a81ccb55 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Wed, 30 Jul 2025 00:20:11 +0545 Subject: [PATCH 04/21] feat: Json response on errors in vva-be --- govtool/backend/app/Main.hs | 20 +++++--- govtool/backend/src/VVA/API.hs | 12 +++-- govtool/backend/src/VVA/Ipfs.hs | 78 +++++++++++++++++++++++++++----- govtool/backend/src/VVA/Types.hs | 11 ++++- govtool/backend/vva-be.cabal | 2 + 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/govtool/backend/app/Main.hs b/govtool/backend/app/Main.hs index d82821424..4ba525ad0 100644 --- a/govtool/backend/app/Main.hs +++ b/govtool/backend/app/Main.hs @@ -16,6 +16,7 @@ import Control.Monad.Trans.Except import Control.Monad.Trans.Reader import Data.Aeson hiding (Error) +import Data.Aeson (encode) import qualified Data.ByteString as BS import Data.ByteString.Char8 (unpack) import qualified Data.Cache as Cache @@ -35,7 +36,7 @@ import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.IO as Text import qualified Data.Text.Lazy as LazyText import qualified Data.Text.Lazy.Encoding as LazyText - +import qualified Data.ByteString.Lazy.Char8 as BS8 import Database.PostgreSQL.Simple (close, connectPostgreSQL, Connection) import Network.Wai @@ -62,8 +63,10 @@ import VVA.API.Types import VVA.CommandLine import VVA.Config import VVA.Types (AppEnv (..), - AppError (CriticalError, InternalError, NotFoundError, ValidationError), + AppError (..), CacheEnv (..)) +import VVA.Ipfs (IpfsError(..)) + -- Function to create a connection pool with optimized settings createOptimizedConnectionPool :: BS.ByteString -> IO (Pool Connection) @@ -288,10 +291,15 @@ liftServer appEnv = where handleErrors :: Either AppError a -> Handler a handleErrors (Right x) = pure x - handleErrors (Left (ValidationError msg)) = throwError $ err400 { errBody = BS.fromStrict $ encodeUtf8 msg } - handleErrors (Left (NotFoundError msg)) = throwError $ err404 { errBody = BS.fromStrict $ encodeUtf8 msg } - handleErrors (Left (CriticalError msg)) = throwError $ err500 { errBody = BS.fromStrict $ encodeUtf8 msg } - handleErrors (Left (InternalError msg)) = throwError $ err500 { errBody = BS.fromStrict $ encodeUtf8 msg } + handleErrors (Left appError) = do + let status = case appError of + ValidationError _ -> err400 + NotFoundError _ -> err404 + CriticalError _ -> err500 + InternalError _ -> err500 + AppIpfsError (OtherIpfsError _) -> err400 + AppIpfsError _ -> err503 + throwError $ status { errBody = encode appError, errHeaders = [("Content-Type", "application/json")] } -- * Swagger type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json" diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index c17f25d7b..6ab25a806 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -1,10 +1,10 @@ -{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE DataKinds #-} module VVA.API where @@ -31,6 +31,7 @@ import Numeric.Natural (Natural) import Servant.API import Servant.Server +import Servant.Exception (Throws) import System.Random (randomRIO) import Text.Read (readMaybe) @@ -47,16 +48,17 @@ import qualified VVA.Proposal as Proposal import qualified VVA.Transaction as Transaction import qualified VVA.Types as Types import VVA.Types (App, AppEnv (..), - AppError (CriticalError, InternalError, ValidationError), + AppError (CriticalError, InternalError, ValidationError, AppIpfsError), CacheEnv (..)) import Data.Time (TimeZone, localTimeToUTC) import qualified VVA.Ipfs as Ipfs import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as BSL +import Servant.Exception (Throws) type VVAApi = "ipfs" - :> "upload" :> QueryParam "fileName" Text :> ReqBody '[PlainText] Text :> Post '[JSON] UploadResponse + :> "upload" :> QueryParam "fileName" Text :> ReqBody '[PlainText] Text :> Post '[JSON] UploadResponse :<|> "drep" :> "list" :> QueryParam "search" Text :> QueryParams "status" DRepStatus @@ -125,8 +127,8 @@ upload mFileName fileContentText = do throwError $ ValidationError "The uploaded file is larger than 500Kb" eIpfsHash <- liftIO $ Ipfs.ipfsUpload vvaPinataJwt fileName fileContent case eIpfsHash of - Left err -> throwError $ InternalError $ "IPFS upload failed: " <> pack err - Right ipfsHash -> return $ UploadResponse ipfsHash + Left err -> throwError $ AppIpfsError err + Right ipfsHash -> return $ UploadResponse ipfsHash mapDRepType :: Types.DRepType -> DRepType mapDRepType Types.DRep = NormalDRep diff --git a/govtool/backend/src/VVA/Ipfs.hs b/govtool/backend/src/VVA/Ipfs.hs index 2f99414f0..86202f298 100644 --- a/govtool/backend/src/VVA/Ipfs.hs +++ b/govtool/backend/src/VVA/Ipfs.hs @@ -2,11 +2,12 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -module VVA.Ipfs (ipfsUpload) where +module VVA.Ipfs (ipfsUpload, IpfsError(..)) where import Control.Exception (SomeException, try) import Control.Monad.IO.Class (liftIO) -import Data.Aeson (FromJSON(parseJSON), withObject, (.:), eitherDecode) +import qualified Data.Aeson as A +import Data.Aeson (FromJSON(parseJSON), withObject, (.:), eitherDecode, ToJSON(..), encode,(.=),object) import qualified Data.ByteString.Lazy as LBS import Data.Text (Text) import qualified Data.Text.Encoding as TE @@ -14,11 +15,13 @@ import GHC.Generics (Generic) import Network.HTTP.Client (newManager, parseRequest, httpLbs, method, requestHeaders, RequestBody(..), Request, responseBody, responseStatus) import Network.HTTP.Client.TLS (tlsManagerSettings) import Network.HTTP.Client.MultipartFormData (formDataBody, partBS, partFileRequestBody) -import Network.HTTP.Types.Status (statusIsSuccessful) +import Network.HTTP.Types.Status (statusIsSuccessful, Status, status503, status400) import qualified Data.ByteString.Lazy.Char8 as LBS8 import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Text as T +import Servant.Server (ServerError (errBody)) +import Servant.Exception (ToServantErr(..), Exception(..)) data PinataData = PinataData @@ -38,11 +41,64 @@ instance FromJSON PinataSuccessResponse where parseJSON = withObject "PinataSuccessResponse" $ \v -> PinataSuccessResponse <$> v .: "data" -ipfsUpload :: Maybe Text -> Text -> LBS.ByteString -> IO (Either String Text) +data IpfsError + = PinataConnectionError String + | PinataAPIError Status LBS.ByteString + | PinataDecodingError String LBS.ByteString + | IpfsUnconfiguredError + | OtherIpfsError String + deriving (Show, Generic) + +instance ToJSON IpfsError where + toJSON (PinataConnectionError msg) = + object ["errorType" .= A.String "PinataConnectionError", "message" .= msg] + + toJSON (PinataAPIError status body) = + object + [ "errorType" .= A.String "PinataAPIError" + , "message" .= ("Pinata API returned error status : " ++ show status) + , "pinataResponse" .= object + [ "status" .= show status + , "body" .= TL.unpack (TL.decodeUtf8 body) + ] + ] + + toJSON (PinataDecodingError msg body) = + object + [ "errorType" .= A.String "PinataDecodingError" + , "message" .= msg + , "pinataResponse" .= object + [ "status" .= ("unknown" :: String) + , "body" .= TL.unpack (TL.decodeUtf8 body) + ] + ] + + toJSON IpfsUnconfiguredError = + object ["errorType" .= A.String "IpfsUnconfiguredError", "message" .= ("Backend is not configured for upfs upload" :: String)] + + toJSON (OtherIpfsError msg) = + object ["errorType" .= A.String "OtherIpfsError", "message" .= msg] + + +instance Exception IpfsError + + + +instance ToServantErr IpfsError where + status (OtherIpfsError _) = status400 + status _ = status503 + + message (PinataConnectionError msg) = T.pack ("Pinata service connection error: " <> msg) + message (PinataAPIError status body) = T.pack ("Pinata API error: " <> show status <> " - " <> LBS8.unpack body) + message (PinataDecodingError msg body) = T.pack ("Pinata decoding error: " <> msg <> " - " <> LBS8.unpack body) + message IpfsUnconfiguredError = T.pack ("Backend is not configured to support ipfs upload") + message (OtherIpfsError msg) = T.pack msg + +ipfsUpload :: Maybe Text -> Text -> LBS.ByteString -> IO (Either IpfsError Text) ipfsUpload maybeJwt fileName fileContent = case maybeJwt of - Nothing -> pure $ Left "Backend is not configured to support ipfs upload" - Just "" -> pure $ Left "Backend is not configured to support ipfs upload" + Nothing -> pure $ Left $ IpfsUnconfiguredError + Just "" -> pure $ Left $ IpfsUnconfiguredError Just jwt -> do manager <- newManager tlsManagerSettings initialRequest <- parseRequest "https://uploads.pinata.cloud/v3/files" @@ -60,18 +116,18 @@ ipfsUpload maybeJwt fileName fileContent = Left (e :: SomeException) -> do let errMsg = show e liftIO $ putStrLn errMsg - pure $ Left errMsg + pure $ Left $ PinataConnectionError errMsg Right response -> do let body = responseBody response let status = responseStatus response if statusIsSuccessful status then case eitherDecode body of Left err -> do - let errMsg = "Failed to decode Pinata API reponse: " <> err <> "\nResponse body: " <> LBS8.unpack body + let errMsg = "Failed to decode Pinata API reponse: " <> err liftIO $ putStrLn errMsg - pure $ Left errMsg + pure $ Left $ PinataDecodingError errMsg body Right (res :: PinataSuccessResponse) -> pure $ Right $ cid $ pinataData res else do - let errMsg = "Pinata API request failed with status: " <> show status <> "\nResponse body: " <> LBS8.unpack body + let errMsg = "Pinata API request failed with status: " <> show status liftIO $ putStrLn errMsg - pure $ Left errMsg + pure $ Left $ PinataAPIError status body diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index 25c248bd3..d5b0f2cf2 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -15,7 +15,7 @@ import Control.Monad.Except (MonadError) import Control.Monad.Fail (MonadFail) import Control.Monad.IO.Class (MonadIO) import Control.Monad.Reader (MonadReader) - +import qualified Data.Aeson as A import Data.Aeson (Value, ToJSON (..), object, (.=)) import qualified Data.Cache as Cache import Data.Has @@ -30,6 +30,7 @@ import Database.PostgreSQL.Simple.FromField (FromField(..), returnErr import VVA.Cache import VVA.Config +import VVA.Ipfs (IpfsError) type App m = (MonadReader AppEnv m, MonadIO m, MonadFail m, MonadError AppError m) @@ -57,10 +58,18 @@ data AppError | NotFoundError Text | CriticalError Text | InternalError Text + | AppIpfsError IpfsError deriving (Show) instance Exception AppError +instance ToJSON AppError where + toJSON (ValidationError msg) = object ["errorType" .= A.String "ValidationError", "message" .= msg] + toJSON (NotFoundError msg) = object ["errorType" .= A.String "NotFoundError", "message" .= msg] + toJSON (CriticalError msg) = object ["errorType" .= A.String "CriticalError", "message" .= msg] + toJSON (InternalError msg) = object ["errorType" .= A.String "InternalError", "message" .= msg] + toJSON (AppIpfsError err) = toJSON err + data Vote = Vote { voteProposalId :: Integer diff --git a/govtool/backend/vva-be.cabal b/govtool/backend/vva-be.cabal index 37cbf1062..8eb0de86d 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -52,6 +52,7 @@ executable vva-be , text , servant-swagger-ui , servant-server + , servant-exceptions , servant-openapi3 , servant , wai @@ -91,6 +92,7 @@ library , bytestring , optparse-applicative , servant + , servant-exceptions , openapi3 , lens , postgresql-simple From 53f2860f099c42552562bdebbb32047b7696fd62 Mon Sep 17 00:00:00 2001 From: joseph rana Date: Mon, 28 Jul 2025 14:32:58 +0545 Subject: [PATCH 05/21] chore: update vote context store options card styles and texts --- .../VoteContext/VoteContextChoice.tsx | 94 ++++++++----------- .../VoteContext/VoteContextWrapper.tsx | 64 +++++++------ govtool/frontend/src/i18n/locales/en.json | 1 + 3 files changed, 76 insertions(+), 83 deletions(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx index 03893daa7..10edd7514 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -44,63 +44,45 @@ export const VoteContextChoice = ({ }; return ( - - - {t("createGovernanceAction.storeAndMaintainDataTitle")} - - - {t("createGovernanceAction.learnMoreAboutStoringInformation")} - - - {t("createGovernanceAction.govToolProvidesOptions")} - - -
    -
  • - - {t("createGovernanceAction.govToolCanPinToIPFS")} - -
  • -
  • - - {t("createGovernanceAction.storeYourselfInRepo")} - -
  • -
-
- - {t("createGovernanceAction.chooseDataStorageOption")} - - - - - - - - + {t("createGovernanceAction.learnMoreAboutStoringInformation")} + + + + {t("createGovernanceAction.chooseDataStorageOption")} + + + + + +
); }; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx index 97a421a73..a7f48f99d 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx @@ -7,8 +7,10 @@ import { Button } from "@atoms"; type VoteContextWrapperProps = { onContinue?: () => void; isContinueDisabled?: boolean; + showCancelButton? : boolean; onCancel: () => void; showContinueButton?: boolean; + showAllButtons? : boolean; }; export const VoteContextWrapper: FC< @@ -17,8 +19,10 @@ export const VoteContextWrapper: FC< onContinue, isContinueDisabled, onCancel, + showCancelButton = true, children, showContinueButton = true, + showAllButtons = true }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -34,40 +38,46 @@ export const VoteContextWrapper: FC< > {children} + { + showAllButtons && + { + showCancelButton && + + } + {showContinueButton && ( -{showContinueButton && ( - -)} - + variant="contained" + > + {t("continue")} + + )} + + } ); }; diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 99c192a7c..d06178a76 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -251,6 +251,7 @@ "govToolProvidesOptions": "GovTool currently provides two options for storing your rationale:", "govToolCanPinToIPFS": "GovTool can pin it to IPFS", "storeYourselfInRepo": "Store it yourself in a repository such as GitHub", + "storingOptionsForYourVoterRationale" : "Storing options for your voter rationale", "chooseDataStorageOption": "Choose a data storage option:", "govToolPinsDataToIPFS": "GovTool pins data to IPFS", "downloadAndStoreYourself": "Download and store yourself", From 7481c0581092fff2419b6bb5c55ab37cc447b23f Mon Sep 17 00:00:00 2001 From: joseph rana Date: Mon, 28 Jul 2025 15:59:57 +0545 Subject: [PATCH 06/21] chore: update vote rationale pinned card styles and text --- .../VoteContext/VoteContextGovTool.tsx | 78 +++++++++++-------- govtool/frontend/src/i18n/locales/en.json | 13 ++-- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx index c8214e4c3..000c754f8 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx @@ -4,7 +4,7 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { useMutation } from "react-query"; import { Spacer } from "@atoms"; -import { useTranslation } from "@hooks"; +import { useScreenDimension, useTranslation } from "@hooks"; import { VoteContextWrapper } from "@organisms"; import { postIpfs } from "@services"; import { downloadTextFile, openInNewTab } from "@utils"; @@ -15,6 +15,7 @@ import { LINKS } from "@/consts/links"; import { ICONS } from "@/consts/icons"; import { useSnackbar } from "@context"; import { copyToClipboard } from "@utils"; +import { primaryBlue } from "@/consts"; interface PostIpfsResponse { ipfsCid: string; @@ -44,6 +45,8 @@ export const VoteContextGovTool = ({ const { t } = useTranslation(); const { addSuccessAlert } = useSnackbar(); + const { isMobile } = useScreenDimension(); + const openLink = () => openInNewTab(LINKS.STORING_INFORMATION_OFFLINE); const { mutate, isLoading } = useMutation({ @@ -74,8 +77,9 @@ export const VoteContextGovTool = ({ onContinue={submitVoteContext} isContinueDisabled={!apiResponse} onCancel={onCancel} + showAllButtons={false} > - + {t("createGovernanceAction.rationalePinnedToIPFS")} - {t("createGovernanceAction.readFullGuide")} - + {t("createGovernanceAction.learnMore")} - - {t("createGovernanceAction.recommendations")} - - {isLoading ? ( ) : apiResponse ? ( <> - - {t("createGovernanceAction.downloadAndStoreMetadataFile")} + + {t("createGovernanceAction.optionalDownloadAndStoreMetadataFile")} - - {t("createGovernanceAction.rePinYourFile")} + + {t("createGovernanceAction.rePinYourFileToIPFS")} - - {apiResponse.ipfsCid ? `ipfs://${apiResponse.ipfsCid}` : "[URI]"} - - {apiResponse.ipfsCid && ( - { - copyToClipboard(`ipfs://${apiResponse.ipfsCid}`); - addSuccessAlert(t("alerts.copiedToClipboard")); - }} - sx={{ cursor: "pointer", display: "flex", alignItems: "center" }} + + {apiResponse.ipfsCid ? ( + - copy - + IPFS URI: {`https://ipfs.io/ipfs/${apiResponse.ipfsCid}`} + + ) : ( + "[URI]" )} + ) : ( @@ -149,6 +143,22 @@ export const VoteContextGovTool = ({ {t("createGovernanceAction.uploadingToIPFS")} )} + + + +
); }; diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index d06178a76..f7a54695e 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -256,17 +256,18 @@ "govToolPinsDataToIPFS": "GovTool pins data to IPFS", "downloadAndStoreYourself": "Download and store yourself", "uploadingToIPFS": "Uploading to IPFS...", - "rationalePinnedToIPFS": "Your rationale will be pinned to IPFS", - "readFullGuide": "Read full guide", - "recommendations": "Recommendations", - "downloadAndStoreMetadataFile": "Download and store your metadata file\n(if you needed in the future you can re-pin it on IPFS)", - "rePinYourFile": "Re-pin your file", + "rationalePinnedToIPFS": "GovTool has pinned your rationale to IPFS", + "learnMore" : "Learn more", + "optionalDownloadAndStoreMetadataFile": "Optional: Download and store a backup copy of your metadata file", + "rePinYourFileToIPFS": "Re-pin your file to IPFS", "modals": { "submitTransactionSuccess": { "message": "Your Governance Action may take a little time to submit to the chain.", "title": "Governance Action submitted!" } - } + }, + "back" : "Back", + "continue" : "Continue" }, "delegation": { "description": "You can delegate your voting power to a DRep or to a pre-defined voting option.", From 9382b1e789978f961324db8b59342741301cddba Mon Sep 17 00:00:00 2001 From: joseph rana Date: Mon, 28 Jul 2025 16:51:27 +0545 Subject: [PATCH 07/21] chore: update vote context terms style --- .../src/components/organisms/VoteContext/VoteContextTerms.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx index ce92076d3..07423d052 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -27,7 +27,7 @@ export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { isContinueDisabled={isContinueDisabled} onCancel={onCancel} > - + {t("createGovernanceAction.storeDataTitle")} Date: Tue, 29 Jul 2025 12:23:26 +0545 Subject: [PATCH 08/21] fix: update vote context workflow --- .../VoteContext/VoteContextCheckResult.tsx | 7 +- .../VoteContext/VoteContextChoice.tsx | 2 +- .../VoteContext/VoteContextGovTool.tsx | 33 +------- .../VoteContext/VoteContextModal.tsx | 3 +- .../VoteContextStoringInformation.tsx | 1 + .../VoteContext/VoteContextWrapper.tsx | 81 +++++++++---------- .../src/hooks/forms/useVoteContextForm.tsx | 4 +- govtool/frontend/src/i18n/locales/en.json | 1 + 8 files changed, 51 insertions(+), 81 deletions(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx index 2e5ab3def..7d23df7c3 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx @@ -48,7 +48,10 @@ export const VoteContextCheckResult = ({ {errorMessage ? "Data validation failed" : "Success"} - {errorMessage ?? "Data check has been successful"} + {errorMessage ?? "GovTool has processed has your rationale"} + + + {errorMessage ?? "You can now proceed to vote."} {!errorMessage ? ( - -
); }; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx index 878a19d42..7913f6196 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -134,11 +134,10 @@ const VoteContextFlow = ({ )} {step === 4 && ( diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx index 953528fba..45bb80ba7 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx @@ -49,6 +49,7 @@ export const VoteContextStoringInformation = ({ onContinue={validateURL} isContinueDisabled={isContinueDisabled} onCancel={onCancel} + useSubmitLabel > {t("createGovernanceAction.storingInformationTitle")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx index a7f48f99d..b86e5c2b5 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx @@ -7,10 +7,10 @@ import { Button } from "@atoms"; type VoteContextWrapperProps = { onContinue?: () => void; isContinueDisabled?: boolean; - showCancelButton? : boolean; - onCancel: () => void; - showContinueButton?: boolean; - showAllButtons? : boolean; + onCancel? : () => void; + hideAllBtn? : boolean; + useBackLabel? : boolean; + useSubmitLabel? : boolean; }; export const VoteContextWrapper: FC< @@ -19,10 +19,10 @@ export const VoteContextWrapper: FC< onContinue, isContinueDisabled, onCancel, - showCancelButton = true, children, - showContinueButton = true, - showAllButtons = true + hideAllBtn = false, + useBackLabel = false, + useSubmitLabel = false }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -39,44 +39,35 @@ export const VoteContextWrapper: FC< {children} { - showAllButtons && - - { - showCancelButton && - - } - {showContinueButton && ( - - )} - + !hideAllBtn && + + + + } ); diff --git a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx index acd8b0a96..e53fb168c 100644 --- a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx @@ -79,11 +79,11 @@ export const useVoteContextForm = ( } catch (error: any) { if (Object.values(MetadataValidationStatus).includes(error)) { if (setErrorMessage) setErrorMessage(error); - if (setStep) setStep(4); + if (setStep) setStep(5); } } finally { if (setSavedHash) setSavedHash(hash); - if (setStep) setStep(4); + if (setStep) setStep(5); } }, [hash], diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index f7a54695e..5ae5f1609 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -260,6 +260,7 @@ "learnMore" : "Learn more", "optionalDownloadAndStoreMetadataFile": "Optional: Download and store a backup copy of your metadata file", "rePinYourFileToIPFS": "Re-pin your file to IPFS", + "oneMomentPlease" : "One Moment please", "modals": { "submitTransactionSuccess": { "message": "Your Governance Action may take a little time to submit to the chain.", From 618205fb23cd9b47ab18aa52704ed018eae62e51 Mon Sep 17 00:00:00 2001 From: joseph rana Date: Tue, 29 Jul 2025 14:23:43 +0545 Subject: [PATCH 09/21] feat: validate and render vote context --- .../components/molecules/VoteActionForm.tsx | 60 +++++++++++-------- .../queries/useGetVoteContextTextFromFile.ts | 33 ++++++---- govtool/frontend/src/i18n/locales/en.json | 2 +- .../frontend/src/models/metadataValidation.ts | 1 + .../requests/getVoteContextTextFromFile.ts | 20 ++++--- 5 files changed, 72 insertions(+), 44 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index 9bc79a797..4ce7a28d9 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -36,7 +36,7 @@ export const VoteActionForm = ({ useState(false); const { voter } = useGetVoterInfo(); - const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl); + const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl , voteContextHash); const { isMobile, screenWidth } = useScreenDimension(); const { openModal } = useModal(); @@ -61,6 +61,7 @@ export const VoteActionForm = ({ if (previousVote?.vote) { setValue("vote", previousVote.vote); setIsVoteSubmitted(true); + } }, [previousVote?.vote, setValue, setIsVoteSubmitted]); @@ -68,33 +69,36 @@ export const VoteActionForm = ({ if (previousVote?.url) { setVoteContextUrl(previousVote.url); } + if (previousVote?.metadataHash) { + setVoteContextHash(previousVote.metadataHash); + } }, [previousVote?.url, setVoteContextUrl]); - + const renderCancelButton = useMemo( () => ( ), [previousVote?.vote, setValue], ); - + const renderChangeVoteButton = useMemo( () => ( )} - { - !voteContextText && - - {t("optional")} - - } - - {voteContextText - ? t("govActions.yourVoteRationale") - : t("govActions.youCanProvideContext") - } - {voteContextText && ( )} - @@ -369,7 +329,7 @@ export const VoteActionForm = ({ !voteContextHash)) } isLoading={isVoteLoading} - onClick={confirmVote} + onClick={handleVoteClick} size="extraLarge" > {t("govActions.vote")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx index 7d23df7c3..d4f18a340 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx @@ -67,7 +67,7 @@ export const VoteContextCheckResult = ({ }} variant="contained" > - {t("govActions.goToVote")} + {t("govActions.submitVote")} ) : ( void; + vote?: Vote; + confirmVote: ( + vote?: Vote, + url?: string, + hash?: string | null, + ) => void; }; export const VoteContextModal = () => { @@ -35,7 +42,7 @@ export const VoteContextModal = () => { const { getValues } = methods; const submitVoteContext = () => { - if (state && savedHash) { + if (state) { state.onSubmit( getValues("storingURL"), savedHash, @@ -74,7 +81,12 @@ export const VoteContextModal = () => { /> )} {step === 1 && ( - + {})} + vote={state?.vote} + /> )} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx index 3f40d7622..e24f4f58b 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -5,16 +5,25 @@ import { Typography } from "@atoms"; import { VoteContextWrapper } from "@organisms"; import { useTranslation, useVoteContextForm } from "@/hooks"; import { ControlledField } from ".."; +import { Vote } from "@/models"; type VoteContextTextProps = { setStep: Dispatch>; onCancel: () => void; + confirmVote: ( + vote?: Vote, + url?: string, + hash?: string | null, + ) => void; + vote?: Vote; }; const MAX_LENGTH = 10000; export const VoteContextText = ({ setStep, onCancel, + confirmVote, + vote, }: VoteContextTextProps) => { const { t } = useTranslation(); @@ -26,10 +35,6 @@ export const VoteContextText = ({ name: "voteContextText", placeholder: t("govActions.provideContext"), rules: { - required: { - value: true, - message: t("createGovernanceAction.fields.validations.required"), - }, maxLength: { value: MAX_LENGTH, message: t("createGovernanceAction.fields.validations.maxLength", { @@ -44,6 +49,10 @@ export const VoteContextText = ({ onContinue={() => setStep(2)} isContinueDisabled={isContinueDisabled} onCancel={onCancel} + onSkip={() => confirmVote(vote)} + continueLabel={ + isContinueDisabled ? t("govActions.skip") : t("govActions.continue") + } > void; isContinueDisabled?: boolean; - onCancel? : () => void; - hideAllBtn? : boolean; - useBackLabel? : boolean; - useSubmitLabel? : boolean; + onCancel?: () => void; + hideAllBtn?: boolean; + useBackLabel?: boolean; + useSubmitLabel?: boolean; + onSkip?: () => void; + continueLabel?: string; }; export const VoteContextWrapper: FC< @@ -22,7 +24,9 @@ export const VoteContextWrapper: FC< children, hideAllBtn = false, useBackLabel = false, - useSubmitLabel = false + useSubmitLabel = false, + onSkip, + continueLabel, }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -59,13 +63,21 @@ export const VoteContextWrapper: FC< } diff --git a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx index 343ff146f..8c2891da5 100644 --- a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx @@ -7,7 +7,7 @@ import { useLocation, useNavigate, useParams } from "react-router-dom"; import { PATHS } from "@consts"; import { useCardano, useSnackbar } from "@context"; import { useWalletErrorModal } from "@hooks"; -import { ProposalVote } from "@/models"; +import { ProposalVote, Vote } from "@/models"; export interface VoteActionFormValues { vote: string; @@ -72,19 +72,23 @@ export const useVoteActionForm = ({ !areFormErrors; const confirmVote = useCallback( - async (values: VoteActionFormValues) => { - if (!canVote) return; + async ( + vote?: Vote, + url?: string, + hash?: string | null, + ) => { + if (!canVote || !vote) return; setIsLoading(true); - const urlSubmitValue = voteContextUrl ?? ""; - const hashSubmitValue = voteContextHash ?? ""; + const urlSubmitValue = url ?? ""; + const hashSubmitValue = hash ?? ""; try { const isPendingTx = isPendingTransaction(); if (isPendingTx) return; const votingBuilder = await buildVote( - values.vote, + vote, txHash, index, urlSubmitValue, @@ -116,7 +120,7 @@ export const useVoteActionForm = ({ ); return { - confirmVote: handleSubmit(confirmVote), + confirmVote, setValue, vote, registerInput, diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 7411317c2..fde58d430 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -468,6 +468,9 @@ "cip129GovernanceActionId": "Governance Action ID:", "governanceActionType": "Governance Action Type:", "goToVote": "Go to Vote", + "submitVote": "Submit Vote", + "skip": "Skip", + "continue": "Continue", "membersToBeRemovedFromTheCommittee": "Members to be removed from the Committee", "membersToBeAddedToTheCommittee": "Members to be added to the Committee", "changeToTermsOfExistingMembers": "Change to terms of existing members", From c6112f28bee33faf138943858d1046a458dc148c Mon Sep 17 00:00:00 2001 From: joseph rana Date: Tue, 29 Jul 2025 19:49:49 +0545 Subject: [PATCH 11/21] feat: add edit vote context --- .../components/molecules/VoteActionForm.tsx | 118 ++++++++++++------ .../VoteContext/VoteContextModal.tsx | 3 + .../organisms/VoteContext/VoteContextText.tsx | 3 + govtool/frontend/src/i18n/locales/en.json | 1 + 4 files changed, 87 insertions(+), 38 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index 669467333..afed1a452 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -3,7 +3,7 @@ import { Box } from "@mui/material"; import { Trans } from "react-i18next"; import { Button, Radio, Typography } from "@atoms"; -import { orange } from "@consts"; +import { fadedPurple } from "@/consts"; import { useModal } from "@context"; import { useScreenDimension, @@ -60,9 +60,11 @@ export const VoteActionForm = ({ setVoteContextUrl(url); setVoteContextHash(hash ?? undefined); confirmVote(vote as Vote, url, hash); + setVoteContextData(url , hash); }, vote: vote as Vote, confirmVote, + previousRationale : voteContextText } satisfies VoteContextModalState, }); }; @@ -126,6 +128,10 @@ export const VoteActionForm = ({ ), [confirmVote, areFormErrors, vote, isVoteLoading], ); + + useEffect(()=>{ + console.log(previousVote?.metadataHash , voteContextHash) + } , [previousVote?.metadataHash , voteContextHash]) return ( )} {voteContextText && ( + <> + {t("govActions.yourVoteRationale")} + {voteContextText && ( + - {voteContextText} - - + {t("showMore")} + + + + )} + + )} + + )} + - - {t("govActions.selectDifferentOption")} - - {previousVote?.vote && previousVote?.vote !== vote ? ( + { + voteContextText && ( + + ) + } + {previousVote?.vote && previousVote?.vote !== vote ? ( void; vote?: Vote; + previousRationale?: string confirmVote: ( vote?: Vote, url?: string, hash?: string | null, ) => void; + // onRationaleChange : () }; export const VoteContextModal = () => { @@ -86,6 +88,7 @@ export const VoteContextModal = () => { onCancel={closeModal} confirmVote={state?.confirmVote ?? (() => {})} vote={state?.vote} + previousRationale={state?.previousRationale} /> )} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx index e24f4f58b..4d852b129 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -16,6 +16,7 @@ type VoteContextTextProps = { hash?: string | null, ) => void; vote?: Vote; + previousRationale? : string }; const MAX_LENGTH = 10000; @@ -24,6 +25,7 @@ export const VoteContextText = ({ onCancel, confirmVote, vote, + previousRationale }: VoteContextTextProps) => { const { t } = useTranslation(); @@ -82,6 +84,7 @@ export const VoteContextText = ({ isModifiedLayout maxLength={MAX_LENGTH} data-testid="provide-context-input" + defaultValue={previousRationale} /> ); diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index fde58d430..85726ec6b 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -454,6 +454,7 @@ "changeYourVote": "Change your vote", "chooseHowToVote": "Choose how you want to vote:", "yourVoteRationale": "Your Vote Rationale", + "editRationale" : "Edit Rationale", "dataMissing": "Data Missing", "dataMissingTooltipExplanation": "Please click “View Details” for more information.", "details": "Governance Details:", From 982cdb23e358d41c751d5d16a21ad9135c05a6e3 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Tue, 29 Jul 2025 21:25:43 +0545 Subject: [PATCH 12/21] Use single button to change vote or rationale --- .../components/molecules/VoteActionForm.tsx | 29 +++++-------------- .../organisms/VoteContext/VoteContextText.tsx | 2 +- .../VoteContext/VoteContextWrapper.tsx | 3 ++ govtool/frontend/src/i18n/locales/en.json | 10 ++++--- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index afed1a452..57811af54 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -331,24 +331,9 @@ export const VoteActionForm = ({ )} - + - { - voteContextText && ( - - ) - } {previousVote?.vote && previousVote?.vote !== vote ? ( - {t("govActions.vote")} + {previousVote?.vote && previousVote?.vote === vote + ? t("govActions.changeRationale") + : t("govActions.vote")} )} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx index 4d852b129..322df0050 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -53,7 +53,7 @@ export const VoteContextText = ({ onCancel={onCancel} onSkip={() => confirmVote(vote)} continueLabel={ - isContinueDisabled ? t("govActions.skip") : t("govActions.continue") + isContinueDisabled ? t("govActions.voting.voteWithoutMetadata") : t("govActions.voting.continue") } > void; continueLabel?: string; + disableNext?: boolean; }; export const VoteContextWrapper: FC< @@ -27,6 +28,7 @@ export const VoteContextWrapper: FC< useSubmitLabel = false, onSkip, continueLabel, + disableNext = false, }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -63,6 +65,7 @@ export const VoteContextWrapper: FC< ) : ( {setStep(2)}} onContinue = {() => {setStep(5)}} useBackLabel + isContinueDisabled={!apiResponse} > {t("createGovernanceAction.rationalePinnedToIPFS")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx index 45bb80ba7..47f3b86e9 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx @@ -49,6 +49,7 @@ export const VoteContextStoringInformation = ({ onContinue={validateURL} isContinueDisabled={isContinueDisabled} onCancel={onCancel} + isVoteWithMetadata useSubmitLabel > diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx index 07423d052..672623a5c 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -26,6 +26,7 @@ export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { onContinue={() => setStep(4)} isContinueDisabled={isContinueDisabled} onCancel={onCancel} + isVoteWithMetadata > {t("createGovernanceAction.storeDataTitle")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx index 8fc2bcae2..28e19cdff 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -65,8 +65,7 @@ export const VoteContextText = ({ onCancel={onCancel} onSkip={() => confirmVote(vote)} continueLabel={buttonLabel} - isChangeVote={previousRationale !== undefined && previousRationale !== null} - isRationaleChanged={isRationaleChanged} + isContinueDisabled={(previousRationale !== undefined && previousRationale !== null ) && !isRationaleChanged} > void; continueLabel?: string; - isChangeVote?: boolean; - isRationaleChanged?:boolean; isApiLoading?:boolean; + isContinueDisabled?:boolean }; export const VoteContextWrapper: FC< @@ -30,8 +29,7 @@ export const VoteContextWrapper: FC< useSubmitLabel = false, onSkip, continueLabel, - isChangeVote = false, - isRationaleChanged=true, + isContinueDisabled }) => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -67,7 +65,7 @@ export const VoteContextWrapper: FC< ), [previousVote?.vote, setValue], ); - + const renderChangeVoteButton = useMemo( () => ( - + > + {t("showMore")} + + + )} - + )} - + )} - - + - {previousVote?.vote && previousVote?.vote !== vote ? ( + {previousVote?.vote && previousVote?.vote !== vote ? ( handleVoteClick(false)} + onClick={() => handleVoteClick(false)} size="extraLarge" > {previousVote?.vote && previousVote?.vote === vote diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx index 3b4119d46..a108b3261 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextChoice.tsx @@ -14,7 +14,6 @@ type VoteContextChoiceProps = { setJsonldContent: Dispatch>; setMetadataHash: Dispatch>; generateMetadata: () => Promise<{ jsonld: NodeObject; jsonHash: string }>; - onCancel: () => void; }; export const VoteContextChoice = ({ @@ -23,7 +22,6 @@ export const VoteContextChoice = ({ setJsonldContent, setMetadataHash, generateMetadata, - onCancel, }: VoteContextChoiceProps) => { const { t } = useTranslation(); const { isMobile } = useScreenDimension(); @@ -44,13 +42,13 @@ export const VoteContextChoice = ({ }; return ( - - - {t("createGovernanceAction.storingOptionsForYourVoterRationale")} - - + + {t("createGovernanceAction.storingOptionsForYourVoterRationale")} + + + {t("createGovernanceAction.learnMoreAboutStoringInformation")} + + + + {t("createGovernanceAction.chooseDataStorageOption")} + + + + - - + {t("createGovernanceAction.govToolPinsDataToIPFS")} + + + ); }; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx index 8050fe30a..152464a09 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextGovTool.tsx @@ -7,11 +7,10 @@ import { postIpfs } from "@services"; import { downloadTextFile, openInNewTab } from "@utils"; import { NodeObject } from "jsonld"; import { UseFormSetValue } from "react-hook-form"; -import { VoteContextFormValues } from "@hooks"; +import { VoteContextFormValues, useTranslation } from "@hooks"; import { LINKS } from "@/consts/links"; import { ICONS } from "@/consts/icons"; import { primaryBlue } from "@/consts"; -import { useTranslation } from "@hooks"; interface PostIpfsResponse { ipfsCid: string; @@ -20,7 +19,6 @@ interface PostIpfsResponse { type VoteContextGovToolProps = { setStep: Dispatch>; setSavedHash: Dispatch>; - submitVoteContext: () => void; jsonldContent: NodeObject | null; metadataHash: string | null; setValue: UseFormSetValue; @@ -37,10 +35,9 @@ export const VoteContextGovTool = ({ const [uploadInitiated, setUploadInitiated] = useState(false); // New state to track upload const { t } = useTranslation(); - const openLink = () => openInNewTab(LINKS.STORING_INFORMATION_OFFLINE); - const { mutate, isLoading , isError } = useMutation({ + const { mutate, isLoading, isError } = useMutation({ mutationFn: postIpfs, onSuccess: (data) => { const ipfsUrl = `ipfs://${data.ipfsCid}`; @@ -65,13 +62,13 @@ export const VoteContextGovTool = ({ return ( {setStep(2)}} - onContinue = {() => {setStep(5)}} + onCancel={() => { setStep(2); }} + onContinue={() => { setStep(5); }} useBackLabel isContinueDisabled={!apiResponse || isError} isVoteWithMetadata > - + {t("createGovernanceAction.rationalePinnedToIPFS")} ) : apiResponse ? ( <> - + {t("createGovernanceAction.optionalDownloadAndStoreMetadataFile")} - + {t("createGovernanceAction.rePinYourFileToIPFS")} - - {apiResponse.ipfsCid ? ( - - IPFS URI: {`https://ipfs.io/ipfs/${apiResponse.ipfsCid}`} - + }, }} + variant="body1" + > + {apiResponse.ipfsCid ? ( + + IPFS URI: {`https://ipfs.io/ipfs/${apiResponse.ipfsCid}`} + ) : ( "[URI]" )} - + - ) : ( + ) : ( {t("createGovernanceAction.uploadingToIPFS")} diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx index a9f77d65a..4e95affa3 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction, useState } from "react"; -import { useForm, FormProvider } from "react-hook-form"; +import { useForm, FormProvider, UseFormReturn } from "react-hook-form"; import { ModalWrapper } from "@atoms"; import { useModal } from "@context"; @@ -13,7 +13,6 @@ import { } from "@organisms"; import { NodeObject } from "jsonld"; import { VoteContextFormValues, useVoteContextForm } from "@hooks"; -import { UseFormReturn } from "react-hook-form"; import { Vote } from "@/models"; export type VoteContextModalState = { @@ -144,7 +143,6 @@ const VoteContextFlow = ({ setJsonldContent={setJsonldContent} setMetadataHash={setMetadataHash} generateMetadata={generateMetadata} - onCancel={onCancel} /> )} {step === 3 && storeDataYourself && ( @@ -157,7 +155,7 @@ const VoteContextFlow = ({ submitVoteContext={submitVoteContext} jsonldContent={jsonldContent} metadataHash={metadataHash} - setValue={methods.setValue} + setValue={methods.setValue} /> )} {step === 4 && ( diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx index 672623a5c..beadd03ca 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -28,7 +28,7 @@ export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { onCancel={onCancel} isVoteWithMetadata > - + {t("createGovernanceAction.storeDataTitle")} { - console.log({"currentRationale":currentRationale,previousRationale:previousRationale}) - return currentRationale !== previousRationale; - }, [currentRationale, previousRationale]); + const isRationaleChanged = useMemo(() => currentRationale !== previousRationale, + [currentRationale, previousRationale]); const buttonLabel = useMemo(() => { if (currentRationale === "") { @@ -57,7 +55,6 @@ export const VoteContextText = ({ }, }, }; - console.log("Previous rationale",previousRationale) return ( setStep(2)} @@ -65,7 +62,8 @@ export const VoteContextText = ({ onCancel={onCancel} onSkip={() => confirmVote(vote)} continueLabel={buttonLabel} - isContinueDisabled={(previousRationale !== undefined && previousRationale !== null ) && !isRationaleChanged} + isContinueDisabled={(previousRationale !== undefined && previousRationale !== null) + && !isRationaleChanged} > void; continueLabel?: string; - isApiLoading?:boolean; isContinueDisabled?:boolean }; @@ -47,42 +46,40 @@ export const VoteContextWrapper: FC< { !hideAllBtn && - - + - + : t("continue"))} + + } ); diff --git a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx index 7df909089..8d25e7804 100644 --- a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx @@ -38,8 +38,6 @@ type Props = { export const useVoteActionForm = ({ previousVote, - voteContextHash, - voteContextUrl, closeModal, }: Props) => { const [isLoading, setIsLoading] = useState(false); @@ -54,7 +52,6 @@ export const useVoteActionForm = ({ const { control, - handleSubmit, formState: { errors, isDirty }, setValue, register: registerInput, @@ -73,52 +70,55 @@ export const useVoteActionForm = ({ index !== null && !areFormErrors; - const confirmVote = useCallback( - async ( - vote?: Vote, - url?: string, - hash?: string | null, - ) => { - if (!canVote || !vote) return; - - setIsLoading(true); - - const urlSubmitValue = url ?? ""; - const hashSubmitValue = hash ?? ""; - - try { - const isPendingTx = isPendingTransaction(); - if (isPendingTx) return; - const votingBuilder = await buildVote( - vote, - txHash, - index, - urlSubmitValue, - hashSubmitValue, - ); - const result = await buildSignSubmitConwayCertTx({ - votingBuilder, - type: "vote", - resourceId: txHash + index, - }); - if (result) { - addSuccessAlert("Vote submitted"); - navigate(PATHS.dashboardGovernanceActions, { - state: { - isVotedListOnLoad: !!previousVote?.vote, - }, - }); - closeModal(); - } - } catch (error) { - openWalletErrorModal({ - error, - dataTestId: "vote-transaction-error-modal", + const confirmVote = useCallback( + async ( + voteValue?: Vote, + url?: string, + hashValue?: string | null, + ) => { + if (!canVote || !voteValue) return; + + setIsLoading(true); + + const urlSubmitValue = url ?? ""; + const hashSubmitValue = hashValue ?? ""; + + try { + const isPendingTx = isPendingTransaction(); + if (isPendingTx) return; + + const votingBuilder = await buildVote( + voteValue, + txHash, + index, + urlSubmitValue, + hashSubmitValue, + ); + + const result = await buildSignSubmitConwayCertTx({ + votingBuilder, + type: "vote", + resourceId: txHash + index, + }); + + if (result) { + addSuccessAlert("Vote submitted"); + navigate(PATHS.dashboardGovernanceActions, { + state: { + isVotedListOnLoad: !!previousVote?.vote, + }, }); - } finally { - setIsLoading(false); + closeModal(); } - }, + } catch (error) { + openWalletErrorModal({ + error, + dataTestId: "vote-transaction-error-modal", + }); + } finally { + setIsLoading(false); + } + }, [buildVote, buildSignSubmitConwayCertTx, txHash, index, canVote], ); diff --git a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx index 687f9a687..e53fb168c 100644 --- a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx @@ -35,7 +35,6 @@ export const useVoteContextForm = ( reset, } = useFormContext(); - const generateMetadata = useCallback(async () => { const { voteContextText } = getValues(); const body = await generateMetadataBody({ diff --git a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts index 3dd7a78f5..440fb3275 100644 --- a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts +++ b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts @@ -5,14 +5,14 @@ import { QUERY_KEYS } from "@/consts"; import { useCardano } from "@/context"; import { useGetVoterInfo } from "."; -export const useGetVoteContextTextFromFile = (url: string | undefined , contextHash : string | undefined) => { +export const useGetVoteContextTextFromFile = (url: string | undefined, + contextHash : string | undefined) => { const { dRepID } = useCardano(); const { voter } = useGetVoterInfo(); - const { data, isLoading } = useQuery( [QUERY_KEYS.useGetVoteContextFromFile, url], - () => getVoteContextTextFromFile(url , contextHash), + () => getVoteContextTextFromFile(url, contextHash), { enabled: !!url && @@ -26,12 +26,11 @@ export const useGetVoteContextTextFromFile = (url: string | undefined , contextH if (data?.valid) { return { voteContextText, - isLoading - } + isLoading + }; } return { - voteContextText : undefined, + voteContextText: undefined, isLoading - } - + }; }; diff --git a/govtool/frontend/src/services/requests/getVoteContextTextFromFile.ts b/govtool/frontend/src/services/requests/getVoteContextTextFromFile.ts index 0e94a3ae0..32778cf8b 100644 --- a/govtool/frontend/src/services/requests/getVoteContextTextFromFile.ts +++ b/govtool/frontend/src/services/requests/getVoteContextTextFromFile.ts @@ -1,15 +1,16 @@ import { postValidate } from "./metadataValidation"; import { MetadataStandard } from "@/models"; -export const getVoteContextTextFromFile = async (url: string | undefined , contextHash : string | undefined) => { +export const getVoteContextTextFromFile = async (url: string | undefined, + contextHash : string | undefined) => { if (!url || !contextHash) { throw new Error("Missing Vote Context values"); } const response = await postValidate({ - "standard" : MetadataStandard.CIP100, - "url" : url, - "hash" : contextHash - }) - - return {valid : response.valid , metadata : response.metadata}; + standard: MetadataStandard.CIP100, + url, + hash: contextHash + }); + + return { valid: response.valid, metadata: response.metadata }; }; diff --git a/govtool/frontend/src/services/requests/postIpfs.ts b/govtool/frontend/src/services/requests/postIpfs.ts index 105065ed2..346dff3c4 100644 --- a/govtool/frontend/src/services/requests/postIpfs.ts +++ b/govtool/frontend/src/services/requests/postIpfs.ts @@ -1,7 +1,7 @@ import { API } from "../API"; export const postIpfs = async ({ content }: { content: string }) => { - const response = await API.post("/ipfs/upload", content,{ + const response = await API.post("/ipfs/upload", content, { headers: { "Content-Type": "text/plain;charset=utf-8" } From 4ad9f76be0bd618704c53c58b18ea35cb08c6e07 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Wed, 30 Jul 2025 11:52:10 +0545 Subject: [PATCH 19/21] fix: tsc lint error on voteContextModal --- .../src/components/organisms/VoteContext/VoteContextModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx index 4e95affa3..8bd2582bf 100644 --- a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -152,7 +152,6 @@ const VoteContextFlow = ({ Date: Wed, 30 Jul 2025 15:43:38 +0545 Subject: [PATCH 20/21] fix: handle empty rationale states for first and revote --- .../src/components/molecules/VoteActionForm.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index e839fffa1..2b3c8c947 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -36,7 +36,12 @@ export const VoteActionForm = ({ useState(false); const { voter } = useGetVoterInfo(); - const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl, voteContextHash); + const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl, voteContextHash) || {}; + + const finalVoteContextText = + ((previousVote != null || undefined) && !voteContextUrl && !voteContextHash) + ? "" + : voteContextText; const { isMobile } = useScreenDimension(); const { openModal, closeModal } = useModal(); @@ -64,7 +69,7 @@ export const VoteActionForm = ({ }, vote: vote as Vote, confirmVote, - previousRationale: isVoteChanged ? undefined : voteContextText + previousRationale: isVoteChanged ? undefined : finalVoteContextText } satisfies VoteContextModalState, }); }; @@ -242,7 +247,7 @@ export const VoteActionForm = ({ {t("govActions.showVotes")} )} - {voteContextText && ( + {finalVoteContextText && ( <> {t("govActions.yourVoteRationale")} - {voteContextText && ( + {finalVoteContextText && ( - {voteContextText} + {finalVoteContextText} {!showWholeVoteContext && ( From ed197ab585ceaeb1005e8a5ab57d698a045a59a2 Mon Sep 17 00:00:00 2001 From: joseph rana Date: Wed, 30 Jul 2025 16:59:13 +0545 Subject: [PATCH 21/21] fix: add error text display for invalid vote context --- .../components/molecules/VoteActionForm.tsx | 11 ++++++-- .../queries/useGetVoteContextTextFromFile.ts | 25 +++++++++++++------ govtool/frontend/src/i18n/locales/en.json | 1 + 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index 2b3c8c947..25c1807d2 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -12,7 +12,7 @@ import { useGetVoteContextTextFromFile, } from "@hooks"; import { formatDisplayDate } from "@utils"; -import { fadedPurple } from "@/consts"; +import { errorRed, fadedPurple } from "@/consts"; import { ProposalData, ProposalVote, Vote } from "@/models"; import { VoteContextModalState, SubmittedVotesModalState } from "../organisms"; @@ -36,7 +36,8 @@ export const VoteActionForm = ({ useState(false); const { voter } = useGetVoterInfo(); - const { voteContextText } = useGetVoteContextTextFromFile(voteContextUrl, voteContextHash) || {}; + const { voteContextText, valid: voteContextValid = true } = + useGetVoteContextTextFromFile(voteContextUrl, voteContextHash) || {}; const finalVoteContextText = ((previousVote != null || undefined) && !voteContextUrl && !voteContextHash) @@ -247,6 +248,12 @@ export const VoteActionForm = ({ {t("govActions.showVotes")} )} + { + !voteContextValid && + + {t("govActions.invalidVoteContext")} + + } {finalVoteContextText && ( <> {t("govActions.yourVoteRationale")} diff --git a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts index 440fb3275..7fc7f8f5c 100644 --- a/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts +++ b/govtool/frontend/src/hooks/queries/useGetVoteContextTextFromFile.ts @@ -23,14 +23,25 @@ export const useGetVoteContextTextFromFile = (url: string | undefined, const voteContextText = (data?.metadata as { comment?: string })?.comment || ""; - if (data?.valid) { + if (url === undefined || contextHash === undefined) { return { - voteContextText, - isLoading - }; - } - return { voteContextText: undefined, - isLoading + isLoading: false, + valid: true }; + } + if (data) { + if (data?.valid) { + return { + voteContextText, + isLoading, + valid: true + }; + } + return { + voteContextText: undefined, + isLoading, + valid: false + }; + } }; diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index 8cd3b74d8..a60dfb632 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -456,6 +456,7 @@ "changeYourVote": "Change your vote", "chooseHowToVote": "Choose how you want to vote:", "yourVoteRationale": "Your Vote Rationale", + "invalidVoteContext": "Invalid Vote Context", "dataMissing": "Data Missing", "dataMissingTooltipExplanation": "Please click “View Details” for more information.", "details": "Governance Details:",