diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index de95e6301..7c4b3f519 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -28,6 +28,7 @@ import qualified Data.Text.Lazy.Encoding as TL import Data.Time (TimeZone, localTimeToUTC) import Data.Time.LocalTime (TimeZone, getCurrentTimeZone) import qualified Data.Vector as V +import Data.Hashable (hash, hashWithSalt) import Numeric.Natural (Natural) @@ -64,6 +65,7 @@ type VVAApi = :> QueryParam "sort" DRepSortMode :> QueryParam "page" Natural :> QueryParam "pageSize" Natural + :> QueryParam "seed" Text :> Get '[JSON] ListDRepsResponse :<|> "drep" :> "get-voting-power" :> Capture "drepId" HexText :> Get '[JSON] Integer :<|> "drep" :> "getVotes" @@ -175,8 +177,8 @@ delegationToResponse Types.Delegation {..} = } -drepList :: App m => Maybe Text -> [DRepStatus] -> Maybe DRepSortMode -> Maybe Natural -> Maybe Natural -> m ListDRepsResponse -drepList mSearchQuery statuses mSortMode mPage mPageSize = do +drepList :: App m => Maybe Text -> [DRepStatus] -> Maybe DRepSortMode -> Maybe Natural -> Maybe Natural -> Maybe Text -> m ListDRepsResponse +drepList mSearchQuery statuses mSortMode mPage mPageSize mSeed = do CacheEnv {dRepListCache} <- asks vvaCache dreps <- cacheRequest dRepListCache (fromMaybe "" mSearchQuery) (DRep.listDReps mSearchQuery) @@ -193,17 +195,22 @@ drepList mSearchQuery statuses mSortMode mPage mPageSize = do Types.DRep -> True - let filterDRepsByStatus = case statuses of [] -> id _ -> filter $ \Types.DRepRegistration {..} -> mapDRepStatus dRepRegistrationStatus `elem` statuses - randomizedOrderList <- mapM (\_ -> randomRIO (0, 1 :: Double)) dreps + let seedInt :: Int + seedInt = + maybe 0 (hash . Text.toCaseFold) mSeed + + randomKey :: Types.DRepRegistration -> Int + randomKey Types.DRepRegistration{..} = + hashWithSalt seedInt dRepRegistrationDRepHash let sortDReps = case mSortMode of Nothing -> id - Just Random -> fmap snd . sortOn fst . Prelude.zip randomizedOrderList + Just Random -> sortOn randomKey Just VotingPower -> sortOn $ \Types.DRepRegistration {..} -> Down dRepRegistrationVotingPower Just Activity -> sortOn $ \Types.DRepRegistration {..} -> diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index 4b6e6afcc..150e5ef57 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -13,7 +13,7 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.9", + "@intersect.mbo/govtool-outcomes-pillar-ui": "1.5.10", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "1.0.15-beta", "@mui/icons-material": "^5.14.3", @@ -3392,14 +3392,14 @@ } }, "node_modules/@intersect.mbo/govtool-outcomes-pillar-ui": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.9.tgz", - "integrity": "sha512-Lq0ILRbUrFSqT8QK6meXud7S45uEwvABmSIIXhm+4fgLm7iPtKpr0ctu9++M7EsYJxPsUFHpa/4sM4zhmVB46w==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.10.tgz", + "integrity": "sha512-Pxz4FjMiJcQnZGZacFqiWaYkBb94Fg8u0vTQwCveus4mWAloMuP0coRFIHgCTR2jKKzBQTcQEsQC8lQLvcheiw==", "license": "ISC", "dependencies": { "@fontsource/poppins": "^5.0.14", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "axios": "^1.10.0", + "axios": "^1.12.2", "bech32": "^2.0.0", "buffer": "^6.0.3", "react-diff-view": "^3.2.1", @@ -9352,9 +9352,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index a23ab3685..23bae4a13 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -27,7 +27,7 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.9", + "@intersect.mbo/govtool-outcomes-pillar-ui": "1.5.10", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", "@intersect.mbo/pdf-ui": "1.0.15-beta", "@mui/icons-material": "^5.14.3", diff --git a/govtool/frontend/src/consts/dRepDirectory/sorting.ts b/govtool/frontend/src/consts/dRepDirectory/sorting.ts index 339f708ae..2fb4af207 100644 --- a/govtool/frontend/src/consts/dRepDirectory/sorting.ts +++ b/govtool/frontend/src/consts/dRepDirectory/sorting.ts @@ -15,4 +15,8 @@ export const DREP_DIRECTORY_SORTING = [ key: "Status", label: "Status", }, + { + key: "Random", + label: "Random", + }, ]; diff --git a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts index db62f9c09..80c91ed5b 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts @@ -24,7 +24,7 @@ type Args = GetDRepListArguments & { }; export function useGetDRepListPaginatedQuery( - { page, pageSize = 10, filters = [], searchPhrase, sorting, status }: Args, + { page, pageSize = 10, filters = [], searchPhrase, sorting, status, sortingSeed }: Args, options?: UseQueryOptions>, ): PaginatedResult { const { pendingTransaction } = useCardano(); @@ -47,6 +47,7 @@ export function useGetDRepListPaginatedQuery( searchPhrase ?? "", sorting ?? "", status?.length ? status : "", + sortingSeed ?? "" ]; const baselineKey = useMemo( @@ -64,6 +65,7 @@ export function useGetDRepListPaginatedQuery( searchPhrase, sorting, status, + sortingSeed, }), { keepPreviousData: true, @@ -87,6 +89,7 @@ export function useGetDRepListPaginatedQuery( searchPhrase: "", sorting, status, + sortingSeed }), { initialData: () => diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index 4f48125a6..3040c77c0 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -148,6 +148,7 @@ export enum DRepListSort { VotingPower = "VotingPower", RegistrationDate = "RegistrationDate", Status = "Status", + Random = "Random", } type Reference = { diff --git a/govtool/frontend/src/pages/DRepDirectoryContent.tsx b/govtool/frontend/src/pages/DRepDirectoryContent.tsx index e7559d247..cd67675c3 100644 --- a/govtool/frontend/src/pages/DRepDirectoryContent.tsx +++ b/govtool/frontend/src/pages/DRepDirectoryContent.tsx @@ -61,6 +61,27 @@ export const DRepDirectoryContent: FC = ({ ...dataActionsBarProps } = useDataActionsBar(); + const SEED_STORAGE_KEY = "drep_directory_sorting_seed"; + + const makeSeed = () => + (globalThis.crypto?.randomUUID?.() as string | undefined) ?? + Math.random().toString(36).slice(2); + + const getStoredSeed = () => { + if (typeof window === "undefined") return ""; + return sessionStorage.getItem(SEED_STORAGE_KEY) || ""; + }; + + const [sortingSeed, setSortingSeed] = useState(() => getStoredSeed()); + + useEffect(() => { + if (lastPath && !lastPath.includes("drep_directory")) { + const newSeed = makeSeed(); + setSortingSeed(newSeed); + sessionStorage.setItem(SEED_STORAGE_KEY, newSeed); + } + }, [sortingSeed]); + const { page, pageSize, setPage, setPageSize } = usePagination(); const { chosenFilters, chosenSorting, setChosenFilters, setChosenSorting } = @@ -108,6 +129,7 @@ export const DRepDirectoryContent: FC = ({ searchPhrase: debouncedSearchText, sorting: chosenSorting as DRepListSort, status: chosenFilters as DRepStatus[], + sortingSeed }, { enabled: !!chosenSorting }, ); diff --git a/govtool/frontend/src/services/requests/getDRepList.ts b/govtool/frontend/src/services/requests/getDRepList.ts index 12270b7ea..5d9ba4bd7 100644 --- a/govtool/frontend/src/services/requests/getDRepList.ts +++ b/govtool/frontend/src/services/requests/getDRepList.ts @@ -14,6 +14,7 @@ export type GetDRepListArguments = { sorting?: DRepListSort; status?: DRepStatus[]; searchPhrase?: string; + sortingSeed?: string; }; export const getDRepList = async ({ @@ -23,6 +24,7 @@ export const getDRepList = async ({ pageSize = 10, searchPhrase: rawSearchPhrase = "", status = [], + sortingSeed = "" }: GetDRepListArguments): Promise> => { const searchPhrase = await dRepSearchPhraseProcessor(rawSearchPhrase); @@ -34,6 +36,7 @@ export const getDRepList = async ({ ...(filters.length && { type: filters }), ...(sorting && { sort: sorting }), ...(status.length && { status }), + ...(sortingSeed && { seed: sortingSeed }), }, });