From 236fd9a250ad0cc61d0132b58a52db98168a5ced Mon Sep 17 00:00:00 2001 From: Leszek Wiesner Date: Wed, 2 Apr 2025 11:59:24 +0200 Subject: [PATCH 01/67] =?UTF-8?q?=F0=9F=9A=80=20Release=20`3.7.3`=20(#4868?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace Subscan with Statescan (#4867) * Replace Subscan with Statescan * Fix formatting * Fix failing playwright install in CI checks * Bump version to 3.7.3 --------- Co-authored-by: Theophile Sandoz --- .github/workflows/CI.yml | 3 +- CHANGELOG.md | 10 ++++- packages/ui/package.json | 2 +- packages/ui/src/app/components/SideBar.tsx | 4 +- .../common/components/BlockTime/BlockInfo.tsx | 2 +- .../icons/symbols/ExplorerSymbol.tsx | 27 ++++++++++++ .../icons/symbols/SubscanSymbol.tsx | 23 ---------- .../Sidebar/LinksIcons/ValidatorsIcon.tsx | 44 +++++++++---------- 8 files changed, 63 insertions(+), 52 deletions(-) create mode 100644 packages/ui/src/common/components/icons/symbols/ExplorerSymbol.tsx delete mode 100644 packages/ui/src/common/components/icons/symbols/SubscanSymbol.tsx diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 30b80ab257..2c1c8622ab 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -75,7 +75,8 @@ jobs: needs: install timeout-minutes: 60 strategy: - matrix: { node: ["18.x"], os: [ubuntu-latest] } + # Temporary fix for: https://github.com/microsoft/playwright/issues/30368 + matrix: { node: ["18.x"], os: [ubuntu-22.04] } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index bb9767530d..ba15f3e4da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.7.3] - 2025-04-01 + +### Fixed +- Replace joystream.subscan.io links with explorer.joystream.org. + ## [3.7.2] - 2024-08-10 ### Fixed @@ -412,8 +417,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.1] - 2022-12-02 -[unreleased]: https://github.com/Joystream/pioneer/compare/v3.7.2...HEAD -[3.8.0]: https://github.com/Joystream/pioneer/compare/v3.7.1...v3.7.2 +[unreleased]: https://github.com/Joystream/pioneer/compare/v3.7.3...HEAD +[3.7.3]: https://github.com/Joystream/pioneer/compare/v3.7.2...v3.7.3 +[3.7.2]: https://github.com/Joystream/pioneer/compare/v3.7.1...v3.7.2 [3.7.1]: https://github.com/Joystream/pioneer/compare/v3.7.0...v3.7.1 [3.7.0]: https://github.com/Joystream/pioneer/compare/v3.6.0...v3.7.0 [3.6.0]: https://github.com/Joystream/pioneer/compare/v3.5.2...v3.6.0 diff --git a/packages/ui/package.json b/packages/ui/package.json index d48c3b9986..6927e881d4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@joystream/pioneer", - "version": "3.7.2", + "version": "3.7.3", "license": "GPL-3.0-only", "scripts": { "build": "node --max_old_space_size=4096 ./build.js", diff --git a/packages/ui/src/app/components/SideBar.tsx b/packages/ui/src/app/components/SideBar.tsx index 49ec938860..13cb2d7168 100644 --- a/packages/ui/src/app/components/SideBar.tsx +++ b/packages/ui/src/app/components/SideBar.tsx @@ -9,7 +9,7 @@ import { MembersRoutes, ProfileRoutes } from '@/app/constants/routes' import { BountyRoutes } from '@/bounty/constants' import { Arrow } from '@/common/components/icons' import { LinkSymbol, LinkSymbolStyle, PolkadotSymbol } from '@/common/components/icons/symbols' -import { SubscanSymbol } from '@/common/components/icons/symbols/SubscanSymbol' +import { ExplorerSymbol } from '@/common/components/icons/symbols/ExplorerSymbol' import { AppsIcon } from '@/common/components/page/Sidebar/LinksIcons/AppsIcon' import { BandwidthIcon } from '@/common/components/page/Sidebar/LinksIcons/BandwidthIcon' import { BountyIcon } from '@/common/components/page/Sidebar/LinksIcons/BountyIcon' @@ -127,7 +127,7 @@ export const SideBarContent = () => { - } to="https://joystream.subscan.io"> + } to="https://explorer.joystream.org"> Explorer diff --git a/packages/ui/src/common/components/BlockTime/BlockInfo.tsx b/packages/ui/src/common/components/BlockTime/BlockInfo.tsx index 802f699475..314a30805a 100644 --- a/packages/ui/src/common/components/BlockTime/BlockInfo.tsx +++ b/packages/ui/src/common/components/BlockTime/BlockInfo.tsx @@ -19,7 +19,7 @@ export const BlockInfo = ({ block, lessInfo, inline }: BlockInfoProp) => { const [endpoints] = useNetworkEndpoints() const blockLink = endpoints.nodeRpcEndpoint == process.env.REACT_APP_MAINNET_NODE_SOCKET - ? 'joystream.subscan.io/block' + ? 'explorer.joystream.org/#/blocks' : `polkadot.js.org/apps/?rpc=${endpoints.nodeRpcEndpoint}/ws-rpc#/explorer/query` return ( evt.stopPropagation()} href={`https://${blockLink}/${block.number}`}> diff --git a/packages/ui/src/common/components/icons/symbols/ExplorerSymbol.tsx b/packages/ui/src/common/components/icons/symbols/ExplorerSymbol.tsx new file mode 100644 index 0000000000..1bdd7eabe1 --- /dev/null +++ b/packages/ui/src/common/components/icons/symbols/ExplorerSymbol.tsx @@ -0,0 +1,27 @@ +import React from 'react' + +export function ExplorerSymbol() { + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/packages/ui/src/common/components/icons/symbols/SubscanSymbol.tsx b/packages/ui/src/common/components/icons/symbols/SubscanSymbol.tsx deleted file mode 100644 index 04248343b6..0000000000 --- a/packages/ui/src/common/components/icons/symbols/SubscanSymbol.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' - -export function SubscanSymbol() { - return ( - - - - - - - - - - - - - - - ) -} diff --git a/packages/ui/src/common/components/page/Sidebar/LinksIcons/ValidatorsIcon.tsx b/packages/ui/src/common/components/page/Sidebar/LinksIcons/ValidatorsIcon.tsx index 2a04bde7c8..3e1e1f31d2 100644 --- a/packages/ui/src/common/components/page/Sidebar/LinksIcons/ValidatorsIcon.tsx +++ b/packages/ui/src/common/components/page/Sidebar/LinksIcons/ValidatorsIcon.tsx @@ -1,27 +1,27 @@ import React from 'react' -import { Icon } from '../../../icons' - export const ValidatorsIcon = () => ( - - + + - + d="M26.791 19.3753L21.541 24.6253L18.916 22.0003M21.5621 12.5593L14.6739 16.4949C14.0506 16.8509 13.666 17.5136 13.666 18.2314V25.7585C13.666 26.482 14.0567 27.149 14.6878 27.5029L21.7388 31.4569C22.3414 31.7948 23.0759 31.7978 23.6812 31.4647L30.6302 27.6407C31.2691 27.2891 31.666 26.6177 31.666 25.8884V18.2474C31.666 17.5211 31.2723 16.8519 30.6374 16.4991L23.5256 12.5476C22.9141 12.2079 22.1695 12.2123 21.5621 12.5593Z" + stroke="#1B202C" + stroke-width="1.5" + stroke-linecap="round" + stroke-linejoin="round" + > + + + + + + + ) From 72cde78f7e0204a8da6c6f587ee6c07edcca4338 Mon Sep 17 00:00:00 2001 From: Leszek Wiesner Date: Tue, 15 Apr 2025 11:20:02 +0200 Subject: [PATCH 02/67] =?UTF-8?q?=F0=9F=9A=80=20Release=20`3.8.0`=20(#4870?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace Subscan with Statescan (#4867) * Replace Subscan with Statescan * Fix formatting * Fix failing playwright install in CI checks * Council and election pages: Small enhancements - Issue 4866 (#4869) * react snap setup * react snap setup * react snap setup * seo * council and past council page * Add term start and end (estimated) dates for current term * total number of votes & total voting stake during Voting and Revealing period * Add Election Result and Revealed votes statistics * added past councils route and candidate vote highlight * latest election fields update * undo react-snap * removed uneccesary package update * removed uneccesary package update * chore: remove yarn.lock from tracked files * git error fix * chore: revert yarn.lock to match main * removed eslint comment Co-authored-by: Leszek Wiesner * Apply suggestions from code review Co-authored-by: Leszek Wiesner * Update packages/ui/src/index.html Co-authored-by: Leszek Wiesner * Update packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilListItem.tsx Co-authored-by: Leszek Wiesner * Update packages/ui/src/council/components/election/pastElection/PastElectionStats.tsx Co-authored-by: Leszek Wiesner * estimated ends at * council id * rearranged mock * review changes * review changes * test error fix --------- Co-authored-by: Leszek Wiesner * Bump version, update CHANGELOG --------- Co-authored-by: Theophile Sandoz Co-authored-by: Victor Emmanuel <33874323+vrrayz@users.noreply.github.com> --- CHANGELOG.md | 16 +- packages/ui/package.json | 2 +- packages/ui/src/app/pages/Council/Council.tsx | 66 ++- .../Council/PastCouncils/PastCouncil.tsx | 2 +- .../app/pages/Election/Election.stories.tsx | 7 +- .../ui/src/app/pages/Election/Election.tsx | 31 +- .../PastElections/PastElection.stories.tsx | 555 ++++++++++++++++++ .../Election/PastElections/PastElection.tsx | 33 +- .../PastElections/PastElections.stories.tsx | 227 +++++++ .../Election/PastElections/PastElections.tsx | 3 +- .../__generated__/baseTypes.generated.ts | 11 + .../components/Activities/ActivityIcon.tsx | 2 +- .../common/components/BlockTime/BlockTime.tsx | 2 +- .../components/statistics/StatisticBar.tsx | 16 +- .../election/CandidateVote/CandidateVote.tsx | 1 - .../CandidateVote/CandidateVoteList.tsx | 54 +- .../pastElection/PastElectionStats.tsx | 26 +- .../pastElection/PastElectionTabs.tsx | 1 + .../PastElectionsListRow.tsx | 15 + .../PastCouncilsList/PastCouncilListItem.tsx | 2 +- .../PastCouncilsList.stories.tsx | 6 +- .../__generated__/council.generated.tsx | 36 ++ .../ui/src/council/queries/council.graphql | 12 + packages/ui/src/council/types/Election.ts | 7 + .../ui/src/council/types/LatestElection.ts | 4 + packages/ui/src/council/types/PastCouncil.ts | 2 + packages/ui/src/council/types/PastElection.ts | 27 +- .../__generated__/overview.generated.tsx | 5 + .../ui/src/overview/queries/overview.graphql | 4 + 29 files changed, 1132 insertions(+), 43 deletions(-) create mode 100644 packages/ui/src/app/pages/Election/PastElections/PastElection.stories.tsx create mode 100644 packages/ui/src/app/pages/Election/PastElections/PastElections.stories.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index ba15f3e4da..0ee61efdc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.8.0] - 2025-04-15 + +### Added +- "Started at" and "Estimated end" dates & blocks on council page, +- Current term number on council page, +- Voting statistics on current election page (votes, revealed votes, stake, revealed stake), +- Election result on past election page, +- Election result on pase elections listing, +- Stake vs revealed stake stats on past election page. + +### Fixed +- Made council term number on past council page and past council listing human-readable. + ## [3.7.3] - 2025-04-01 ### Fixed @@ -417,7 +430,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.1] - 2022-12-02 -[unreleased]: https://github.com/Joystream/pioneer/compare/v3.7.3...HEAD +[unreleased]: https://github.com/Joystream/pioneer/compare/v3.8.0...HEAD +[3.8.0]: https://github.com/Joystream/pioneer/compare/v3.7.3...v3.8.0 [3.7.3]: https://github.com/Joystream/pioneer/compare/v3.7.2...v3.7.3 [3.7.2]: https://github.com/Joystream/pioneer/compare/v3.7.1...v3.7.2 [3.7.1]: https://github.com/Joystream/pioneer/compare/v3.7.0...v3.7.1 diff --git a/packages/ui/package.json b/packages/ui/package.json index 6927e881d4..3403162c43 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@joystream/pioneer", - "version": "3.7.3", + "version": "3.8.0", "license": "GPL-3.0-only", "scripts": { "build": "node --max_old_space_size=4096 ./build.js", diff --git a/packages/ui/src/app/pages/Council/Council.tsx b/packages/ui/src/app/pages/Council/Council.tsx index 16f697f82f..e7a84053f4 100644 --- a/packages/ui/src/app/pages/Council/Council.tsx +++ b/packages/ui/src/app/pages/Council/Council.tsx @@ -4,17 +4,27 @@ import styled from 'styled-components' import { PageHeaderWithHint } from '@/app/components/PageHeaderWithHint' import { PageLayout } from '@/app/components/PageLayout' import { ActivitiesBlock } from '@/common/components/Activities/ActivitiesBlock' +import { AboutText, BlockTimeWrapper } from '@/common/components/BlockTime' +import { BlockInfo } from '@/common/components/BlockTime/BlockInfo' import { MainPanel } from '@/common/components/page/PageContent' import { SidePanel } from '@/common/components/page/SidePanel' -import { BlockDurationStatistics, MultiValueStat, Statistics } from '@/common/components/statistics' +import { + BlockDurationStatistics, + MultiValueStat, + StatisticItem, + StatisticItemSpacedContent, + StatisticLabel, + Statistics, +} from '@/common/components/statistics' import { NotFoundText } from '@/common/components/typography/NotFoundText' import { useRefetchQueries } from '@/common/hooks/useRefetchQueries' -import { MILLISECONDS_PER_BLOCK } from '@/common/model/formatters' +import { formatDateString, MILLISECONDS_PER_BLOCK } from '@/common/model/formatters' import { asBN } from '@/common/utils/bn' import { CouncilList, CouncilOrder } from '@/council/components/councilList' import { ViewElectionButton } from '@/council/components/ViewElectionButton' import { useCouncilActivities } from '@/council/hooks/useCouncilActivities' import { useCouncilorWithDetails } from '@/council/hooks/useCouncilorWithDetails' +import { useCouncilPeriodInformation } from '@/council/hooks/useCouncilPeriodInformation' import { useCouncilStatistics } from '@/council/hooks/useCouncilStatistics' import { useElectedCouncil } from '@/council/hooks/useElectedCouncil' import { useElectionStage } from '@/council/hooks/useElectionStage' @@ -32,16 +42,39 @@ export const Council = () => { const { council, isLoading } = useElectedCouncil() const { idlePeriodRemaining, budget, reward } = useCouncilStatistics() + const periodInformation = useCouncilPeriodInformation() + + const endsAt = useMemo( + () => + periodInformation && council + ? { + number: periodInformation.periodEnds[3], + timestamp: ( + new Date().getTime() + + (periodInformation.periodEnds[3] - periodInformation.currentBlock) * MILLISECONDS_PER_BLOCK + ).toString(), + } + : { number: 0, timestamp: new Date().getTime().toString() }, + [periodInformation, council] + ) + const { activities } = useCouncilActivities() const [order, setOrder] = useState({ key: 'member' }) const { councilors, isLoading: isLoadingCouncilors } = useCouncilorWithDetails(council) const sortedCouncilors = useMemo(() => councilors.sort(sortBy(order)), [councilors]) - const header = } /> + const header = ( + } + /> + ) const isCouncilorLoading = !isRefetched && (isLoading || isLoadingCouncilors) const rewardPerDay = useMemo(() => reward?.period?.mul(reward?.singleCouncilorAmount ?? asBN(0)) ?? asBN(0), [reward]) + const main = ( @@ -65,6 +98,28 @@ export const Council = () => { { label: 'Per Week', value: rewardPerDay.mul(asBN(7)) }, ]} /> + + + + Started At + + {council && ( + + {formatDateString(council.electedAt.timestamp)} + + + )} + + + Estimated end + {council && ( + + {formatDateString(Number(endsAt.timestamp))} + + + )} + + {!isCouncilorLoading && sortedCouncilors.length === 0 ? ( @@ -101,3 +156,8 @@ const StatisticsStyle = styled(Statistics)` grid-template-columns: 1fr 1fr; } ` +const CustomBlockTimeWrapper = styled(BlockTimeWrapper)` + grid-row-gap: 2px; + min-width: 165px; + justify-content: flex-start; +` diff --git a/packages/ui/src/app/pages/Council/PastCouncils/PastCouncil.tsx b/packages/ui/src/app/pages/Council/PastCouncils/PastCouncil.tsx index 05f28d956d..aecafb48c3 100644 --- a/packages/ui/src/app/pages/Council/PastCouncils/PastCouncil.tsx +++ b/packages/ui/src/app/pages/Council/PastCouncils/PastCouncil.tsx @@ -37,7 +37,7 @@ export const PastCouncil = () => { - Council #{council.id} + Council #{parseInt(council.id, 36)} { tooltipText="Elections occur periodically. Each has a sequence of stages referred to as the election cycle. Stages are: announcing period, voting period and revealing period." /> + {(electionStage == 'revealing' || electionStage == 'voting') && ( + + + + {electionStage == 'revealing' && ( + <> + {election?.revealedVotes} /{' '} + + )} + {election?.votesNumber} + + + + + {electionStage == 'revealing' && ( + <> + /{' '} + + )} + + + + + )} + {electionStage === 'inactive' && ( )} @@ -116,3 +142,6 @@ const StyledStatistics = styled(Statistics)<{ size: string; stage: ElectionStage grid-template-columns: ${({ size, stage }) => stage === 'inactive' || size === 'xxs' || size === 'xs' ? '1fr' : '200px 1fr'}; ` +const ValueDivider = styled(TextInlineSmall)` + color: ${Colors.Black[500]}; +` diff --git a/packages/ui/src/app/pages/Election/PastElections/PastElection.stories.tsx b/packages/ui/src/app/pages/Election/PastElections/PastElection.stories.tsx new file mode 100644 index 0000000000..1242427436 --- /dev/null +++ b/packages/ui/src/app/pages/Election/PastElections/PastElection.stories.tsx @@ -0,0 +1,555 @@ +import { Meta, StoryContext, StoryObj } from '@storybook/react' +import { FC } from 'react' + +import { GetPastElectionDocument } from '@/council/queries' +import { MocksParameters } from '@/mocks/providers' + +import { PastElection } from './PastElection' + +type Story = StoryObj + +type Args = { + result: 'successful' | 'failed' +} + +const ElectionRoundId = '00000019' + +export default { + title: 'Pages/Election/PastElections/PastElection', + argTypes: { + result: { + control: { type: 'radio' }, + options: ['successful', 'failed'], + }, + }, + args: { + result: 'successful', + }, + component: PastElection, + parameters: { + router: { path: '/:id', href: `/${ElectionRoundId}` }, + + mocks: ({ args }: StoryContext): MocksParameters => { + const nextElectedCouncil = + args.result == 'successful' + ? { + councilElections: [ + { + cycleId: 44, + }, + ], + } + : null + return { + gql: { + queries: [ + { + query: GetPastElectionDocument, + data: { + electionRoundByUniqueInput: { + id: '00000018', + cycleId: 44, + endedAtBlock: 11851230, + endedAtTime: '2025-03-14T04:00:48.000Z', + endedAtNetwork: 'OLYMPIA', + candidates: [ + { + id: '0000006z', + member: { + id: '2154', + rootAccount: 'j4WimuwF1N3nqff211ifeGSW8q5DW8xWpsjaNf9Feds1MDo5B', + controllerAccount: 'j4WimuwF1N3nqff211ifeGSW8q5DW8xWpsjaNf9Feds1MDo5B', + boundAccounts: [ + 'j4VSHQU7vhHUM8XB3bCBfFGpgb46hJFCgv9DnQSYFhygGLXjx', + 'j4WimuwF1N3nqff211ifeGSW8q5DW8xWpsjaNf9Feds1MDo5B', + 'j4RgyJZGRFbtXiY5AZFoTnoYjpTFHx4ar2N5PsPqKg9ryaHfn', + 'j4TeRe6xT2s6QPgMWmADNnqXSjz4tsNC1XEgc8KWnygoLFtZo', + 'j4VxCgPkSDZrynLGJj5XszwuzVTrNXG1WsLg9cZPqYbgyfpML', + 'j4Ssvsr49QwDvm875HGVestJUPg58LJDS8TaiuRMdSwmPNoWa', + ], + handle: 'marat_mu', + metadata: { + name: null, + about: + 'Deep in crypto. \nMy contacts:\nDiscord - MarikJudo\nTelegram - @МarikJudo\nKeybase - marikjudo', + avatar: { + __typename: 'AvatarUri', + avatarUri: 'https://atlas-services.joystream.org/avatars/migrated/2154.webp', + }, + isVerifiedValidator: false, + }, + isVerified: true, + isFoundingMember: true, + isCouncilMember: false, + inviteCount: 2, + roles: [ + { + id: 'operationsWorkingGroupGamma-28', + group: { + name: 'operationsWorkingGroupGamma', + }, + createdAt: '2024-02-16T06:50:30.000Z', + isLead: false, + isActive: true, + }, + ], + createdAt: '2024-12-20T02:23:00.000Z', + stakingaccountaddedeventmember: [ + { + createdAt: '2023-11-23T14:25:00.001Z', + inBlock: 5011185, + network: 'OLYMPIA', + account: 'j4VSHQU7vhHUM8XB3bCBfFGpgb46hJFCgv9DnQSYFhygGLXjx', + }, + { + createdAt: '2024-01-07T17:55:18.001Z', + inBlock: 5658642, + network: 'OLYMPIA', + account: 'j4WimuwF1N3nqff211ifeGSW8q5DW8xWpsjaNf9Feds1MDo5B', + }, + { + createdAt: '2024-01-15T14:37:24.000Z', + inBlock: 5771640, + network: 'OLYMPIA', + account: 'j4RgyJZGRFbtXiY5AZFoTnoYjpTFHx4ar2N5PsPqKg9ryaHfn', + }, + { + createdAt: '2024-02-11T17:43:42.001Z', + inBlock: 6161782, + network: 'OLYMPIA', + account: 'j4TeRe6xT2s6QPgMWmADNnqXSjz4tsNC1XEgc8KWnygoLFtZo', + }, + { + createdAt: '2024-02-15T17:44:18.000Z', + inBlock: 6219315, + network: 'OLYMPIA', + account: 'j4VxCgPkSDZrynLGJj5XszwuzVTrNXG1WsLg9cZPqYbgyfpML', + }, + { + createdAt: '2024-05-23T09:48:42.001Z', + inBlock: 7617210, + network: 'OLYMPIA', + account: 'j4Ssvsr49QwDvm875HGVestJUPg58LJDS8TaiuRMdSwmPNoWa', + }, + ], + }, + stake: '1666666666660000', + noteMetadata: { + header: 'MarikJudo for Council', + bulletPoints: ['Marketing', 'Roadmap'], + bannerImageUri: 'https://i.postimg.cc/Pr3mDmgp/21.png', + description: + 'Hey, everybody! \nI am running for this election because I want to strengthen my work in areas such as Marketing and Roadmap. \nAs you know, at the moment we are in the stage of suspension of our ambassador program, but we still have a number of active creators who are ready to continue cooperation. I am convinced that as a project focused on video content creation, we will sooner or later come to the conclusion that interaction with content creators is the key to success. Therefore, I want to discuss and realize this issue in advance and within the scope of the DAO marketing campaign. \nAlso, as you may have seen, we recently published our roadmap. https://t.co/dS1kVBr85n \nIn my opinion, it is very cool and so its strict implementation is my priority!', + }, + status: 'FAILED', + stakingAccountId: 'j4WimuwF1N3nqff211ifeGSW8q5DW8xWpsjaNf9Feds1MDo5B', + votePower: '0', + votesReceived: [], + }, + { + id: '00000071', + member: { + id: '515', + rootAccount: 'j4SWVha668Nv1YroUkQB68yzfSAjp5xJg4dsXCcKjXJSLwwoa', + controllerAccount: 'j4SWVha668Nv1YroUkQB68yzfSAjp5xJg4dsXCcKjXJSLwwoa', + boundAccounts: [ + 'j4ReW51wy7KnuUyZEQiJsJdZR3iQFmf3tZ8uaVWW5y1uKHdnF', + 'j4RP5odJ8pDGFdaCK3a8kEefHwTXvtzz8C3R1G6o9tF5ALwRz', + 'j4Wg7JSHHLQpxFQfhEB9T57bbeSjFNKpFcoppifCNj3VxWj4C', + 'j4WkPQ89RT5geSQ3KgL2zsD1dVnD6cgfzKtJWhQQNyrVyfJcb', + 'j4S8usvaKAZZdYpj9Hp4wsTYbRSh9hg4NwezX5XjXUFWyTvr1', + 'j4Ue4cDRUmbQ2qXoVfS9p3D7puDQVmZ5dewNAJQeL7a4jif48', + 'j4VgJtzZKvFbamrzhNATcK69HBrouazF7kx9bnWqQt3zNGKPQ', + 'j4UQcm2fysev3AQitU11f2byVbxjJw7u3Jz8ftPydHixX8eQ5', + 'j4USoHhqNrKHEhJCrXY5Lj4g2X9iXRjQ9ZkxijxiFgJB43xPR', + 'j4Wu1J7oshW9EhFnhriNMJKRXEBaVaKUmr69pdWKZ8ZK644ub', + 'j4S3furSLyPz5uMZ6FRGmY4Ks53Lj4TCCio1BSoM7EXPGqW6e', + 'j4SX2fZsSsaatUwPcCWDUGVV5HAd3HoYDWherGrFz5Dg6wJQQ', + 'j4VSMCK6HsUT2Xiufd7ZtWeFfVAooa7B9SHneR8wBKngEcpVz', + 'j4VJYdeHRx4BzoLeVjHXquLGcEXjkJb13uu4Wq7awyKB1sJ4E', + 'j4U8JopV4oAraxF2G6pGp6bk6JKpBkKkPJf7ELeSJu84P9pEa', + 'j4S3pRewxQgjUj3EuwZ1SYwaEqDqbzBwkP3bLGs5CwJ1D99Q6', + ], + handle: 'l1dev', + metadata: { + name: null, + about: + 'FM report: https://github.com/Joystream/community-repo/tree/master/contributions/fm-reports/l1dev', + avatar: { + __typename: 'AvatarUri', + avatarUri: 'https://atlas-services.joystream.org/avatars/migrated/515.webp', + }, + isVerifiedValidator: false, + }, + isVerified: true, + isFoundingMember: true, + isCouncilMember: false, + inviteCount: 2, + roles: [ + { + id: 'operationsWorkingGroupAlpha-2', + group: { + name: 'operationsWorkingGroupAlpha', + }, + createdAt: '2023-01-06T12:15:24.000Z', + isLead: false, + isActive: false, + }, + { + id: 'distributionWorkingGroup-0', + group: { + name: 'distributionWorkingGroup', + }, + createdAt: '2023-01-04T13:03:30.000Z', + isLead: false, + isActive: false, + }, + { + id: 'appWorkingGroup-3', + group: { + name: 'appWorkingGroup', + }, + createdAt: '2023-03-30T16:23:30.001Z', + isLead: false, + isActive: false, + }, + { + id: 'appWorkingGroup-5', + group: { + name: 'appWorkingGroup', + }, + createdAt: '2023-05-30T00:30:48.000Z', + isLead: false, + isActive: false, + }, + ], + createdAt: '2024-11-21T23:37:48.000Z', + stakingaccountaddedeventmember: [ + { + createdAt: '2022-12-19T16:47:48.000Z', + inBlock: 141522, + network: 'OLYMPIA', + account: 'j4ReW51wy7KnuUyZEQiJsJdZR3iQFmf3tZ8uaVWW5y1uKHdnF', + }, + { + createdAt: '2023-01-02T22:27:54.000Z', + inBlock: 346424, + network: 'OLYMPIA', + account: 'j4RP5odJ8pDGFdaCK3a8kEefHwTXvtzz8C3R1G6o9tF5ALwRz', + }, + { + createdAt: '2023-01-03T16:56:06.000Z', + inBlock: 357132, + network: 'OLYMPIA', + account: 'j4Wg7JSHHLQpxFQfhEB9T57bbeSjFNKpFcoppifCNj3VxWj4C', + }, + { + createdAt: '2023-01-04T13:36:06.001Z', + inBlock: 369532, + network: 'OLYMPIA', + account: 'j4WkPQ89RT5geSQ3KgL2zsD1dVnD6cgfzKtJWhQQNyrVyfJcb', + }, + { + createdAt: '2023-01-04T20:32:24.000Z', + inBlock: 373695, + network: 'OLYMPIA', + account: 'j4S8usvaKAZZdYpj9Hp4wsTYbRSh9hg4NwezX5XjXUFWyTvr1', + }, + { + createdAt: '2023-01-05T22:43:54.000Z', + inBlock: 389410, + network: 'OLYMPIA', + account: 'j4Ue4cDRUmbQ2qXoVfS9p3D7puDQVmZ5dewNAJQeL7a4jif48', + }, + { + createdAt: '2023-01-19T18:35:00.001Z', + inBlock: 588293, + network: 'OLYMPIA', + account: 'j4VgJtzZKvFbamrzhNATcK69HBrouazF7kx9bnWqQt3zNGKPQ', + }, + { + createdAt: '2023-01-19T18:47:24.000Z', + inBlock: 588417, + network: 'OLYMPIA', + account: 'j4UQcm2fysev3AQitU11f2byVbxjJw7u3Jz8ftPydHixX8eQ5', + }, + { + createdAt: '2023-01-23T09:45:48.001Z', + inBlock: 640426, + network: 'OLYMPIA', + account: 'j4USoHhqNrKHEhJCrXY5Lj4g2X9iXRjQ9ZkxijxiFgJB43xPR', + }, + { + createdAt: '2023-01-23T09:49:18.001Z', + inBlock: 640461, + network: 'OLYMPIA', + account: 'j4Wu1J7oshW9EhFnhriNMJKRXEBaVaKUmr69pdWKZ8ZK644ub', + }, + { + createdAt: '2023-03-21T19:15:48.000Z', + inBlock: 1466233, + network: 'OLYMPIA', + account: 'j4S3furSLyPz5uMZ6FRGmY4Ks53Lj4TCCio1BSoM7EXPGqW6e', + }, + { + createdAt: '2023-03-29T23:04:54.001Z', + inBlock: 1583547, + network: 'OLYMPIA', + account: 'j4SX2fZsSsaatUwPcCWDUGVV5HAd3HoYDWherGrFz5Dg6wJQQ', + }, + { + createdAt: '2023-05-01T14:06:18.000Z', + inBlock: 2052380, + network: 'OLYMPIA', + account: 'j4VSMCK6HsUT2Xiufd7ZtWeFfVAooa7B9SHneR8wBKngEcpVz', + }, + { + createdAt: '2023-05-19T11:03:00.000Z', + inBlock: 2308757, + network: 'OLYMPIA', + account: 'j4VJYdeHRx4BzoLeVjHXquLGcEXjkJb13uu4Wq7awyKB1sJ4E', + }, + { + createdAt: '2023-06-03T22:21:12.000Z', + inBlock: 2530478, + network: 'OLYMPIA', + account: 'j4U8JopV4oAraxF2G6pGp6bk6JKpBkKkPJf7ELeSJu84P9pEa', + }, + { + createdAt: '2023-06-10T02:19:24.000Z', + inBlock: 2619240, + network: 'OLYMPIA', + account: 'j4S3pRewxQgjUj3EuwZ1SYwaEqDqbzBwkP3bLGs5CwJ1D99Q6', + }, + ], + }, + stake: '2500000000000000', + noteMetadata: { + header: 'l1.media for you', + bulletPoints: [ + 'Make the DAO socially relevant by providing actual utility and solve actual problems', + 'Focus on services', + ], + bannerImageUri: 'https://joystreamstats.live/static/media/ai-office.jpeg', + description: + "No self-voting. If elected i'll spend more time to assemble a team and restore l1.media and joystreamstats.live\n\n* Focus on services\n* Keep gov atmosphere joyful\n* For devs it’s currently unattainable to gain big allocations. Provide new opportunities for big contribtutions.\n* The council/wg reporting system is ineffective to achieve goals.\n* Create gw for regional growing communities\n* Make running a gw as easy as mastodon\n* Put price tags on roadmap items", + }, + status: 'FAILED', + stakingAccountId: 'j4Wg7JSHHLQpxFQfhEB9T57bbeSjFNKpFcoppifCNj3VxWj4C', + votePower: '0', + votesReceived: [], + }, + { + id: '0000006y', + member: { + id: '957', + rootAccount: 'j4U6QUxnCrhhbxaLyqXNq6VnVH1Bq5MBzo8wkjaPDsSzUZdbF', + controllerAccount: 'j4VoXReFKK1FSTNczaYdcSSC9T96cc1RUb4aja1oXFWKbpMjk', + boundAccounts: [ + 'j4U6QUxnCrhhbxaLyqXNq6VnVH1Bq5MBzo8wkjaPDsSzUZdbF', + 'j4S8ySabhToiXsEzjnzcXAtEDXv1YYzC4rY7VoWopCXrFSpqK', + 'j4Tgk9dFzGMZk4hW25k9uf4gWfBN8vg3SAc8xbDeQpDyNGEDG', + 'j4TZHG7owBRZWSbNG6RhiE4hrRdTZvc6yqGUTdVcEKCu9HFnN', + 'j4VSjLBvfcbBTEMNMGiBUSCXBpxvvyPv3FERAkXpe4BAQ4oQi', + 'j4U5cu23otryUAE15zq6ZFtdtXf4NQGr4bRcmqpNVBka5NgD2', + 'j4UBm6X5SPBcMtC9Xr9ExRMAGAjc5XicgJd2Mcy9WcmQUDNLD', + 'j4VQ8XM23ddHAQQE5sS6yU4RLgLHLNQ84CSJDJ5cK9PYKteYy', + 'j4W8EQ8Bxzvtf7ngjGNPSm8LQsoRWWH32BhP6qcQQB7xG7p59', + 'j4SLWkHT47sQGLseyqw85Gaf2dEpN5sNFKpb9yRTrvQfpcFHJ', + 'j4WQ1qCAjvbPHb4ptizot5XcM2BBq9TrWUgw24NgRH9B3pXQr', + 'j4UBmT3fUnxxKh24pYqEBWYG7GgkJJ8WpqBJwuk5jjuWNyRvo', + 'j4SW7xG7PPuTdbZEHAFdnYS3zUaGNtHbtLck2xosY41rMzo2d', + 'j4WVAzoHp76Qt8qQ5tR9wZPhXoYcqBHHFxgmvVL21T5YMHd7b', + 'j4Wrd8Pjqh1jeGkgUv95fDegNeijsioBQx4QrEuevDRFU3qpz', + 'j4VGTrFGj7PKzDPHysicwWh3o8AFTH647Vk4AZAH2fJGsP3VV', + 'j4UETNicTHDkrUEWjxv9PB3XAY754DREYDRDfeB9aqgeySA9J', + ], + handle: 'leet_joy', + metadata: { + name: 'leet_joy', + about: 'FM', + avatar: { + __typename: 'AvatarUri', + avatarUri: + 'https://atlas-services.joystream.org/avatars/d4372f26-9822-4d22-a67c-8f5aec23d187.webp', + }, + isVerifiedValidator: true, + }, + isVerified: true, + isFoundingMember: true, + isCouncilMember: true, + inviteCount: 2, + roles: [ + { + id: 'operationsWorkingGroupGamma-0', + group: { + name: 'operationsWorkingGroupGamma', + }, + createdAt: '2023-01-14T11:24:42.002Z', + isLead: false, + isActive: false, + }, + ], + createdAt: '2025-03-14T04:00:48.000Z', + stakingaccountaddedeventmember: [ + { + createdAt: '2023-01-06T14:48:18.000Z', + inBlock: 399054, + network: 'OLYMPIA', + account: 'j4U6QUxnCrhhbxaLyqXNq6VnVH1Bq5MBzo8wkjaPDsSzUZdbF', + }, + { + createdAt: '2023-01-23T12:20:42.001Z', + inBlock: 641975, + network: 'OLYMPIA', + account: 'j4S8ySabhToiXsEzjnzcXAtEDXv1YYzC4rY7VoWopCXrFSpqK', + }, + { + createdAt: '2023-01-31T18:40:18.000Z', + inBlock: 760971, + network: 'OLYMPIA', + account: 'j4Tgk9dFzGMZk4hW25k9uf4gWfBN8vg3SAc8xbDeQpDyNGEDG', + }, + { + createdAt: '2023-02-19T09:25:00.000Z', + inBlock: 1028636, + network: 'OLYMPIA', + account: 'j4TZHG7owBRZWSbNG6RhiE4hrRdTZvc6yqGUTdVcEKCu9HFnN', + }, + { + createdAt: '2023-03-09T01:59:18.000Z', + inBlock: 1283187, + network: 'OLYMPIA', + account: 'j4VSjLBvfcbBTEMNMGiBUSCXBpxvvyPv3FERAkXpe4BAQ4oQi', + }, + { + createdAt: '2023-03-21T17:07:06.000Z', + inBlock: 1464946, + network: 'OLYMPIA', + account: 'j4U5cu23otryUAE15zq6ZFtdtXf4NQGr4bRcmqpNVBka5NgD2', + }, + { + createdAt: '2023-06-25T13:31:30.000Z', + inBlock: 2841634, + network: 'OLYMPIA', + account: 'j4UBm6X5SPBcMtC9Xr9ExRMAGAjc5XicgJd2Mcy9WcmQUDNLD', + }, + { + createdAt: '2023-08-05T17:28:30.001Z', + inBlock: 3433271, + network: 'OLYMPIA', + account: 'j4VQ8XM23ddHAQQE5sS6yU4RLgLHLNQ84CSJDJ5cK9PYKteYy', + }, + { + createdAt: '2023-08-17T21:09:06.000Z', + inBlock: 3607907, + network: 'OLYMPIA', + account: 'j4W8EQ8Bxzvtf7ngjGNPSm8LQsoRWWH32BhP6qcQQB7xG7p59', + }, + { + createdAt: '2023-08-29T00:36:36.000Z', + inBlock: 3767651, + network: 'OLYMPIA', + account: 'j4SLWkHT47sQGLseyqw85Gaf2dEpN5sNFKpb9yRTrvQfpcFHJ', + }, + { + createdAt: '2023-10-17T05:14:48.000Z', + inBlock: 4474973, + network: 'OLYMPIA', + account: 'j4WQ1qCAjvbPHb4ptizot5XcM2BBq9TrWUgw24NgRH9B3pXQr', + }, + { + createdAt: '2023-12-27T16:11:36.000Z', + inBlock: 5499565, + network: 'OLYMPIA', + account: 'j4UBmT3fUnxxKh24pYqEBWYG7GgkJJ8WpqBJwuk5jjuWNyRvo', + }, + { + createdAt: '2024-02-05T13:55:36.000Z', + inBlock: 6073114, + network: 'OLYMPIA', + account: 'j4SW7xG7PPuTdbZEHAFdnYS3zUaGNtHbtLck2xosY41rMzo2d', + }, + { + createdAt: '2024-03-05T21:08:06.000Z', + inBlock: 6491625, + network: 'OLYMPIA', + account: 'j4WVAzoHp76Qt8qQ5tR9wZPhXoYcqBHHFxgmvVL21T5YMHd7b', + }, + { + createdAt: '2024-08-20T10:49:54.001Z', + inBlock: 8895002, + network: 'OLYMPIA', + account: 'j4Wrd8Pjqh1jeGkgUv95fDegNeijsioBQx4QrEuevDRFU3qpz', + }, + { + createdAt: '2024-08-20T17:56:06.000Z', + inBlock: 8899214, + network: 'OLYMPIA', + account: 'j4VGTrFGj7PKzDPHysicwWh3o8AFTH647Vk4AZAH2fJGsP3VV', + }, + { + createdAt: '2024-11-19T17:59:06.000Z', + inBlock: 10206444, + network: 'OLYMPIA', + account: 'j4UETNicTHDkrUEWjxv9PB3XAY754DREYDRDfeB9aqgeySA9J', + }, + ], + }, + stake: '1666666666660000', + noteMetadata: { + header: 'leet_joy', + bulletPoints: ['high exp as cm', 'hodler'], + bannerImageUri: + 'https://github.com/Joystream/founding-members/blob/main/avatars/selected-avatars/42-leet_joy.png?raw=true', + description: + "Hello everyone, [_**@leet\\_joy**_](#mention?member-id=957) and I'm ready to continue work as CM in Term 44, supporting protocol adoption and development.\n\nPriorities :\n\n* Builders: SDK & Infrastructure authentication;\n* Infrastructure: cost optimization, reduce number of active hosts to 4\n* Marketing: strengthen the team, increase content reach\n* Content: delete large trash files, improve top video in categories\n\nWhen it comes to spending, I would opt for a conservative plan and not exceed 0.5% inflation per term. \n\nPrevious council applications:\n\n* [Term 43](https://pioneerapp.xyz/#/election/past-elections/43?candidate=0000006v)\n* [Term 42](https://pioneerapp.xyz/#/election/past-elections/42?candidate=0000006s)\n* [Term 41](https://pioneerapp.xyz/#/election/past-elections/41?candidate=0000006n)\n* [Term 40](https://pioneerapp.xyz/#/election/past-elections/40?candidate=0000006k)", + }, + status: 'ELECTED', + stakingAccountId: 'j4TZHG7owBRZWSbNG6RhiE4hrRdTZvc6yqGUTdVcEKCu9HFnN', + votePower: '143333330000000000', + votesReceived: [ + { + id: '00000381', + }, + ], + }, + ], + castVotes: [ + { + stake: '1666700000000', + stakeLocked: true, + voteForId: '00000072', + castBy: 'j4SEkmk9DM2TScQiPX4tcPxDHPXAKJQzuzbwJ6CiHMWrw7c2m', + }, + { + stake: '1779950000000000', + stakeLocked: true, + voteForId: '00000072', + castBy: 'j4UBmT3fUnxxKh24pYqEBWYG7GgkJJ8WpqBJwuk5jjuWNyRvo', + }, + { + stake: '895000000000000', + stakeLocked: true, + voteForId: '00000072', + castBy: 'j4UBm6X5SPBcMtC9Xr9ExRMAGAjc5XicgJd2Mcy9WcmQUDNLD', + }, + { + stake: '1618390000000000', + stakeLocked: true, + voteForId: null, + castBy: 'j4VoXReFKK1FSTNczaYdcSSC9T96cc1RUb4aja1oXFWKbpMjk', + }, + ], + nextElectedCouncil, + }, + }, + }, + ], + }, + } + }, + }, +} satisfies Meta + +export const Default: Story = {} diff --git a/packages/ui/src/app/pages/Election/PastElections/PastElection.tsx b/packages/ui/src/app/pages/Election/PastElections/PastElection.tsx index b55df19a84..339912521f 100644 --- a/packages/ui/src/app/pages/Election/PastElections/PastElection.tsx +++ b/packages/ui/src/app/pages/Election/PastElections/PastElection.tsx @@ -1,18 +1,20 @@ import React, { useEffect } from 'react' -import { useHistory, useParams } from 'react-router-dom' +import { generatePath, Link, useHistory, useParams } from 'react-router-dom' +import styled from 'styled-components' import { PageHeaderWithButtons, PageHeaderWrapper, PageLayout } from '@/app/components/PageLayout' -import { BadgesRow, BadgeStatus } from '@/common/components/BadgeStatus' +import { BadgesRow, BadgeStatus, BadgeStatusCss } from '@/common/components/BadgeStatus' import { ButtonsGroup, CopyButtonTemplate } from '@/common/components/buttons' import { LinkIcon } from '@/common/components/icons' import { Loading } from '@/common/components/Loading' import { MainPanel, RowGapBlock } from '@/common/components/page/PageContent' import { PageTitle } from '@/common/components/page/PageTitle' import { PreviousPage } from '@/common/components/page/PreviousPage' +import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' import { getUrl } from '@/common/utils/getUrl' import { PastElectionStats } from '@/council/components/election/pastElection/PastElectionStats' import { PastElectionTabs } from '@/council/components/election/pastElection/PastElectionTabs' -import { ElectionRoutes } from '@/council/constants' +import { CouncilRoutes, ElectionRoutes } from '@/council/constants' import { useCandidatePreviewViaUrlParameter } from '@/council/hooks/useCandidatePreviewViaUrlParameter' import { usePastElection } from '@/council/hooks/usePastElection' @@ -55,6 +57,23 @@ export const PastElection = () => { Past Election + {election.result == 'successful' ? ( + + Successful + + ) : ( + + Failed + + + + + )} @@ -77,3 +96,11 @@ export const PastElection = () => { return } +const StyledBadge = styled(Link)` + ${BadgeStatusCss} +` +const TooltipBadge = styled(BadgeStatus)` + display: flex; + align-items: center; + gap: 4px; +` diff --git a/packages/ui/src/app/pages/Election/PastElections/PastElections.stories.tsx b/packages/ui/src/app/pages/Election/PastElections/PastElections.stories.tsx new file mode 100644 index 0000000000..441efd52af --- /dev/null +++ b/packages/ui/src/app/pages/Election/PastElections/PastElections.stories.tsx @@ -0,0 +1,227 @@ +import { Meta, StoryObj } from '@storybook/react' +import { FC } from 'react' + +import { GetPastElectionsDocument } from '@/council/queries' +import { MocksParameters } from '@/mocks/providers' + +import { PastElections } from './PastElections' + +type Story = StoryObj + +export default { + title: 'Pages/Election/PastElections/PastElections', + component: PastElections, + parameters: { + mocks: (): MocksParameters => { + return { + gql: { + queries: [ + { + query: GetPastElectionsDocument, + data: { + electionRounds: [ + { + id: '00000018', + cycleId: 44, + endedAtBlock: 11851230, + endedAtTime: '2025-03-14T04:00:48.000Z', + endedAtNetwork: 'OLYMPIA', + candidates: [ + { + stake: '1666666666660000', + }, + { + stake: '2500000000000000', + }, + { + stake: '1666666666660000', + }, + { + stake: '1666666666660000', + }, + { + stake: '1666666666660000', + }, + ], + castVotes: [ + { + voteForId: null, + stake: '41000000000000000', + }, + { + voteForId: null, + stake: '1666660000000000', + }, + { + voteForId: null, + stake: '1754810000000000', + }, + { + voteForId: null, + stake: '937830000000000', + }, + { + voteForId: null, + stake: '1740630000000000', + }, + { + voteForId: null, + stake: '945220000000000', + }, + { + voteForId: null, + stake: '1533080000000000', + }, + { + voteForId: null, + stake: '499940000000000', + }, + { + voteForId: null, + stake: '937380000000000', + }, + { + voteForId: '00000072', + stake: '18000000000000000', + }, + { + voteForId: '00000072', + stake: '20900000000000000', + }, + { + voteForId: '00000072', + stake: '1666700000000', + }, + { + voteForId: '00000072', + stake: '1779950000000000', + }, + { + voteForId: '00000072', + stake: '895000000000000', + }, + { + voteForId: '00000072', + stake: '1618390000000000', + }, + { + voteForId: '00000072', + stake: '17600240000000000', + }, + { + voteForId: '00000072', + stake: '3333330000000000', + }, + { + voteForId: '00000072', + stake: '15413050000000000', + }, + { + voteForId: '00000072', + stake: '554080000000000', + }, + { + voteForId: '00000072', + stake: '1060000000000000', + }, + { + voteForId: '00000070', + stake: '5500000000000000', + }, + { + voteForId: '00000070', + stake: '5500000000000000', + }, + { + voteForId: '00000070', + stake: '8500000000000000', + }, + { + voteForId: '00000070', + stake: '2200000000000000', + }, + { + voteForId: '00000070', + stake: '2000000000000000', + }, + { + voteForId: '00000070', + stake: '1392790000000000', + }, + { + voteForId: '00000070', + stake: '510000000000000', + }, + { + voteForId: '00000070', + stake: '1660000000000000', + }, + { + voteForId: null, + stake: '8430000000000000', + }, + { + voteForId: '00000070', + stake: '1660000000000000', + }, + { + voteForId: null, + stake: '2000000000000000', + }, + { + voteForId: null, + stake: '8500000000000000', + }, + { + voteForId: null, + stake: '10000000000000000', + }, + { + voteForId: '00000072', + stake: '9000000000000000', + }, + { + voteForId: '00000070', + stake: '40000000000000000', + }, + { + voteForId: '00000070', + stake: '11600000000000000', + }, + { + voteForId: '0000006y', + stake: '143333330000000000', + }, + ], + nextElectedCouncil: { + councilElections: [ + { + cycleId: 44, + }, + ], + }, + }, + { + id: '00000019', + cycleId: 45, + endedAtBlock: 12139230, + endedAtTime: '2025-04-03T04:25:48.000Z', + endedAtNetwork: 'OLYMPIA', + candidates: [ + { + stake: '1666666666660000', + }, + ], + castVotes: [], + }, + ], + }, + }, + ], + }, + } + }, + }, +} satisfies Meta + +export const Default: Story = {} diff --git a/packages/ui/src/app/pages/Election/PastElections/PastElections.tsx b/packages/ui/src/app/pages/Election/PastElections/PastElections.tsx index 6b94882207..06bcb56144 100644 --- a/packages/ui/src/app/pages/Election/PastElections/PastElections.tsx +++ b/packages/ui/src/app/pages/Election/PastElections/PastElections.tsx @@ -53,6 +53,7 @@ export const PastElections = () => { Total Votes staked Revealed votes Total candidates + Result @@ -66,7 +67,7 @@ export const PastElections = () => { return } -export const PastElectionsColLayout = '48px 176px 140px 140px 100px 100px' +export const PastElectionsColLayout = '48px 176px 140px 140px 100px 100px 48px' const PastElectionsListHeaders = styled(ListHeaders)` grid-column-gap: 24px; diff --git a/packages/ui/src/common/api/queries/__generated__/baseTypes.generated.ts b/packages/ui/src/common/api/queries/__generated__/baseTypes.generated.ts index 446c1e10fd..29097f873f 100644 --- a/packages/ui/src/common/api/queries/__generated__/baseTypes.generated.ts +++ b/packages/ui/src/common/api/queries/__generated__/baseTypes.generated.ts @@ -12201,6 +12201,7 @@ export enum EntitySubscriptionKind { ForumCategoryEntityPost = 'FORUM_CATEGORY_ENTITY_POST', ForumCategoryEntityThread = 'FORUM_CATEGORY_ENTITY_THREAD', ForumThreadEntityPost = 'FORUM_THREAD_ENTITY_POST', + ProposalEntityDiscussion = 'PROPOSAL_ENTITY_DISCUSSION', } export enum EntitySubscriptionStatus { @@ -13240,6 +13241,11 @@ export enum GeneralSubscriptionKind { ForumThreadContributor = 'FORUM_THREAD_CONTRIBUTOR', ForumThreadCreator = 'FORUM_THREAD_CREATOR', ForumThreadMention = 'FORUM_THREAD_MENTION', + ProposalDiscussionAll = 'PROPOSAL_DISCUSSION_ALL', + ProposalDiscussionContributor = 'PROPOSAL_DISCUSSION_CONTRIBUTOR', + ProposalDiscussionCreator = 'PROPOSAL_DISCUSSION_CREATOR', + ProposalDiscussionMention = 'PROPOSAL_DISCUSSION_MENTION', + ProposalDiscussionReply = 'PROPOSAL_DISCUSSION_REPLY', } export type GeoCoordinates = BaseGraphQlObject & { @@ -18372,6 +18378,11 @@ export enum NotificationKind { ForumThreadCreator = 'FORUM_THREAD_CREATOR', ForumThreadEntityPost = 'FORUM_THREAD_ENTITY_POST', ForumThreadMention = 'FORUM_THREAD_MENTION', + ProposalDiscussionAll = 'PROPOSAL_DISCUSSION_ALL', + ProposalDiscussionContributor = 'PROPOSAL_DISCUSSION_CONTRIBUTOR', + ProposalDiscussionCreator = 'PROPOSAL_DISCUSSION_CREATOR', + ProposalDiscussionMention = 'PROPOSAL_DISCUSSION_MENTION', + ProposalDiscussionReply = 'PROPOSAL_DISCUSSION_REPLY', } export type OfferAcceptedEvent = BaseGraphQlObject & diff --git a/packages/ui/src/common/components/Activities/ActivityIcon.tsx b/packages/ui/src/common/components/Activities/ActivityIcon.tsx index 1a747fc514..9e7cf5fa22 100644 --- a/packages/ui/src/common/components/Activities/ActivityIcon.tsx +++ b/packages/ui/src/common/components/Activities/ActivityIcon.tsx @@ -21,7 +21,7 @@ export const ActivityIcon = React.memo(({ category }: ActivityIconProps) => { ) }) -const IconWrap = styled.div<{ iconStyle: IconStyle }>` +export const IconWrap = styled.div<{ iconStyle: IconStyle }>` display: flex; grid-area: activityicon; justify-content: center; diff --git a/packages/ui/src/common/components/BlockTime/BlockTime.tsx b/packages/ui/src/common/components/BlockTime/BlockTime.tsx index 445883810d..e1885e93f9 100644 --- a/packages/ui/src/common/components/BlockTime/BlockTime.tsx +++ b/packages/ui/src/common/components/BlockTime/BlockTime.tsx @@ -36,7 +36,7 @@ const Separator = styled.span` line-height: inherit; ` -const AboutText = styled(TextMedium)` +export const AboutText = styled(TextMedium)` color: ${Colors.Black[600]}; width: max-content; ` diff --git a/packages/ui/src/common/components/statistics/StatisticBar.tsx b/packages/ui/src/common/components/statistics/StatisticBar.tsx index df9a2dd7f6..d3bc430eab 100644 --- a/packages/ui/src/common/components/statistics/StatisticBar.tsx +++ b/packages/ui/src/common/components/statistics/StatisticBar.tsx @@ -11,9 +11,17 @@ import { NumericValue } from './NumericValueStat' export interface StatisticBarProps extends StatisticHeaderProps, FractionValueProps { value: number threshold?: number + figureWidth?: number } -export const StatisticBar = ({ value, threshold, numerator, denominator, ...headerProps }: StatisticBarProps) => ( +export const StatisticBar = ({ + value, + threshold, + figureWidth, + numerator, + denominator, + ...headerProps +}: StatisticBarProps) => ( <> @@ -22,7 +30,7 @@ export const StatisticBar = ({ value, threshold, numerator, denominator, ...head -
+
@@ -49,9 +57,9 @@ const ThresholdBar = styled.div<{ threshold?: number }>` `}; ` -const Figure = styled.div` +const Figure = styled.div<{ figureWidth?: number }>` display: flex; - width: 120px; + width: ${({ figureWidth }) => figureWidth || 120}px; justify-content: flex-end; align-items: flex-end; margin-left: auto; diff --git a/packages/ui/src/council/components/election/CandidateVote/CandidateVote.tsx b/packages/ui/src/council/components/election/CandidateVote/CandidateVote.tsx index 24de3fbf51..3f94614d0a 100644 --- a/packages/ui/src/council/components/election/CandidateVote/CandidateVote.tsx +++ b/packages/ui/src/council/components/election/CandidateVote/CandidateVote.tsx @@ -161,7 +161,6 @@ const CandidateVoteWrapper = styled(ListItem)` * { word-break: normal; } - &:hover, &:focus, &:focus-within { diff --git a/packages/ui/src/council/components/election/CandidateVote/CandidateVoteList.tsx b/packages/ui/src/council/components/election/CandidateVote/CandidateVoteList.tsx index 3ea6e08318..edc05ec6b2 100644 --- a/packages/ui/src/council/components/election/CandidateVote/CandidateVoteList.tsx +++ b/packages/ui/src/council/components/election/CandidateVote/CandidateVoteList.tsx @@ -1,24 +1,62 @@ -import React from 'react' +import React, { useMemo } from 'react' import styled from 'styled-components' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { TextMedium } from '@/common/components/typography' +import { Colors } from '@/common/constants' + import { CandidateVote, CandidateVoteProps } from './CandidateVote' interface VotesListProps { votes: CandidateVoteProps[] + isSuccessfulPastElection?: boolean } -export const CandidateVoteList = ({ votes }: VotesListProps) => { +export const CandidateVoteList = ({ votes, isSuccessfulPastElection }: VotesListProps) => { + const winners = useMemo(() => { + if (!isSuccessfulPastElection) return [] + return votes.filter((vote, index) => index < 3) + }, [isSuccessfulPastElection, votes]) + + const losers = useMemo(() => { + if (!isSuccessfulPastElection) return [] + return votes.filter((vote, index) => index >= 3) + }, [isSuccessfulPastElection, votes]) return ( - - {votes.map((vote, index) => ( - - ))} - + + {winners.length > 0 ? ( + <> + + Winners + + {winners.map((vote, index) => ( + + ))} + + + + Losers + + {losers.map((vote, index) => ( + + ))} + + + + ) : ( + + {votes.map((vote, index) => ( + + ))} + + )} + ) } -const VotesListStyles = styled.section` +const VotesListStyles = styled.section<{ winners?: boolean }>` display: grid; width: 100%; max-width: 100%; + border: ${({ winners }) => (winners ? `3px solid ${Colors.Blue[500]}` : 'none')}; ` diff --git a/packages/ui/src/council/components/election/pastElection/PastElectionStats.tsx b/packages/ui/src/council/components/election/pastElection/PastElectionStats.tsx index ca30f49364..793505cbbe 100644 --- a/packages/ui/src/council/components/election/pastElection/PastElectionStats.tsx +++ b/packages/ui/src/council/components/election/pastElection/PastElectionStats.tsx @@ -1,8 +1,9 @@ +import BN from 'bn.js' import React from 'react' import { NumericValueStat, StatisticBar, StatisticItem, Statistics, StatsBlock } from '@/common/components/statistics' -import { TextHuge } from '@/common/components/typography' -import { formatDateString } from '@/common/model/formatters' +import { TextHuge, ValueInMJoys } from '@/common/components/typography' +import { formatDateString, formatJoyValue } from '@/common/model/formatters' import { Block } from '@/common/types' interface PastElectionStatsProps { @@ -11,6 +12,8 @@ interface PastElectionStatsProps { totalCandidates: number revealedVotes: number totalVotes: number + totalRevealedVoteStake: BN + totalVoteStake: BN } export const PastElectionStats = ({ @@ -19,6 +22,8 @@ export const PastElectionStats = ({ totalCandidates, revealedVotes, totalVotes, + totalRevealedVoteStake, + totalVoteStake, }: PastElectionStatsProps) => ( @@ -42,5 +47,22 @@ export const PastElectionStats = ({ denominator={totalVotes + ' votes'} /> + + + {formatJoyValue(totalRevealedVoteStake.divn(1e6), { precision: 2 })} + + } + denominator={ + + {formatJoyValue(totalVoteStake.divn(1e6), { precision: 2 })} + + } + /> + ) diff --git a/packages/ui/src/council/components/election/pastElection/PastElectionTabs.tsx b/packages/ui/src/council/components/election/pastElection/PastElectionTabs.tsx index 6c72c49062..6814098b16 100644 --- a/packages/ui/src/council/components/election/pastElection/PastElectionTabs.tsx +++ b/packages/ui/src/council/components/election/pastElection/PastElectionTabs.tsx @@ -69,6 +69,7 @@ export const PastElectionTabs = ({ election }: PastElectionTabsProps) => { myStake: sumStakes(myVotesTmp), } })} + isSuccessfulPastElection={election.result === 'successful' && tab === 'votingResults'} /> ) } diff --git a/packages/ui/src/council/components/election/pastElection/PastElectionsList/PastElectionsListRow.tsx b/packages/ui/src/council/components/election/pastElection/PastElectionsList/PastElectionsListRow.tsx index a12d25081e..e8b80576cf 100644 --- a/packages/ui/src/council/components/election/pastElection/PastElectionsList/PastElectionsListRow.tsx +++ b/packages/ui/src/council/components/election/pastElection/PastElectionsList/PastElectionsListRow.tsx @@ -3,7 +3,10 @@ import { generatePath } from 'react-router' import styled from 'styled-components' import { PastElectionsColLayout } from '@/app/pages/Election/PastElections/PastElections' +import { IconWrap } from '@/common/components/Activities/ActivityIcon' import { BlockTime } from '@/common/components/BlockTime' +import { CheckboxIcon } from '@/common/components/icons' +import { ClosedIcon } from '@/common/components/icons/activities' import { TableListItem, TableListItemAsLinkHover } from '@/common/components/List' import { GhostRouterLink } from '@/common/components/RouterLink' import { TokenValue } from '@/common/components/typography' @@ -33,6 +36,15 @@ export const PastElectionsListRow = ({ election }: PastElectionsListRowProps) => + {election.result == 'successful' ? ( + + + + ) : ( + + + + )} ) } @@ -43,3 +55,6 @@ const PastElectionsListRowItem = styled(TableListItem)` ${TableListItemAsLinkHover}; ` +const StyledIconWrap = styled(IconWrap)` + grid-area: auto; +` diff --git a/packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilListItem.tsx b/packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilListItem.tsx index 7982ad34aa..c348b2bd39 100644 --- a/packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilListItem.tsx +++ b/packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilListItem.tsx @@ -30,7 +30,7 @@ export const PastCouncilListItem = ({ council }: Props) => { as={GhostRouterLink} to={generatePath(CouncilRoutes.pastCouncil, { id: council.id })} > - #{council.id} + #{parseInt(council.id, 36)} {isLoading ? : } {isLoading ? : } diff --git a/packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilsList.stories.tsx b/packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilsList.stories.tsx index 8066ece867..e12dd10e9e 100644 --- a/packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilsList.stories.tsx +++ b/packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilsList.stories.tsx @@ -24,9 +24,9 @@ export const Default = Template.bind({}) Default.args = { councils: [ - { id: '0', endedAt: randomBlock() }, - { id: '1', endedAt: randomBlock() }, - { id: '1', endedAt: randomBlock() }, + { id: '0', endedAt: randomBlock(), electionCycleId: 10 }, + { id: '1', endedAt: randomBlock(), electionCycleId: 11 }, + { id: '1', endedAt: randomBlock(), electionCycleId: 12 }, ], isLoading: false, } diff --git a/packages/ui/src/council/queries/__generated__/council.generated.tsx b/packages/ui/src/council/queries/__generated__/council.generated.tsx index 0b7b6e5e54..fb365ad9ac 100644 --- a/packages/ui/src/council/queries/__generated__/council.generated.tsx +++ b/packages/ui/src/council/queries/__generated__/council.generated.tsx @@ -235,6 +235,7 @@ export type PastCouncilFieldsFragment = { endedAtBlock?: number | null endedAtNetwork?: Types.Network | null endedAtTime?: any | null + councilElections: Array<{ __typename: 'ElectionRound'; cycleId: number }> } export type PastCouncilDetailedFieldsFragment = { @@ -244,6 +245,7 @@ export type PastCouncilDetailedFieldsFragment = { endedAtNetwork?: Types.Network | null endedAtTime?: any | null councilMembers: Array<{ __typename: 'CouncilMember'; accumulatedReward: string; unpaidReward: string }> + councilElections: Array<{ __typename: 'ElectionRound'; cycleId: number }> } export type ElectionCandidateFieldsFragment = { @@ -352,6 +354,7 @@ export type ElectionRoundFieldsFragment = { } votesReceived: Array<{ __typename: 'CastVote'; id: string }> }> + castVotes: Array<{ __typename: 'CastVote'; voteForId?: string | null; stake: string }> } export type LatestElectionRoundFieldsFragment = { @@ -409,6 +412,7 @@ export type LatestElectionRoundFieldsFragment = { } votesReceived: Array<{ __typename: 'CastVote'; id: string }> }> + castVotes: Array<{ __typename: 'CastVote'; voteForId?: string | null; stake: string }> } export type PastElectionRoundFieldsFragment = { @@ -420,6 +424,10 @@ export type PastElectionRoundFieldsFragment = { endedAtNetwork?: Types.Network | null candidates: Array<{ __typename: 'Candidate'; stake: string }> castVotes: Array<{ __typename: 'CastVote'; voteForId?: string | null; stake: string }> + nextElectedCouncil?: { + __typename: 'ElectedCouncil' + councilElections: Array<{ __typename: 'ElectionRound'; cycleId: number }> + } | null } export type PastElectionRoundDetailedFieldsFragment = { @@ -487,6 +495,10 @@ export type PastElectionRoundDetailedFieldsFragment = { voteForId?: string | null castBy: string }> + nextElectedCouncil?: { + __typename: 'ElectedCouncil' + councilElections: Array<{ __typename: 'ElectionRound'; cycleId: number }> + } | null } export type ElectionCandidateDetailedFieldsFragment = { @@ -733,6 +745,7 @@ export type GetPastCouncilsQuery = { endedAtBlock?: number | null endedAtNetwork?: Types.Network | null endedAtTime?: any | null + councilElections: Array<{ __typename: 'ElectionRound'; cycleId: number }> }> } @@ -758,6 +771,7 @@ export type GetPastCouncilQuery = { endedAtNetwork?: Types.Network | null endedAtTime?: any | null councilMembers: Array<{ __typename: 'CouncilMember'; accumulatedReward: string; unpaidReward: string }> + councilElections: Array<{ __typename: 'ElectionRound'; cycleId: number }> } | null budgetSpendingEvents: Array<{ __typename: 'BudgetSpendingEvent' @@ -1118,6 +1132,7 @@ export type GetCurrentElectionQuery = { } votesReceived: Array<{ __typename: 'CastVote'; id: string }> }> + castVotes: Array<{ __typename: 'CastVote'; voteForId?: string | null; stake: string }> }> } @@ -1180,6 +1195,7 @@ export type GetLatestElectionQuery = { } votesReceived: Array<{ __typename: 'CastVote'; id: string }> }> + castVotes: Array<{ __typename: 'CastVote'; voteForId?: string | null; stake: string }> }> } @@ -1200,6 +1216,10 @@ export type GetPastElectionsQuery = { endedAtNetwork?: Types.Network | null candidates: Array<{ __typename: 'Candidate'; stake: string }> castVotes: Array<{ __typename: 'CastVote'; voteForId?: string | null; stake: string }> + nextElectedCouncil?: { + __typename: 'ElectedCouncil' + councilElections: Array<{ __typename: 'ElectionRound'; cycleId: number }> + } | null }> } @@ -1281,6 +1301,10 @@ export type GetPastElectionQuery = { voteForId?: string | null castBy: string }> + nextElectedCouncil?: { + __typename: 'ElectedCouncil' + councilElections: Array<{ __typename: 'ElectionRound'; cycleId: number }> + } | null } | null } @@ -1658,6 +1682,9 @@ export const PastCouncilFieldsFragmentDoc = gql` endedAtBlock endedAtNetwork endedAtTime + councilElections { + cycleId + } } ` export const PastCouncilDetailedFieldsFragmentDoc = gql` @@ -1698,6 +1725,10 @@ export const ElectionRoundFieldsFragmentDoc = gql` candidates { ...ElectionCandidateFields } + castVotes { + voteForId + stake + } } ${ElectionCandidateFieldsFragmentDoc} ` @@ -1722,6 +1753,11 @@ export const PastElectionRoundFieldsFragmentDoc = gql` voteForId stake } + nextElectedCouncil { + councilElections { + cycleId + } + } } ` export const PastElectionRoundDetailedFieldsFragmentDoc = gql` diff --git a/packages/ui/src/council/queries/council.graphql b/packages/ui/src/council/queries/council.graphql index a537c27716..499947c6f8 100644 --- a/packages/ui/src/council/queries/council.graphql +++ b/packages/ui/src/council/queries/council.graphql @@ -71,6 +71,9 @@ fragment PastCouncilFields on ElectedCouncil { endedAtBlock endedAtNetwork endedAtTime + councilElections { + cycleId + } } fragment PastCouncilDetailedFields on ElectedCouncil { @@ -106,6 +109,10 @@ fragment ElectionRoundFields on ElectionRound { candidates { ...ElectionCandidateFields } + castVotes { + voteForId + stake + } } fragment LatestElectionRoundFields on ElectionRound { @@ -126,6 +133,11 @@ fragment PastElectionRoundFields on ElectionRound { voteForId stake } + nextElectedCouncil { + councilElections { + cycleId + } + } } fragment PastElectionRoundDetailedFields on ElectionRound { diff --git a/packages/ui/src/council/types/Election.ts b/packages/ui/src/council/types/Election.ts index 79552c8a82..7f284367bd 100644 --- a/packages/ui/src/council/types/Election.ts +++ b/packages/ui/src/council/types/Election.ts @@ -1,6 +1,7 @@ import BN from 'bn.js' import { BN_ZERO } from '@/common/constants' +import { sumStakes } from '@/common/utils/bn' import { ElectionRoundFieldsFragment } from '@/council/queries' import { asElectionCandidate, ElectionCandidate } from '@/council/types/Candidate' @@ -10,10 +11,16 @@ export interface Election { cycleId: number candidates: ElectionCandidate[] totalElectionStake: BN + revealedVotes: number + votesNumber: number + totalVotesStake: BN } export const asElection = (fields: ElectionRoundFieldsFragment): Election => ({ cycleId: fields.cycleId, candidates: fields.candidates.filter((candidate) => candidate.status !== 'WITHDRAWN').map(asElectionCandidate), totalElectionStake: fields.candidates.reduce((prev, next) => prev.add(new BN(next.votePower)), BN_ZERO), + revealedVotes: fields.castVotes.filter((castVote) => castVote.voteForId).length, + totalVotesStake: sumStakes(fields.castVotes), + votesNumber: fields.castVotes.length, }) diff --git a/packages/ui/src/council/types/LatestElection.ts b/packages/ui/src/council/types/LatestElection.ts index 3f6ff18b15..1e819e0c92 100644 --- a/packages/ui/src/council/types/LatestElection.ts +++ b/packages/ui/src/council/types/LatestElection.ts @@ -1,6 +1,7 @@ import { BN } from '@polkadot/util' import { BN_ZERO } from '@/common/constants' +import { sumStakes } from '@/common/utils' import { LatestElectionRoundFieldsFragment } from '@/council/queries' import { asElectionCandidate } from '@/council/types/Candidate' @@ -15,4 +16,7 @@ export const asLatestElection = (fields: LatestElectionRoundFieldsFragment): Lat candidates: fields.candidates.map(asElectionCandidate), isFinished: fields.isFinished, totalElectionStake: fields.candidates.reduce((prev, next) => prev.add(new BN(next.votePower)), BN_ZERO), + revealedVotes: fields.castVotes.filter((castVote) => castVote.voteForId).length, + totalVotesStake: sumStakes(fields.castVotes), + votesNumber: fields.castVotes.length, }) diff --git a/packages/ui/src/council/types/PastCouncil.ts b/packages/ui/src/council/types/PastCouncil.ts index 4162787d05..aba23596a9 100644 --- a/packages/ui/src/council/types/PastCouncil.ts +++ b/packages/ui/src/council/types/PastCouncil.ts @@ -17,6 +17,7 @@ import { asProposalDetails, DetailsFragment, FundingRequestDetails } from '@/pro export interface PastCouncil { id: string endedAt: Block + electionCycleId: number | undefined } export interface PastCouncilWithDetails extends PastCouncil { @@ -28,6 +29,7 @@ export interface PastCouncilWithDetails extends PastCouncil { export const asPastCouncil = (fields: PastCouncilFieldsFragment): PastCouncil => ({ id: fields.id, + electionCycleId: fields.councilElections[0]?.cycleId, endedAt: asBlock({ createdAt: fields.endedAtTime, inBlock: fields.endedAtBlock ?? -1, diff --git a/packages/ui/src/council/types/PastElection.ts b/packages/ui/src/council/types/PastElection.ts index c39c134f3a..2a1d55ca0a 100644 --- a/packages/ui/src/council/types/PastElection.ts +++ b/packages/ui/src/council/types/PastElection.ts @@ -21,22 +21,29 @@ export interface PastElection { revealedVotes: number totalVotes: number totalVoteStake: BN + result: 'successful' | 'failed' + totalRevealedVoteStake: BN } export interface PastElectionWithDetails extends PastElection { votingResults: ElectionVotingResult[] } -export const asPastElection = (fields: PastElectionRoundFieldsFragment): PastElection => ({ - id: fields.id, - cycleId: fields.cycleId, - finishedAtBlock: maybeAsBlock(fields.endedAtBlock, fields.endedAtTime, fields.endedAtNetwork), - totalCandidatesStake: sumStakes(fields.candidates), - totalCandidates: fields.candidates.length, - revealedVotes: fields.castVotes.filter((castVote) => castVote.voteForId).length, - totalVotes: fields.castVotes.length, - totalVoteStake: sumStakes(fields.castVotes), -}) +export const asPastElection = (fields: PastElectionRoundFieldsFragment): PastElection => { + const revealedVotesArray = fields.castVotes.filter((castVote) => castVote.voteForId) + return { + id: fields.id, + cycleId: fields.cycleId, + finishedAtBlock: maybeAsBlock(fields.endedAtBlock, fields.endedAtTime, fields.endedAtNetwork), + totalCandidatesStake: sumStakes(fields.candidates), + totalCandidates: fields.candidates.length, + revealedVotes: revealedVotesArray.length, + totalVotes: fields.castVotes.length, + totalVoteStake: sumStakes(fields.castVotes), + result: fields.nextElectedCouncil ? 'successful' : 'failed', + totalRevealedVoteStake: sumStakes(revealedVotesArray), + } +} export const asPastElectionWithDetails = ( fields: PastElectionRoundDetailedFieldsFragment diff --git a/packages/ui/src/overview/queries/__generated__/overview.generated.tsx b/packages/ui/src/overview/queries/__generated__/overview.generated.tsx index 3ece7d3c29..b4c060eb5f 100644 --- a/packages/ui/src/overview/queries/__generated__/overview.generated.tsx +++ b/packages/ui/src/overview/queries/__generated__/overview.generated.tsx @@ -103,6 +103,7 @@ export type GetAllDeadLinesQuery = { } votesReceived: Array<{ __typename: 'CastVote'; id: string }> }> + castVotes: Array<{ __typename: 'CastVote'; voteForId?: string | null; stake: string }> }> proposals: Array<{ __typename: 'Proposal'; updatedAt?: any | null; id: string; title: string }> upcomingWorkingGroupOpenings?: Array<{ @@ -248,6 +249,10 @@ export const GetAllDeadLinesDocument = gql` candidates { ...ElectionCandidateFields } + castVotes { + voteForId + stake + } } proposals( where: { diff --git a/packages/ui/src/overview/queries/overview.graphql b/packages/ui/src/overview/queries/overview.graphql index c82cd56776..cd58f5d292 100644 --- a/packages/ui/src/overview/queries/overview.graphql +++ b/packages/ui/src/overview/queries/overview.graphql @@ -52,6 +52,10 @@ query GetAllDeadLines($proposalCreator: MembershipWhereInput, $group: WorkingGro candidates { ...ElectionCandidateFields } + castVotes { + voteForId + stake + } } proposals( where: { creator: $proposalCreator, isFinalized_eq: false, status_json: { isTypeOf_eq: "ProposalStatusDeciding" } } From 7b4ffd197a168dd9300f18a72df258014162b19a Mon Sep 17 00:00:00 2001 From: Leszek Wiesner Date: Wed, 2 Apr 2025 11:59:24 +0200 Subject: [PATCH 03/67] =?UTF-8?q?=F0=9F=9A=80=20Release=20`3.7.3`=20(#4868?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace Subscan with Statescan (#4867) * Replace Subscan with Statescan * Fix formatting * Fix failing playwright install in CI checks * Bump version to 3.7.3 --------- Co-authored-by: Theophile Sandoz --- CHANGELOG.md | 10 ++++++++-- packages/ui/package.json | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb9767530d..ba15f3e4da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.7.3] - 2025-04-01 + +### Fixed +- Replace joystream.subscan.io links with explorer.joystream.org. + ## [3.7.2] - 2024-08-10 ### Fixed @@ -412,8 +417,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.1] - 2022-12-02 -[unreleased]: https://github.com/Joystream/pioneer/compare/v3.7.2...HEAD -[3.8.0]: https://github.com/Joystream/pioneer/compare/v3.7.1...v3.7.2 +[unreleased]: https://github.com/Joystream/pioneer/compare/v3.7.3...HEAD +[3.7.3]: https://github.com/Joystream/pioneer/compare/v3.7.2...v3.7.3 +[3.7.2]: https://github.com/Joystream/pioneer/compare/v3.7.1...v3.7.2 [3.7.1]: https://github.com/Joystream/pioneer/compare/v3.7.0...v3.7.1 [3.7.0]: https://github.com/Joystream/pioneer/compare/v3.6.0...v3.7.0 [3.6.0]: https://github.com/Joystream/pioneer/compare/v3.5.2...v3.6.0 diff --git a/packages/ui/package.json b/packages/ui/package.json index d48c3b9986..6927e881d4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@joystream/pioneer", - "version": "3.7.2", + "version": "3.7.3", "license": "GPL-3.0-only", "scripts": { "build": "node --max_old_space_size=4096 ./build.js", From c6865f71058becd1771c85bcdebb92cc4298f2af Mon Sep 17 00:00:00 2001 From: Leszek Wiesner Date: Tue, 15 Apr 2025 11:20:02 +0200 Subject: [PATCH 04/67] =?UTF-8?q?=F0=9F=9A=80=20Release=20`3.8.0`=20(#4870?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace Subscan with Statescan (#4867) * Replace Subscan with Statescan * Fix formatting * Fix failing playwright install in CI checks * Council and election pages: Small enhancements - Issue 4866 (#4869) * react snap setup * react snap setup * react snap setup * seo * council and past council page * Add term start and end (estimated) dates for current term * total number of votes & total voting stake during Voting and Revealing period * Add Election Result and Revealed votes statistics * added past councils route and candidate vote highlight * latest election fields update * undo react-snap * removed uneccesary package update * removed uneccesary package update * chore: remove yarn.lock from tracked files * git error fix * chore: revert yarn.lock to match main * removed eslint comment Co-authored-by: Leszek Wiesner * Apply suggestions from code review Co-authored-by: Leszek Wiesner * Update packages/ui/src/index.html Co-authored-by: Leszek Wiesner * Update packages/ui/src/council/components/pastCouncil/PastCouncilsList/PastCouncilListItem.tsx Co-authored-by: Leszek Wiesner * Update packages/ui/src/council/components/election/pastElection/PastElectionStats.tsx Co-authored-by: Leszek Wiesner * estimated ends at * council id * rearranged mock * review changes * review changes * test error fix --------- Co-authored-by: Leszek Wiesner * Bump version, update CHANGELOG --------- Co-authored-by: Theophile Sandoz Co-authored-by: Victor Emmanuel <33874323+vrrayz@users.noreply.github.com> --- CHANGELOG.md | 16 +++++++++++++++- packages/ui/package.json | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba15f3e4da..0ee61efdc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.8.0] - 2025-04-15 + +### Added +- "Started at" and "Estimated end" dates & blocks on council page, +- Current term number on council page, +- Voting statistics on current election page (votes, revealed votes, stake, revealed stake), +- Election result on past election page, +- Election result on pase elections listing, +- Stake vs revealed stake stats on past election page. + +### Fixed +- Made council term number on past council page and past council listing human-readable. + ## [3.7.3] - 2025-04-01 ### Fixed @@ -417,7 +430,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.1] - 2022-12-02 -[unreleased]: https://github.com/Joystream/pioneer/compare/v3.7.3...HEAD +[unreleased]: https://github.com/Joystream/pioneer/compare/v3.8.0...HEAD +[3.8.0]: https://github.com/Joystream/pioneer/compare/v3.7.3...v3.8.0 [3.7.3]: https://github.com/Joystream/pioneer/compare/v3.7.2...v3.7.3 [3.7.2]: https://github.com/Joystream/pioneer/compare/v3.7.1...v3.7.2 [3.7.1]: https://github.com/Joystream/pioneer/compare/v3.7.0...v3.7.1 diff --git a/packages/ui/package.json b/packages/ui/package.json index 6927e881d4..3403162c43 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@joystream/pioneer", - "version": "3.7.3", + "version": "3.8.0", "license": "GPL-3.0-only", "scripts": { "build": "node --max_old_space_size=4096 ./build.js", From 53bc361fa69c989f048e44bb8e79a06814a8fbe1 Mon Sep 17 00:00:00 2001 From: Lezek123 Date: Wed, 21 May 2025 09:22:16 +0200 Subject: [PATCH 05/67] Update mainnet RPC endpoint (#4871) --- docs/admin.md | 2 +- packages/ui/.env.example | 4 ++-- packages/ui/README.md | 2 +- packages/ui/dev/helpers/apiBenchmarking.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/admin.md b/docs/admin.md index caceff1fc7..9ee9a948a1 100644 --- a/docs/admin.md +++ b/docs/admin.md @@ -19,7 +19,7 @@ To deploy Pioneer on Vercel click on the button bellow: For example, for the Joystream mainnet: ```shell - REACT_APP_MAINNET_NODE_SOCKET=wss://rpc.joystream.org:9944 + REACT_APP_MAINNET_NODE_SOCKET=wss://rpc.joystream.org REACT_APP_MAINNET_QUERY_NODE=https://query.joystream.org/graphql REACT_APP_MAINNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql ``` diff --git a/packages/ui/.env.example b/packages/ui/.env.example index dc473fbda6..c13775e0a3 100644 --- a/packages/ui/.env.example +++ b/packages/ui/.env.example @@ -1,5 +1,5 @@ # TESTNET Endpoints -REACT_APP_TESTNET_NODE_SOCKET=wss://rpc.joystream.org:9944 +REACT_APP_TESTNET_NODE_SOCKET=wss://rpc.joystream.org REACT_APP_TESTNET_NODE_HTTP_RPC=https://rpc.joystream.org REACT_APP_TESTNET_QUERY_NODE=https://query.joystream.org/graphql REACT_APP_TESTNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql @@ -7,7 +7,7 @@ REACT_APP_TESTNET_MEMBERSHIP_FAUCET_URL=https://faucet.joystream.org/member-fauc REACT_APP_TESTNET_BACKEND=http://localhost:3000 # MAINNET Endpoints -REACT_APP_MAINNET_NODE_SOCKET=wss://rpc.joystream.org:9944 +REACT_APP_MAINNET_NODE_SOCKET=wss://rpc.joystream.org REACT_APP_MAINNET_NODE_HTTP_RPC=https://rpc.joystream.org REACT_APP_MAINNET_QUERY_NODE=https://query.joystream.org/graphql REACT_APP_MAINNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql diff --git a/packages/ui/README.md b/packages/ui/README.md index b0b8ef1d8f..2da4be6821 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -339,7 +339,7 @@ In case there is the network you wish to connect to has no JSON configuration (o To use custom addresses add the `.env` file in `packages/ui` (example: `packages/ui/.env.example`) and set -1. `REACT_APP_MAINNET_NODE_SOCKET` example `wss://rpc.joystream.org:9944` +1. `REACT_APP_MAINNET_NODE_SOCKET` example `wss://rpc.joystream.org` 2. `REACT_APP_MAINNET_QUERY_NODE` example `https://query.joystream.org/graphql` 3. `REACT_APP_MAINNET_QUERY_NODE_SOCKET` example `wss://query.joystream.org/graphql` 4. `REACT_APP_MAINNET_MEMBERSHIP_FAUCET_URL` example `https://faucet.joystream.org/member-faucet/register` diff --git a/packages/ui/dev/helpers/apiBenchmarking.ts b/packages/ui/dev/helpers/apiBenchmarking.ts index d915805146..0c976feafe 100644 --- a/packages/ui/dev/helpers/apiBenchmarking.ts +++ b/packages/ui/dev/helpers/apiBenchmarking.ts @@ -7,7 +7,7 @@ import { benchmark } from '../../src/common/utils/benchmark' import { ALICE } from '../node-mocks/data/addresses' import { withPromiseApi, withRxApi } from '../node-mocks/lib/api' -const ENDPOINT = 'wss://rpc.joystream.org:9944' // TODO pass as a parameter +const ENDPOINT = 'wss://rpc.joystream.org' // TODO pass as a parameter const DEFAULT_DURATION = 5_000 export const apiBenchmarking = { From 4883e2d33d66ed62cc9cbe733f683871352599e6 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Tue, 21 Oct 2025 03:30:34 +0200 Subject: [PATCH 06/67] Show 50 validators per page (#4877) --- .../ui/src/app/pages/Validators/ValidatorList.stories.tsx | 4 ++-- packages/ui/src/validators/hooks/useValidatorsList.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx index ba5c1a66d6..1228913226 100644 --- a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx +++ b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx @@ -172,12 +172,12 @@ export const TestsFilters: Story = { await selectFromDropdown(screen, stateFilter, 'active') await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(3)) await userEvent.click(screen.getByText('Clear all filters')) - await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)) + //await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)) await userEvent.type(searchElement, 'alice{enter}') await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(2)) expect(screen.queryByText('Clear all filters')) await userEvent.click(screen.getByText('Clear all filters')) - await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)) + //await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)) }) await step('Sort', async () => { diff --git a/packages/ui/src/validators/hooks/useValidatorsList.tsx b/packages/ui/src/validators/hooks/useValidatorsList.tsx index 8a5ae7b717..85595e8f7d 100644 --- a/packages/ui/src/validators/hooks/useValidatorsList.tsx +++ b/packages/ui/src/validators/hooks/useValidatorsList.tsx @@ -3,7 +3,7 @@ import { useContext, useEffect, useMemo, useReducer, useState } from 'react' import { ValidatorsContext } from '../providers/context' import { ValidatorDetailsOrder } from '../types' -const VALIDATOR_PER_PAGE = 7 +const VALIDATOR_PER_PAGE = 50 const DESCENDING_KEYS: ValidatorDetailsOrder['key'][] = ['apr'] export const useValidatorsList = () => { From 2ab7ca97028d3e1f1ca701e41d67fa2aa434da7d Mon Sep 17 00:00:00 2001 From: goldstarhigher Date: Thu, 30 Oct 2025 16:46:44 -0500 Subject: [PATCH 07/67] Update balance tooltip position (#4884) --- .../ui/src/common/components/typography/TokenValue.tsx | 5 +++-- .../ui/src/memberships/components/ProfileComponent.tsx | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/common/components/typography/TokenValue.tsx b/packages/ui/src/common/components/typography/TokenValue.tsx index c1a4cbe8c8..071dd8859d 100644 --- a/packages/ui/src/common/components/typography/TokenValue.tsx +++ b/packages/ui/src/common/components/typography/TokenValue.tsx @@ -18,9 +18,10 @@ interface ValueProps extends ValueSizingProps { className?: string isLoading?: boolean mjoy?: boolean + placement?: 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' } -export const TokenValue = React.memo(({ className, value, size, isLoading, mjoy }: ValueProps) => { +export const TokenValue = React.memo(({ className, value, size, isLoading, mjoy, placement }: ValueProps) => { if (isLoading) { return } @@ -29,7 +30,7 @@ export const TokenValue = React.memo(({ className, value, size, isLoading, mjoy return - } return ( - {formatJoyValue(value)}}> + {formatJoyValue(value)}} placement={placement}> {mjoy ? ( {formatJoyValue(value.divn(Math.pow(10, 6)), { precision: 2 })} diff --git a/packages/ui/src/memberships/components/ProfileComponent.tsx b/packages/ui/src/memberships/components/ProfileComponent.tsx index 19fe899c6b..22d8f0a50d 100644 --- a/packages/ui/src/memberships/components/ProfileComponent.tsx +++ b/packages/ui/src/memberships/components/ProfileComponent.tsx @@ -29,9 +29,13 @@ export function ProfileComponent() { - {isBalanceHidden ? ***** : } + {isBalanceHidden ? ( + ***** + ) : ( + + )} From 63d198ad955c1716a7c1cbbfdc5cd412bbea3da3 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Sat, 1 Nov 2025 06:02:09 +0100 Subject: [PATCH 08/67] Refresh the Election page after Restore Votes is clicked (#4885) --- packages/ui/src/common/hooks/useLocalStorage.ts | 11 ++++++++++- .../VoteForCouncil/VoteForCouncilSuccessModal.tsx | 5 ----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/common/hooks/useLocalStorage.ts b/packages/ui/src/common/hooks/useLocalStorage.ts index 20b33e85be..919e16164a 100644 --- a/packages/ui/src/common/hooks/useLocalStorage.ts +++ b/packages/ui/src/common/hooks/useLocalStorage.ts @@ -41,11 +41,20 @@ export const useLocalStorage = (key?: string) => { setState(getItem(key)) }, [key]) + useEffect(() => { + const handleEventOnce = () => setState(getItem(key)) + + document.addEventListener(`storage_event_${key}`, handleEventOnce) + return () => { + document.removeEventListener(`storage_event_${key}`, handleEventOnce) + } + }, [key]) + const dispatch = useCallback( (setStateAction: T | ((prevState?: T) => T)) => { const value = isFunction(setStateAction) ? setStateAction(getItem(key)) : setStateAction - setState(value) setItem(key, value) + document.dispatchEvent(new CustomEvent(`storage_event_${key}`, {})) }, [key] ) diff --git a/packages/ui/src/council/modals/VoteForCouncil/VoteForCouncilSuccessModal.tsx b/packages/ui/src/council/modals/VoteForCouncil/VoteForCouncilSuccessModal.tsx index 34fb30bdcd..d77ce4d727 100644 --- a/packages/ui/src/council/modals/VoteForCouncil/VoteForCouncilSuccessModal.tsx +++ b/packages/ui/src/council/modals/VoteForCouncil/VoteForCouncilSuccessModal.tsx @@ -1,13 +1,10 @@ import React, { useCallback } from 'react' -import { generatePath } from 'react-router' -import { useHistory } from 'react-router-dom' import { ButtonGhost } from '@/common/components/buttons' import { SuccessIcon } from '@/common/components/icons' import { Modal, ModalFooter, ModalHeader, SuccessModalBody } from '@/common/components/Modal' import { TextMedium } from '@/common/components/typography' import { BackupVotesButton } from '@/council/components/election/BackupVotesButton' -import { ElectionRoutes } from '@/council/constants' import { useCandidate } from '@/council/hooks/useCandidate' import { SelectedMember } from '@/memberships/components/SelectMember' @@ -17,11 +14,9 @@ interface Props { } export const VoteForCouncilSuccessModal = ({ onClose, candidateId }: Props) => { - const history = useHistory() const { candidate } = useCandidate(candidateId) const goToElection = useCallback(() => { - history.push(generatePath(ElectionRoutes.currentElection)) onClose() }, [onClose]) From 135f14f7664f191bbfebdcd44186d2b4386f77df Mon Sep 17 00:00:00 2001 From: goldstarhigher Date: Thu, 30 Oct 2025 16:22:30 -0500 Subject: [PATCH 09/67] Update MyRole tooltip (#4883) --- packages/ui/src/app/pages/WorkingGroups/MyRoles/MyRole.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/app/pages/WorkingGroups/MyRoles/MyRole.tsx b/packages/ui/src/app/pages/WorkingGroups/MyRoles/MyRole.tsx index b8286e4f3c..cff0709a07 100644 --- a/packages/ui/src/app/pages/WorkingGroups/MyRoles/MyRole.tsx +++ b/packages/ui/src/app/pages/WorkingGroups/MyRoles/MyRole.tsx @@ -127,7 +127,10 @@ export const MyRole = () => { {isActive && isOwn && ( Leave this position - + From f068abdf0cba1e7c0367a38bf04e1ba9229aa112 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Fri, 31 Oct 2025 04:49:10 +0100 Subject: [PATCH 10/67] Move action buttons to the left (#4886) --- .../ui/src/forum/components/PostList/PostListItem.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/ui/src/forum/components/PostList/PostListItem.tsx b/packages/ui/src/forum/components/PostList/PostListItem.tsx index 926ab01fc9..35dd40dc90 100644 --- a/packages/ui/src/forum/components/PostList/PostListItem.tsx +++ b/packages/ui/src/forum/components/PostList/PostListItem.tsx @@ -263,14 +263,6 @@ export const ForumPostRow = styled.div` ${ForumPostAuthor}, ${ButtonsGroup}, ${BlockTimeWrapper} { flex: 50%; } - - ${ForumPostAuthor}, ${ButtonsGroup}:first-of-type { - justify-content: flex-start; - } - - ${BlockTimeWrapper}, ${ButtonsGroup}:last-of-type { - justify-content: flex-end; - } ` const ForumPostHeader = styled(ForumPostRow)` From 85d019aacba28ad07d1665d43d961612a1bdf91f Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Sat, 1 Nov 2025 07:13:07 +0100 Subject: [PATCH 11/67] Add Latest posts to the forum (#4881) --- .../components/CategoryCard/CategoryCard.tsx | 34 ++-- .../forum/components/PostCard/PostCard.tsx | 89 +++++++++ .../ui/src/forum/components/PostCard/index.ts | 1 + .../components/ThreadCard/ThreadCard.tsx | 6 +- .../forum/components/category/ForumMain.tsx | 28 ++- .../ui/src/forum/hooks/useLatestForumPosts.ts | 55 ++++++ .../queries/__generated__/forum.generated.tsx | 183 ++++++++++++++++++ packages/ui/src/forum/queries/forum.graphql | 25 +++ 8 files changed, 404 insertions(+), 17 deletions(-) create mode 100644 packages/ui/src/forum/components/PostCard/PostCard.tsx create mode 100644 packages/ui/src/forum/components/PostCard/index.ts create mode 100644 packages/ui/src/forum/hooks/useLatestForumPosts.ts diff --git a/packages/ui/src/forum/components/CategoryCard/CategoryCard.tsx b/packages/ui/src/forum/components/CategoryCard/CategoryCard.tsx index b73dbd07db..1cdce189d7 100644 --- a/packages/ui/src/forum/components/CategoryCard/CategoryCard.tsx +++ b/packages/ui/src/forum/components/CategoryCard/CategoryCard.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import { generatePath, Link } from 'react-router-dom' +import { generatePath, Link, useHistory } from 'react-router-dom' import styled, { css } from 'styled-components' import { BadgeStatusCss } from '@/common/components/BadgeStatus' @@ -18,25 +18,33 @@ export interface CategoryCardProps { } export const CategoryCard = ({ className, category, archivedStyles }: CategoryCardProps) => { + const history = useHistory() + const hoverComponent = useMemo(() => { + const handleSubcategoryClick = (e: React.MouseEvent, subcategoryId: string) => { + e.preventDefault() + e.stopPropagation() + history.push(generatePath(ForumRoutes.category, { id: subcategoryId })) + } + return ( {category.subcategories.length && category.subcategories.map((subcategory) => ( - + handleSubcategoryClick(e, subcategory.id)}> {subcategory.title} ))} ) - }, [category.subcategories.length]) + }, [category.subcategories, history]) return (
@@ -56,11 +64,15 @@ export const CategoryCard = ({ className, category, archivedStyles }: CategoryCa ) } -const StyledBadge = styled(Link)` +const StyledBadge = styled.button` ${BadgeStatusCss} + border: none; + background: none; + cursor: pointer; + padding: 0; ` -const Box = styled(Link)<{ archivedStyles?: boolean; ignoreHover?: boolean }>` +const Box = styled(Link)<{ $archivedStyles?: boolean; $ignoreHover?: boolean }>` display: flex; column-gap: 15px; border: 1px solid ${Colors.Black[100]}; @@ -114,8 +126,8 @@ const Box = styled(Link)<{ archivedStyles?: boolean; ignoreHover?: boolean }>` } } - ${({ archivedStyles }) => - archivedStyles && + ${({ $archivedStyles }) => + $archivedStyles && css` background-color: ${Colors.Black[50]}; `} @@ -124,8 +136,8 @@ const Box = styled(Link)<{ archivedStyles?: boolean; ignoreHover?: boolean }>` display: none; } - ${({ ignoreHover }) => - !ignoreHover && + ${({ $ignoreHover }) => + !$ignoreHover && css` :hover { .category-subcategories { diff --git a/packages/ui/src/forum/components/PostCard/PostCard.tsx b/packages/ui/src/forum/components/PostCard/PostCard.tsx new file mode 100644 index 0000000000..8b4114e1a1 --- /dev/null +++ b/packages/ui/src/forum/components/PostCard/PostCard.tsx @@ -0,0 +1,89 @@ +import React from 'react' +import { generatePath } from 'react-router' +import styled from 'styled-components' + +import { BadgeStatus } from '@/common/components/BadgeStatus' +import { ColumnGapBlock } from '@/common/components/page/PageContent' +import { GhostRouterLink } from '@/common/components/RouterLink' +import { TextBig, TextExtraSmall, TextMedium } from '@/common/components/typography' +import { BorderRad, Colors } from '@/common/constants' +import { relativeIfRecent } from '@/common/model/relativeIfRecent' +import { ForumRoutes } from '@/forum/constant' +import { ForumPostWithThread } from '@/forum/hooks/useLatestForumPosts' +import { MemberInfo } from '@/memberships/components' + +interface PostCardProps { + post: ForumPostWithThread + className?: string +} + +export const PostCard = ({ post, className }: PostCardProps) => { + return ( + +
+ +
+ + {relativeIfRecent(post.updatedAt ?? post.createdAt)} + + {post.thread.categoryTitle.toUpperCase()} +
+
+ + {post.thread.title} + + + {post.text} + + +
+ ) +} + +const Box = styled(GhostRouterLink)` + display: grid; + row-gap: 16px; + border: 1px solid ${Colors.Black[100]}; + border-radius: ${BorderRad.s}; + padding: 24px; + cursor: pointer; + + :hover { + border: 1px solid ${Colors.Blue[100]}; + } + + > *:nth-child(3) { + margin-top: -14px; + } + + > *:first-child { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + align-items: center; + gap: 5px; + + > * { + flex: 1; + } + + > *:last-child { + display: flex; + flex-direction: column-reverse; + align-items: flex-end; + justify-content: end; + gap: 5px; + } + } + + > *:last-child { + width: auto; + svg { + color: ${Colors.Black[400]}; + } + } + + ${TextMedium} { + max-height: 55px; + } +` diff --git a/packages/ui/src/forum/components/PostCard/index.ts b/packages/ui/src/forum/components/PostCard/index.ts new file mode 100644 index 0000000000..6ca7f3ba79 --- /dev/null +++ b/packages/ui/src/forum/components/PostCard/index.ts @@ -0,0 +1 @@ +export * from './PostCard' diff --git a/packages/ui/src/forum/components/ThreadCard/ThreadCard.tsx b/packages/ui/src/forum/components/ThreadCard/ThreadCard.tsx index 949e47e428..bab90174cf 100644 --- a/packages/ui/src/forum/components/ThreadCard/ThreadCard.tsx +++ b/packages/ui/src/forum/components/ThreadCard/ThreadCard.tsx @@ -27,7 +27,7 @@ export const ThreadCard = ({ thread, className, watchlistButton }: ThreadCardPro
@@ -70,8 +70,8 @@ const ThreadCardFooter = styled.div` } ` -const Box = styled(GhostRouterLink)<{ isArchived: boolean }>` - ${({ isArchived }) => (isArchived ? `background-color: ${Colors.Black[50]}` : '')}; +const Box = styled(GhostRouterLink)<{ $isArchived: boolean }>` + ${({ $isArchived }) => ($isArchived ? `background-color: ${Colors.Black[50]}` : '')}; display: grid; row-gap: 16px; border: 1px solid ${Colors.Black[100]}; diff --git a/packages/ui/src/forum/components/category/ForumMain.tsx b/packages/ui/src/forum/components/category/ForumMain.tsx index 21d6213188..728837a904 100644 --- a/packages/ui/src/forum/components/category/ForumMain.tsx +++ b/packages/ui/src/forum/components/category/ForumMain.tsx @@ -9,22 +9,25 @@ import { TextBig, TextMedium } from '@/common/components/typography' import { useRefetchQueries } from '@/common/hooks/useRefetchQueries' import { MILLISECONDS_PER_BLOCK } from '@/common/model/formatters' import { CategoryCard } from '@/forum/components/CategoryCard/CategoryCard' +import { PostCard } from '@/forum/components/PostCard/PostCard' import { ThreadCard } from '@/forum/components/ThreadCard/ThreadCard' import { ThreadCardSkeleton } from '@/forum/components/ThreadCard/ThreadCardSkeleton' import { useForumCategories } from '@/forum/hooks/useForumCategories' +import { useLatestForumPosts } from '@/forum/hooks/useLatestForumPosts' import { useLatestForumThreads } from '@/forum/hooks/useLatestForumThreads' export const ForumMain = () => { const { isLoading: isLoadingCategories, forumCategories } = useForumCategories({ isRoot: true }) const isRefetched = useRefetchQueries({ interval: MILLISECONDS_PER_BLOCK, include: ['GetForumCategories'] }) const { threads, isLoading: isLoadingThreads } = useLatestForumThreads(10) - const isLoading = isLoadingCategories || isLoadingThreads + const { posts, isLoading: isLoadingPosts } = useLatestForumPosts(10) + const isLoading = isLoadingCategories || isLoadingThreads || isLoadingPosts if (isLoading && !isRefetched) { return } - if (!forumCategories?.length && !threads.length) { + if (!forumCategories?.length && !threads.length && !posts.length) { return } @@ -35,7 +38,7 @@ export const ForumMain = () => { title="Latest threads" items={ threads.length ? ( - threads.map((thread) => ) + threads.map((thread) => ) ) : ( There are not latest threads ) @@ -45,6 +48,21 @@ export const ForumMain = () => { } /> )} + {!isLoadingPosts ? ( + ) + ) : ( + There are no latest posts + ) + } + /> + ) : ( + } /> + )} + {forumCategories?.length ? ( @@ -83,3 +101,7 @@ const StyledThreadCard = styled(ThreadCard)` min-width: 288px; } ` + +const StyledPostCard = styled(PostCard)` + min-width: 330px; +` diff --git a/packages/ui/src/forum/hooks/useLatestForumPosts.ts b/packages/ui/src/forum/hooks/useLatestForumPosts.ts new file mode 100644 index 0000000000..a3337aa062 --- /dev/null +++ b/packages/ui/src/forum/hooks/useLatestForumPosts.ts @@ -0,0 +1,55 @@ +import { useMemo } from 'react' + +import { ForumPostOrderByInput } from '@/common/api/queries' +import { useGetLatestForumPostsQuery } from '@/forum/queries' +import { asMember, Member } from '@/memberships/types' + +export interface ForumPostWithThread { + id: string + createdAt: string + updatedAt?: string + author: Member + text: string + threadId: string + thread: { + id: string + title: string + categoryId: string + categoryTitle: string + } +} + +export const useLatestForumPosts = (limit: number) => { + const { data, loading } = useGetLatestForumPostsQuery({ + variables: { + orderBy: [ForumPostOrderByInput.UpdatedAtDesc], + limit, + where: { + status_json: { + isTypeOf_not: 'PostStatusRemoved', + }, + }, + }, + }) + + const posts = useMemo( + () => + data?.forumPosts.map((post) => ({ + id: post.id, + createdAt: post.createdAt, + updatedAt: post.updatedAt, + author: asMember(post.author), + text: post.text, + threadId: post.threadId, + thread: { + id: post.thread.id, + title: post.thread.title, + categoryId: post.thread.categoryId, + categoryTitle: post.thread.category.title, + }, + })) ?? [], + [data, loading] + ) + + return { posts, isLoading: loading } +} diff --git a/packages/ui/src/forum/queries/__generated__/forum.generated.tsx b/packages/ui/src/forum/queries/__generated__/forum.generated.tsx index 2e99b18e53..bfe5a8505b 100644 --- a/packages/ui/src/forum/queries/__generated__/forum.generated.tsx +++ b/packages/ui/src/forum/queries/__generated__/forum.generated.tsx @@ -1533,6 +1533,117 @@ export type GetForumThreadMentionQuery = { } | null } +export type ForumPostWithThreadFieldsFragment = { + __typename: 'ForumPost' + id: string + createdAt: any + updatedAt?: any | null + text: string + threadId: string + author: { + __typename: 'Membership' + id: string + rootAccount: string + controllerAccount: string + boundAccounts: Array + handle: string + isVerified: boolean + isFoundingMember: boolean + isCouncilMember: boolean + inviteCount: number + createdAt: any + metadata: { + __typename: 'MemberMetadata' + name?: string | null + about?: string | null + isVerifiedValidator?: boolean | null + avatar?: { __typename: 'AvatarObject' } | { __typename: 'AvatarUri'; avatarUri: string } | null + } + roles: Array<{ + __typename: 'Worker' + id: string + createdAt: any + isLead: boolean + isActive: boolean + group: { __typename: 'WorkingGroup'; name: string } + }> + stakingaccountaddedeventmember?: Array<{ + __typename: 'StakingAccountAddedEvent' + createdAt: any + inBlock: number + network: Types.Network + account: string + }> | null + } + thread: { + __typename: 'ForumThread' + id: string + title: string + categoryId: string + category: { __typename: 'ForumCategory'; title: string } + } +} + +export type GetLatestForumPostsQueryVariables = Types.Exact<{ + where: Types.ForumPostWhereInput + orderBy?: Types.InputMaybe | Types.ForumPostOrderByInput> + limit?: Types.InputMaybe +}> + +export type GetLatestForumPostsQuery = { + __typename: 'Query' + forumPosts: Array<{ + __typename: 'ForumPost' + id: string + createdAt: any + updatedAt?: any | null + text: string + threadId: string + author: { + __typename: 'Membership' + id: string + rootAccount: string + controllerAccount: string + boundAccounts: Array + handle: string + isVerified: boolean + isFoundingMember: boolean + isCouncilMember: boolean + inviteCount: number + createdAt: any + metadata: { + __typename: 'MemberMetadata' + name?: string | null + about?: string | null + isVerifiedValidator?: boolean | null + avatar?: { __typename: 'AvatarObject' } | { __typename: 'AvatarUri'; avatarUri: string } | null + } + roles: Array<{ + __typename: 'Worker' + id: string + createdAt: any + isLead: boolean + isActive: boolean + group: { __typename: 'WorkingGroup'; name: string } + }> + stakingaccountaddedeventmember?: Array<{ + __typename: 'StakingAccountAddedEvent' + createdAt: any + inBlock: number + network: Types.Network + account: string + }> | null + } + thread: { + __typename: 'ForumThread' + id: string + title: string + categoryId: string + category: { __typename: 'ForumCategory'; title: string } + } + }> +} + export const ForumBaseCategoryFieldsFragmentDoc = gql` fragment ForumBaseCategoryFields on ForumCategory { id @@ -1769,6 +1880,27 @@ export const ForumPostMentionFieldsFragmentDoc = gql` } ${MemberFieldsFragmentDoc} ` +export const ForumPostWithThreadFieldsFragmentDoc = gql` + fragment ForumPostWithThreadFields on ForumPost { + id + createdAt + updatedAt + author { + ...MemberFields + } + text + threadId + thread { + id + title + categoryId + category { + title + } + } + } + ${MemberFieldsFragmentDoc} +` export const GetForumCategoriesDocument = gql` query GetForumCategories( $where: ForumCategoryWhereInput @@ -2718,3 +2850,54 @@ export type GetForumThreadMentionQueryResult = Apollo.QueryResult< GetForumThreadMentionQuery, GetForumThreadMentionQueryVariables > +export const GetLatestForumPostsDocument = gql` + query GetLatestForumPosts($where: ForumPostWhereInput!, $orderBy: [ForumPostOrderByInput!], $limit: Int) { + forumPosts(where: $where, orderBy: $orderBy, limit: $limit) { + ...ForumPostWithThreadFields + } + } + ${ForumPostWithThreadFieldsFragmentDoc} +` + +/** + * __useGetLatestForumPostsQuery__ + * + * To run a query within a React component, call `useGetLatestForumPostsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetLatestForumPostsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetLatestForumPostsQuery({ + * variables: { + * where: // value for 'where' + * orderBy: // value for 'orderBy' + * limit: // value for 'limit' + * }, + * }); + */ +export function useGetLatestForumPostsQuery( + baseOptions: Apollo.QueryHookOptions +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useQuery( + GetLatestForumPostsDocument, + options + ) +} +export function useGetLatestForumPostsLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useLazyQuery( + GetLatestForumPostsDocument, + options + ) +} +export type GetLatestForumPostsQueryHookResult = ReturnType +export type GetLatestForumPostsLazyQueryHookResult = ReturnType +export type GetLatestForumPostsQueryResult = Apollo.QueryResult< + GetLatestForumPostsQuery, + GetLatestForumPostsQueryVariables +> diff --git a/packages/ui/src/forum/queries/forum.graphql b/packages/ui/src/forum/queries/forum.graphql index 84eec62286..67f7928dc4 100644 --- a/packages/ui/src/forum/queries/forum.graphql +++ b/packages/ui/src/forum/queries/forum.graphql @@ -350,3 +350,28 @@ query GetForumThreadMention($id: ID!) { ...ForumThreadMentionFields } } + +fragment ForumPostWithThreadFields on ForumPost { + id + createdAt + updatedAt + author { + ...MemberFields + } + text + threadId + thread { + id + title + categoryId + category { + title + } + } +} + +query GetLatestForumPosts($where: ForumPostWhereInput!, $orderBy: [ForumPostOrderByInput!], $limit: Int) { + forumPosts(where: $where, orderBy: $orderBy, limit: $limit) { + ...ForumPostWithThreadFields + } +} From 862202415140c5c83961524b3da3319fb3adf533 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Sat, 1 Nov 2025 07:21:04 +0100 Subject: [PATCH 12/67] Add MyThreads counter (#3929) Co-authored-by: Theophile Sandoz --- packages/ui/src/app/hooks/usePageTabs.ts | 2 +- packages/ui/src/app/pages/Forum/components/ForumTabs.tsx | 6 +++++- packages/ui/src/forum/hooks/useMyThreads.ts | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/app/hooks/usePageTabs.ts b/packages/ui/src/app/hooks/usePageTabs.ts index 49d2b9ba93..f2a7910db2 100644 --- a/packages/ui/src/app/hooks/usePageTabs.ts +++ b/packages/ui/src/app/hooks/usePageTabs.ts @@ -9,7 +9,7 @@ interface Options { hasChanges?: boolean } -export type TabsDefinition = readonly [string, Path] | [string, Path, number] | [string, Path, Options] +export type TabsDefinition = readonly [string, Path] | [string, Path, number | undefined] | [string, Path, Options] export const usePageTabs = (tabs: TabsDefinition[]) => { const history = useHistory() diff --git a/packages/ui/src/app/pages/Forum/components/ForumTabs.tsx b/packages/ui/src/app/pages/Forum/components/ForumTabs.tsx index fe9c2e784c..d336d9fcaf 100644 --- a/packages/ui/src/app/pages/Forum/components/ForumTabs.tsx +++ b/packages/ui/src/app/pages/Forum/components/ForumTabs.tsx @@ -3,11 +3,15 @@ import React from 'react' import { usePageTabs } from '@/app/hooks/usePageTabs' import { Tabs } from '@/common/components/Tabs' import { ForumRoutes } from '@/forum/constant' +import { useMyThreads, UseMyThreadsProps } from '@/forum/hooks/useMyThreads' + +const order = { orderKey: 'updatedAt', isDescending: true } export const ForumTabs = () => { + const { totalCount } = useMyThreads({ page: 1, order } as UseMyThreadsProps) const tabs = usePageTabs([ ['Forum', ForumRoutes.forum], - ['My Threads', ForumRoutes.myThreads], + ['My Threads', ForumRoutes.myThreads, totalCount], ['Watchlist', ForumRoutes.watchlist], ['Archived', ForumRoutes.archived], ]) diff --git a/packages/ui/src/forum/hooks/useMyThreads.ts b/packages/ui/src/forum/hooks/useMyThreads.ts index 28995f1df8..7b7043b17b 100644 --- a/packages/ui/src/forum/hooks/useMyThreads.ts +++ b/packages/ui/src/forum/hooks/useMyThreads.ts @@ -5,7 +5,7 @@ import { useGetForumThreadsCountQuery, useGetForumThreadsQuery } from '@/forum/q import { asForumThread, ForumThread } from '@/forum/types' import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' -interface UseMyThreadsProps { +export interface UseMyThreadsProps { page: number threadsPerPage?: number order: SortOrder @@ -38,7 +38,7 @@ export const useMyThreads = ({ page, threadsPerPage = 5, order }: UseMyThreadsPr variables: { where: variables.where }, }) - const totalCount = countData?.forumThreadsConnection.totalCount + const totalCount = countData?.forumThreadsConnection.totalCount || undefined return { isLoading: loadingPosts || loadingCount, From 20ed7cee974f6c7655e4cd76c79366a2a504ea2c Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Sat, 1 Nov 2025 07:22:46 +0100 Subject: [PATCH 13/67] Tooltip for deactivated `AddProposalButton` (#3909) Co-authored-by: Oleksandr Korniienko Co-authored-by: Theophile Sandoz --- .../src/common/components/Tooltip/Tooltip.tsx | 3 +- .../components/buttons/TransactionButton.tsx | 19 +++++++-- .../components/AddProposalButton.tsx | 42 ++++++++++++------- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/ui/src/common/components/Tooltip/Tooltip.tsx b/packages/ui/src/common/components/Tooltip/Tooltip.tsx index f34db56099..2e0f7ce665 100644 --- a/packages/ui/src/common/components/Tooltip/Tooltip.tsx +++ b/packages/ui/src/common/components/Tooltip/Tooltip.tsx @@ -38,6 +38,7 @@ export interface TooltipPopupProps extends TooltipContentProp { forBig?: boolean hideOnComponentLeave?: boolean boundaryClassName?: string + placement?: 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' } export interface DarkTooltipInnerItemProps { @@ -47,7 +48,6 @@ export interface DarkTooltipInnerItemProps { export const Tooltip = ({ absolute, maxWidth, - placement, children, tooltipText, tooltipOpen = false, @@ -60,6 +60,7 @@ export const Tooltip = ({ offset, hideOnComponentLeave, boundaryClassName, + placement = 'bottom-start', }: TooltipProps) => { const [isTooltipActive, setTooltipActive] = useState(tooltipOpen) const [referenceElementRef, setReferenceElementRef] = useState(null) diff --git a/packages/ui/src/common/components/buttons/TransactionButton.tsx b/packages/ui/src/common/components/buttons/TransactionButton.tsx index d331e13719..d6b4f6f42f 100644 --- a/packages/ui/src/common/components/buttons/TransactionButton.tsx +++ b/packages/ui/src/common/components/buttons/TransactionButton.tsx @@ -4,16 +4,17 @@ import { ReactElement } from 'react-markdown/lib/react-markdown' import { useResponsive } from '@/common/hooks/useResponsive' import { useTransactionStatus } from '@/common/hooks/useTransactionStatus' -import { Tooltip } from '../Tooltip' +import { Tooltip, TooltipContentProp } from '../Tooltip' import { ButtonGhost, ButtonPrimary, ButtonProps, ButtonSecondary } from '.' interface WrapperProps { children: ReactNode isResponsive?: boolean + tooltip?: TooltipContentProp } -export const TransactionButtonWrapper = ({ isResponsive, children }: WrapperProps) => { +export const TransactionButtonWrapper = ({ children, isResponsive, tooltip }: WrapperProps) => { const { isTransactionPending } = useTransactionStatus() const { size } = useResponsive() @@ -23,6 +24,14 @@ export const TransactionButtonWrapper = ({ isResponsive, children }: WrapperProp return {children} } + if (tooltip) { + return ( + + {children} + + ) + } + return <>{children} } @@ -30,16 +39,18 @@ type StyleOption = 'primary' | 'ghost' | 'secondary' interface TransactionButtonProps extends ButtonProps { style: StyleOption + disabled?: boolean isResponsive?: boolean + tooltip?: TooltipContentProp } -export const TransactionButton = ({ isResponsive, disabled, style, ...props }: TransactionButtonProps) => { +export const TransactionButton = ({ isResponsive, disabled, style, tooltip, ...props }: TransactionButtonProps) => { const { isTransactionPending } = useTransactionStatus() const Button = buttonTypes[style] return ( - +
+ ))} +
+ + {error && ( + + Error: {error} + + )} + + + Note: You can nominate up to 16 validators. Your nominations will take effect in the next + era. + - - Nominate Validator + + Cancel + + + {isLoading ? 'Nominating...' : 'Nominate Validators'} diff --git a/packages/ui/src/validators/modals/RebagModal/RebagModal.tsx b/packages/ui/src/validators/modals/RebagModal/RebagModal.tsx new file mode 100644 index 0000000000..4bb89aa03e --- /dev/null +++ b/packages/ui/src/validators/modals/RebagModal/RebagModal.tsx @@ -0,0 +1,138 @@ +import React, { useState, useEffect, useRef } from 'react' + +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { useApi } from '@/api/hooks/useApi' +import { ButtonPrimary, ButtonSecondary } from '@/common/components/buttons' +import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { SuccessModal } from '@/common/components/SuccessModal' +import { TextMedium, TextSmall } from '@/common/components/typography' +import { useModal } from '@/common/hooks/useModal' +import { Address } from '@/common/types' +import { useStakingQueries, useStakingTransactions } from '@/validators/hooks/useStakingSDK' +import { RebagModalCall } from '@/validators/modals/RebagModal/types' + +interface Props { + validatorAddress: Address +} + +export const RebagModal = () => { + const { modalData } = useModal() + const validatorAddress = modalData?.validatorAddress + + if (!validatorAddress) return null + + return +} + +const RebagModalInner = ({ validatorAddress }: Props) => { + const { hideModal } = useModal() + const { api } = useApi() + const { allAccounts } = useMyAccounts() + const { rebag, isConnected } = useStakingTransactions() + const { getStakingInfo } = useStakingQueries() + + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + const [stakingInfo, setStakingInfo] = useState(null) + const isMountedRef = useRef(true) + + useEffect(() => { + return () => { + isMountedRef.current = false + } + }, []) + + useEffect(() => { + const loadStakingInfo = async () => { + if (!allAccounts[0]?.address) return + + try { + const info = await getStakingInfo(allAccounts[0].address) + setStakingInfo(info) + } catch (err) { + setError('Failed to load staking info') + } + } + + loadStakingInfo() + }, [allAccounts]) + + const handleRebag = async () => { + if (!api || !isConnected) { + setError('API not connected') + return + } + + setIsLoading(true) + setError(null) + + try { + const rebagTx = rebag(validatorAddress) + await rebagTx.signAndSend(allAccounts[0]) + + if (isMountedRef.current) { + setSuccess(true) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Rebagging failed') + } finally { + setIsLoading(false) + } + } + + if (success) { + return ( + + ) + } + + return ( + + + + + Rebag your account in the validator bag system for better performance. + + + Account Address: {validatorAddress} + + + {stakingInfo && ( +
+ + Current Staking Info: + + Total Bonded: {stakingInfo.totalBonded} JOY + Active Bonded: {stakingInfo.activeBonded} JOY + Unbonding: {stakingInfo.unbonding} JOY +
+ )} + + {error && ( + + Error: {error} + + )} + + + Note: Rebagging optimizes your account's position in the validator bag system. This can + improve performance but may require additional fees. + +
+
+ + + Cancel + + + {isLoading ? 'Rebagging...' : 'Rebag Account'} + + +
+ ) +} diff --git a/packages/ui/src/validators/modals/RebagModal/index.ts b/packages/ui/src/validators/modals/RebagModal/index.ts new file mode 100644 index 0000000000..fe142ad8ba --- /dev/null +++ b/packages/ui/src/validators/modals/RebagModal/index.ts @@ -0,0 +1,2 @@ +export { RebagModal } from './RebagModal' +export type { RebagModalCall } from './types' diff --git a/packages/ui/src/validators/modals/RebagModal/types.ts b/packages/ui/src/validators/modals/RebagModal/types.ts new file mode 100644 index 0000000000..3ab4df905d --- /dev/null +++ b/packages/ui/src/validators/modals/RebagModal/types.ts @@ -0,0 +1,8 @@ +import { Address } from '@/common/types' + +export interface RebagModalCall { + modal: 'Rebag' + data: { + validatorAddress: Address + } +} diff --git a/packages/ui/src/validators/modals/RebondModal/RebondModal.tsx b/packages/ui/src/validators/modals/RebondModal/RebondModal.tsx new file mode 100644 index 0000000000..48cae29c80 --- /dev/null +++ b/packages/ui/src/validators/modals/RebondModal/RebondModal.tsx @@ -0,0 +1,183 @@ +import React, { useState, useEffect, useRef } from 'react' + +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { useApi } from '@/api/hooks/useApi' +import { ButtonPrimary, ButtonSecondary } from '@/common/components/buttons' +import { InputComponent, InputText } from '@/common/components/forms' +import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { SuccessModal } from '@/common/components/SuccessModal' +import { TextMedium, TextSmall } from '@/common/components/typography' +import { useModal } from '@/common/hooks/useModal' +import { joyStringToPlanckBigInt, planckToJoyString } from '@/common/model/joyValueFromString' +import { Address } from '@/common/types' +import { useStakingQueries, useStakingTransactions } from '@/validators/hooks/useStakingSDK' +import { RebondModalCall } from '@/validators/modals/RebondModal/types' + +interface Props { + validatorAddress: Address +} + +export const RebondModal = () => { + const { modalData } = useModal() + const validatorAddress = modalData?.validatorAddress + + if (!validatorAddress) return null + + return +} + +const RebondModalInner = ({ validatorAddress }: Props) => { + const { hideModal } = useModal() + const { api } = useApi() + const { allAccounts } = useMyAccounts() + const { rebond, isConnected } = useStakingTransactions() + const { getUnbondingInfo } = useStakingQueries() + + const [amount, setAmount] = useState('') + const [unbondingInfo, setUnbondingInfo] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + const isMountedRef = useRef(true) + + useEffect(() => { + return () => { + isMountedRef.current = false + } + }, []) + + useEffect(() => { + const loadUnbondingInfo = async () => { + if (!allAccounts[0]?.address) return + + try { + const info = await getUnbondingInfo(allAccounts[0].address) + setUnbondingInfo(info) + } catch (err) { + setError('Failed to load unbonding info') + } + } + + loadUnbondingInfo() + }, [allAccounts]) + + const handleRebond = async () => { + if (!api || !isConnected) { + setError('API not connected') + return + } + + if (!amount || parseFloat(amount) <= 0) { + setError('Please enter a valid amount') + return + } + + const rebondAmount = joyStringToPlanckBigInt(amount) + if (unbondingInfo && rebondAmount > unbondingInfo.totalUnbonding) { + setError('Amount exceeds unbonding balance') + return + } + + setIsLoading(true) + setError(null) + + try { + const rebondTx = rebond(rebondAmount) + await rebondTx.signAndSend(allAccounts[0]) + + if (isMountedRef.current) { + setSuccess(true) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Rebonding failed') + } finally { + setIsLoading(false) + } + } + + const handleAmountChange = (e: React.ChangeEvent) => { + const value = e.target.value + if (value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) >= 0)) { + setAmount(value) + } + } + + const handleMaxAmount = () => { + if (unbondingInfo) { + setAmount(planckToJoyString(unbondingInfo.totalUnbonding)) + } + } + + if (success) { + return ( + + ) + } + + return ( + + + + + Rebond your unbonding tokens to active staking. + + + Account Address: {validatorAddress} + + + {unbondingInfo && ( +
+ + Unbonding Info: + + Total Unbonding: {planckToJoyString(unbondingInfo.totalUnbonding)} JOY + Unbonding Chunks: {unbondingInfo.chunks.length} +
+ )} + + + + + + {unbondingInfo && ( + + Use Max Amount + + )} + + {error && ( + + Error: {error} + + )} + + + Note: Rebonding converts your unbonding tokens back to active staking. This will restart{' '} + the unbonding period if you decide to unbond again. + +
+
+ + + Cancel + + + {isLoading ? 'Rebonding...' : 'Rebond Tokens'} + + +
+ ) +} diff --git a/packages/ui/src/validators/modals/RebondModal/index.ts b/packages/ui/src/validators/modals/RebondModal/index.ts new file mode 100644 index 0000000000..15d58eb9a8 --- /dev/null +++ b/packages/ui/src/validators/modals/RebondModal/index.ts @@ -0,0 +1,2 @@ +export { RebondModal } from './RebondModal' +export type { RebondModalCall } from './types' diff --git a/packages/ui/src/validators/modals/RebondModal/types.ts b/packages/ui/src/validators/modals/RebondModal/types.ts new file mode 100644 index 0000000000..a4c6a0ac4a --- /dev/null +++ b/packages/ui/src/validators/modals/RebondModal/types.ts @@ -0,0 +1,8 @@ +import { Address } from '@/common/types' + +export interface RebondModalCall { + modal: 'Rebond' + data: { + validatorAddress: Address + } +} diff --git a/packages/ui/src/validators/modals/SetNomineesModal/SetNomineesModal.tsx b/packages/ui/src/validators/modals/SetNomineesModal/SetNomineesModal.tsx new file mode 100644 index 0000000000..bdbff231b2 --- /dev/null +++ b/packages/ui/src/validators/modals/SetNomineesModal/SetNomineesModal.tsx @@ -0,0 +1,314 @@ +import React, { useMemo, useState } from 'react' +import { combineLatest, first, map, of } from 'rxjs' +import styled from 'styled-components' + +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { encodeAddress } from '@/accounts/model/encodeAddress' +import { useApi } from '@/api/hooks/useApi' +import { ButtonSecondary } from '@/common/components/buttons' +import { FailureModal } from '@/common/components/FailureModal' +import { Modal, ModalBody, ModalHeader, ModalTransactionFooter } from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { FilterTextSelect } from '@/common/components/selects' +import { SuccessModal } from '@/common/components/SuccessModal' +import { TextMedium, TextSmall } from '@/common/components/typography' +import { Colors } from '@/common/constants' +import { useMachine } from '@/common/hooks/useMachine' +import { useModal } from '@/common/hooks/useModal' +import { useObservable } from '@/common/hooks/useObservable' +import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' +import { transactionMachine } from '@/common/model/machines' +import { useStakingTransactions } from '@/validators/hooks/useStakingSDK' + +import { SetNomineesModalCall } from '.' + +export const SetNomineesModal = () => { + const { modalData } = useModal() + + if (!modalData) { + return null + } + + return +} + +interface SetNomineesModalInnerProps { + stash: string + currentNominations: string[] +} + +const SetNomineesModalInner = ({ stash, currentNominations }: SetNomineesModalInnerProps) => { + const { hideModal } = useModal() + const { api } = useApi() + const { allAccounts } = useMyAccounts() + const { nominate } = useStakingTransactions() + const [state, , service] = useMachine(transactionMachine) + + const [selectedValidators, setSelectedValidators] = useState(currentNominations) + const [selectedValidatorToAdd, setSelectedValidatorToAdd] = useState(null) + + // Get all validators from the chain + const allValidators = useObservable(() => { + if (!api) return of([] as string[]) + return api.query.session.validators().pipe( + map((validators) => validators.map((v) => v.toString())), + first() + ) + }, [api?.isConnected]) + + // Get validator preferences (commission) for all validators + const validatorsWithDetails = useObservable(() => { + if (!api || !allValidators || allValidators.length === 0) + return of([] as Array<{ account: string; commission?: number }>) + + return combineLatest( + allValidators.map((validator) => + api.query.staking.validators(validator).pipe( + map((prefs: any) => { + if (prefs.isEmpty) return { account: validator, commission: undefined } + const prefsData = prefs.unwrap ? prefs.unwrap() : prefs + return { + account: validator, + commission: prefsData.commission ? prefsData.commission.toNumber() / 10_000_000 : undefined, + } + }), + first() + ) + ) + ) + }, [api?.isConnected, allValidators]) + + const availableValidators = validatorsWithDetails || [] + const isLoadingValidators = allValidators === undefined || validatorsWithDetails === undefined + + const selectedValidatorsEncoded = useMemo( + () => selectedValidators.map((address) => encodeAddress(address)), + [selectedValidators] + ) + + // Get validator options for the select dropdown + const validatorOptions = useMemo(() => { + const selectedSet = new Set(selectedValidatorsEncoded) + return availableValidators + .filter((v) => !selectedSet.has(encodeAddress(v.account))) + .map((v) => { + const address = encodeAddress(v.account) + const commission = v.commission !== undefined ? ` (${v.commission.toFixed(2)}%)` : '' + return `${address}${commission}` + }) + }, [availableValidators, selectedValidatorsEncoded]) + + // Handle adding a validator from the select + const handleAddValidator = (value: string | null) => { + if (!value) return + setSelectedValidatorToAdd(null) + + // Extract the address from the option (format: "address (commission%)") + const addressMatch = value.match(/^(j4[a-zA-Z0-9]+)/) + if (!addressMatch) return + + const validatorAddress = availableValidators.find((v) => encodeAddress(v.account) === addressMatch[1])?.account + if (!validatorAddress) return + + if (!selectedValidators.includes(validatorAddress) && selectedValidators.length < 16) { + setSelectedValidators((prev) => [...prev, validatorAddress]) + } + } + + const transaction = useMemo(() => { + if (!api) return undefined + return nominate(selectedValidators) + }, [api, nominate, selectedValidators]) + + const controllerAccount = useObservable(() => { + if (!api) return of(undefined) + return api.query.staking.bonded(stash).pipe( + map((bonded) => { + if (bonded.isNone) return undefined + return bonded.unwrap().toString() + }), + first() + ) + }, [api?.isConnected, stash]) + + const signerAccount = useMemo(() => { + if (controllerAccount) { + return allAccounts.find((acc) => acc.address === controllerAccount) || allAccounts[0] + } + return allAccounts.find((acc) => acc.address === stash) || allAccounts[0] + }, [allAccounts, controllerAccount, stash]) + + const { isReady, sign, paymentInfo, canAfford } = useSignAndSendTransaction({ + transaction, + signer: signerAccount?.address ?? '', + service: service as any, + skipQueryNode: true, + }) + + const handleValidatorToggle = (validatorAddress: string) => { + setSelectedValidators((prev) => { + if (prev.includes(validatorAddress)) { + return prev.filter((addr) => addr !== validatorAddress) + } else { + if (prev.length >= 16) { + return prev + } + return [...prev, validatorAddress] + } + }) + } + + if (state.matches('canceled')) { + return ( + + + + + The transaction was canceled. Please try again if you want to update your nominations. + + + + + ) + } + + if (state.matches('error')) { + return ( + + There was a problem with updating nominations + + ) + } + + if (state.matches('success')) { + return ( + + ) + } + + const signDisabled = !isReady || !canAfford || isLoadingValidators + + return ( + + + + + + Select validators to nominate for stash {encodeAddress(stash)}. You can nominate up to 16{' '} + validators. + + + + Select Validator + + {isLoadingValidators ? ( + Loading validators... + ) : validatorOptions.length === 0 ? ( + No validators available + ) : ( + + )} + +
+ + Selected Validators: {selectedValidators.length} / 16 + + {selectedValidators.length >= 16 && ( + + Maximum number of nominations reached. Remove a validator to add another. + + )} +
+ + {selectedValidators.length > 0 && ( + + + All Selected Nominees: + + {selectedValidators.map((validatorAddress) => { + const validator = availableValidators.find((v) => v.account === validatorAddress) + const isCurrentlyNominated = currentNominations.includes(validatorAddress) + return ( + +
+
+ {encodeAddress(validatorAddress)} + {isCurrentlyNominated && ( + (Currently Nominated) + )} +
+ {validator?.commission !== undefined && ( + Commission: {validator.commission.toFixed(2)}% + )} +
+ handleValidatorToggle(validatorAddress)}> + Remove + +
+ ) + })} +
+ )} + + {!canAfford && paymentInfo?.partialFee && ( + + Error: Insufficient funds to cover transaction costs + + )} + + + Note: Your nominations will take effect in the next era. You can change nominations at any{' '} + time without unbonding. + +
+
+ + + Cancel + + +
+ ) +} + +const SelectedValidatorsList = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + border: 1px solid ${Colors.Black[200]}; + border-radius: 4px; + background-color: ${Colors.Black[50]}; +` + +const SelectedValidatorItem = styled.div` + display: flex; + align-items: center; + padding: 8px; + background-color: ${Colors.White}; + border: 1px solid ${Colors.Black[200]}; + border-radius: 4px; +` + +const RemoveButton = styled(ButtonSecondary)` + margin-left: 8px; +` diff --git a/packages/ui/src/validators/modals/SetNomineesModal/index.ts b/packages/ui/src/validators/modals/SetNomineesModal/index.ts new file mode 100644 index 0000000000..46a408daff --- /dev/null +++ b/packages/ui/src/validators/modals/SetNomineesModal/index.ts @@ -0,0 +1,13 @@ +import { ModalWithDataCall } from '@/common/providers/modal/types' + +export type SetNomineesModalCall = ModalWithDataCall< + 'SetNomineesModal', + { + stash: string + nominations: string[] + } +> + +export * from './SetNomineesModal' + + diff --git a/packages/ui/src/validators/modals/StakeModal/StakeModal.tsx b/packages/ui/src/validators/modals/StakeModal/StakeModal.tsx index c9552bab26..4eb4444097 100644 --- a/packages/ui/src/validators/modals/StakeModal/StakeModal.tsx +++ b/packages/ui/src/validators/modals/StakeModal/StakeModal.tsx @@ -1,14 +1,23 @@ -import React from 'react' +import React, { useMemo } from 'react' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { useApi } from '@/api/hooks/useApi' -import { ButtonPrimary } from '@/common/components/buttons' -import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' +import { ButtonSecondary } from '@/common/components/buttons' +import { FailureModal } from '@/common/components/FailureModal' +import { Modal, ModalBody, ModalHeader, ModalTransactionFooter } from '@/common/components/Modal' import { RowGapBlock } from '@/common/components/page/PageContent' +import { SuccessModal } from '@/common/components/SuccessModal' import { TextMedium } from '@/common/components/typography' +import { useMachine } from '@/common/hooks/useMachine' import { useModal } from '@/common/hooks/useModal' +import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' +import { transactionMachine } from '@/common/model/machines' import { Address } from '@/common/types' +import { encodeAddress } from '@/accounts/model/encodeAddress' +import { useStakingTransactions } from '@/validators/hooks/useStakingSDK' +import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' -import { StakeModalCall } from '@/validators/modals/StakeModal/types' +import { StakeModalCall } from './types' interface Props { validatorAddress: Address @@ -19,52 +28,110 @@ export const StakeModal = () => { const validatorAddress = modalData?.validatorAddress if (!validatorAddress) return null - + return } const StakeModalInner = ({ validatorAddress }: Props) => { const { hideModal } = useModal() const { api } = useApi() + const { allAccounts } = useMyAccounts() + const { active: activeMembership } = useMyMemberships() + const { nominate } = useStakingTransactions() + const [state, , service] = useMachine(transactionMachine) - const handleStake = async () => { - if (!api) { - console.error('API not available') - return - } + const transaction = useMemo(() => { + if (!api) return undefined + // Nominate this single validator + return nominate([validatorAddress]) + }, [api, nominate, validatorAddress]) - try { - // TODO: Implement actual staking transaction - console.log('Staking with validator:', validatorAddress) - hideModal() - } catch (error) { - console.error('Staking failed:', error) + // Use active membership controller account, or fallback to first account + const signerAccount = useMemo(() => { + if (activeMembership?.controllerAccount) { + return allAccounts.find((acc) => acc.address === activeMembership.controllerAccount) || allAccounts[0] } + return allAccounts[0] + }, [activeMembership, allAccounts]) + + const { isReady, sign, paymentInfo, canAfford } = useSignAndSendTransaction({ + transaction, + signer: signerAccount?.address ?? '', + service: service as any, + skipQueryNode: true, + }) + + if (state.matches('canceled')) { + return ( + + + + The transaction was canceled. Please try again if you want to nominate this validator. + + + + ) + } + + if (state.matches('error')) { + return ( + + There was a problem with nominating the validator + + ) } + if (state.matches('success')) { + return ( + + ) + } + + const signDisabled = !isReady || !canAfford + return ( - - + + - You are about to stake your tokens with this validator. Staking tokens means you are - locking them to support the network and potentially earn rewards. + You are about to nominate {encodeAddress(validatorAddress)} to receive staking rewards. + - Validator Address: {validatorAddress} + Nominating a validator means you are delegating your staked tokens to support this validator. You will earn{' '} + rewards based on the validator's performance and commission rate. + - Note: This is a preview implementation. The actual transaction will be implemented - in a separate PR for testing. + Note: You must have bonded tokens before you can nominate. If you haven't bonded yet, please{' '} + use the "Bond" action first. Your nomination will take effect in the next era. + + {!canAfford && paymentInfo?.partialFee && ( + + Error: Insufficient funds to cover transaction costs + + )} - - - Stake Tokens - - + + + Cancel + + ) } diff --git a/packages/ui/src/validators/modals/StopStakingModal/StopStakingModal.tsx b/packages/ui/src/validators/modals/StopStakingModal/StopStakingModal.tsx new file mode 100644 index 0000000000..5d4b32f96e --- /dev/null +++ b/packages/ui/src/validators/modals/StopStakingModal/StopStakingModal.tsx @@ -0,0 +1,171 @@ +import React, { useMemo } from 'react' + +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { encodeAddress } from '@/accounts/model/encodeAddress' +import { useApi } from '@/api/hooks/useApi' +import { ButtonSecondary } from '@/common/components/buttons' +import { FailureModal } from '@/common/components/FailureModal' +import { Modal, ModalBody, ModalHeader, ModalTransactionFooter } from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { SuccessModal } from '@/common/components/SuccessModal' +import { TextMedium } from '@/common/components/typography' +import { useMachine } from '@/common/hooks/useMachine' +import { useModal } from '@/common/hooks/useModal' +import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' +import { transactionMachine } from '@/common/model/machines' +import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' +import { useStakingTransactions } from '@/validators/hooks/useStakingSDK' + +import { StopStakingModalCall } from '.' + +export const StopStakingModal = () => { + const { hideModal, modalData } = useModal() + + if (!modalData) { + return null + } + + // If already inactive, show a message that there's nothing to stop + if (modalData.role === 'inactive') { + return ( + + + + + + This stash {encodeAddress(modalData.stash)} is already inactive and not participating in + staking. + + There is nothing to stop. + + + + + ) + } + + return +} + +interface StopStakingModalInnerProps { + stash: string + role: 'validator' | 'nominator' +} + +const StopStakingModalInner = ({ stash, role }: StopStakingModalInnerProps) => { + const { hideModal } = useModal() + const { api } = useApi() + const { allAccounts } = useMyAccounts() + const { active: activeMembership } = useMyMemberships() + const { chill, nominate } = useStakingTransactions() + const [state, , service] = useMachine(transactionMachine) + + // For validators: use chill() to stop validating + // For nominators: use nominate([]) to clear nominations + const transaction = useMemo(() => { + if (!api) return undefined + if (role === 'validator') { + return chill() + } else { + // Clear nominations by nominating an empty array + return nominate([]) + } + }, [api, role, chill, nominate]) + + // Use active membership controller account, or fallback to stash account + const signerAccount = useMemo(() => { + if (activeMembership?.controllerAccount) { + return allAccounts.find((acc) => acc.address === activeMembership.controllerAccount) || allAccounts[0] + } + return allAccounts.find((acc) => acc.address === stash) || allAccounts[0] + }, [activeMembership, allAccounts, stash]) + + const { isReady, sign, paymentInfo, canAfford } = useSignAndSendTransaction({ + transaction, + signer: signerAccount?.address ?? '', + service: service as any, + skipQueryNode: true, + }) + + const roleLabel = role === 'validator' ? 'validator' : 'nominator' + const actionLabel = role === 'validator' ? 'stop validating' : 'stop nominating' + + if (state.matches('canceled')) { + return ( + + + + The transaction was canceled. Please try again if you want to {actionLabel}. + + + + ) + } + + if (state.matches('error')) { + return ( + + There was a problem with stopping staking + + ) + } + + if (state.matches('success')) { + return ( + + ) + } + + const signDisabled = !isReady || !canAfford + + return ( + + + + + + You are about to stop participating as a {roleLabel} for stash {encodeAddress(stash)}. + + + {role === 'validator' ? ( + + This will chill your validator, stopping it from participating in the validator set. You will stop earning + validator rewards, but your bonded tokens will remain bonded. + + ) : ( + + This will clear all your nominations. You will stop earning nominator rewards, but your bonded tokens will + remain bonded. + + )} + + + Note: The change will take effect in the next era. You can start{' '} + {role === 'validator' ? 'validating' : 'nominating'} again at any time. + + + {!canAfford && paymentInfo?.partialFee && ( + + Error: Insufficient funds to cover transaction costs + + )} + + + + + Cancel + + + + ) +} diff --git a/packages/ui/src/validators/modals/StopStakingModal/index.ts b/packages/ui/src/validators/modals/StopStakingModal/index.ts new file mode 100644 index 0000000000..023aa0f324 --- /dev/null +++ b/packages/ui/src/validators/modals/StopStakingModal/index.ts @@ -0,0 +1,15 @@ +import { ModalWithDataCall } from '@/common/providers/modal/types' + +import { MyStakingRole } from '@/validators/hooks/useMyStashPositions' + +export type StopStakingModalCall = ModalWithDataCall< + 'StopStakingModal', + { + stash: string + role: MyStakingRole + } +> + +export * from './StopStakingModal' + + diff --git a/packages/ui/src/validators/modals/UnbondModal/UnbondModal.tsx b/packages/ui/src/validators/modals/UnbondModal/UnbondModal.tsx index 64cffb1ba9..3e27e4f6ac 100644 --- a/packages/ui/src/validators/modals/UnbondModal/UnbondModal.tsx +++ b/packages/ui/src/validators/modals/UnbondModal/UnbondModal.tsx @@ -1,13 +1,17 @@ -import React from 'react' +import React, { useState, useEffect, useRef } from 'react' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { useApi } from '@/api/hooks/useApi' -import { ButtonPrimary } from '@/common/components/buttons' +import { ButtonPrimary, ButtonSecondary } from '@/common/components/buttons' +import { InputComponent, InputText } from '@/common/components/forms' import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' import { RowGapBlock } from '@/common/components/page/PageContent' -import { TextMedium } from '@/common/components/typography' +import { SuccessModal } from '@/common/components/SuccessModal' +import { TextMedium, TextSmall } from '@/common/components/typography' import { useModal } from '@/common/hooks/useModal' +import { joyStringToPlanckBigInt, planckToJoyString } from '@/common/model/joyValueFromString' import { Address } from '@/common/types' - +import { useStakingQueries, useStakingTransactions } from '@/validators/hooks/useStakingSDK' import { UnbondModalCall } from '@/validators/modals/UnbondModal/types' interface Props { @@ -19,50 +23,157 @@ export const UnbondModal = () => { const validatorAddress = modalData?.validatorAddress if (!validatorAddress) return null - + return } const UnbondModalInner = ({ validatorAddress }: Props) => { const { hideModal } = useModal() const { api } = useApi() + const { allAccounts } = useMyAccounts() + const { unbond, isConnected } = useStakingTransactions() + const { getStakingInfo } = useStakingQueries() + + const [amount, setAmount] = useState('') + const [maxBonded, setMaxBonded] = useState(BigInt(0)) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + const isMountedRef = useRef(true) + + useEffect(() => { + return () => { + isMountedRef.current = false + } + }, []) + + useEffect(() => { + const loadStakingInfo = async () => { + if (!allAccounts[0]?.address) return + + try { + const stakingInfo = await getStakingInfo(allAccounts[0].address) + if (isMountedRef.current) { + setMaxBonded(stakingInfo.activeBonded) + } + } catch (err) { + setError('Failed to load staking info') + } + } + + loadStakingInfo() + }, [allAccounts]) const handleUnbond = async () => { - if (!api) { - console.error('API not available') + if (!api || !isConnected) { + setError('API not connected') + return + } + + if (!amount || parseFloat(amount) <= 0) { + setError('Please enter a valid amount') + return + } + + const unbondAmount = joyStringToPlanckBigInt(amount) + if (unbondAmount > maxBonded) { + setError('Amount exceeds bonded balance') return } + setIsLoading(true) + setError(null) + try { - // TODO: Implement actual unbonding transaction - console.log('Unbonding from validator:', validatorAddress) - hideModal() - } catch (error) { - console.error('Unbonding failed:', error) + const unbondTx = unbond(unbondAmount) + await unbondTx.signAndSend(allAccounts[0]) + + if (isMountedRef.current) { + setSuccess(true) + } + } catch (err) { + if (isMountedRef.current) { + setError(err instanceof Error ? err.message : 'Unbonding failed') + } + } finally { + if (isMountedRef.current) { + setIsLoading(false) + } + } + } + + const handleAmountChange = (e: React.ChangeEvent) => { + const value = e.target.value + if (value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) >= 0)) { + setAmount(value) } } + const handleMaxAmount = () => { + setAmount(planckToJoyString(maxBonded)) + } + + if (success) { + return ( + + ) + } + return ( - You are about to unbond your tokens from this validator. Unbonding tokens means you are - requesting to withdraw your staked tokens, but they may be subject to an unbonding period. + Unbond your tokens. They will be subject to a 28-day unbonding period before withdrawal. + Validator Address: {validatorAddress} + - Note: This is a preview implementation. The actual transaction will be implemented - in a separate PR for testing. + Bonded Balance: {planckToJoyString(maxBonded)} JOY + + + + + + + Use Max Amount + + + {error && ( + + Error: {error} + + )} + + + Important: Unbonded tokens will be locked for 28 days before you can withdraw them. During{' '} + this period, they will not earn rewards. + - - Unbond Tokens + + Cancel + + + {isLoading ? 'Unbonding...' : 'Unbond Tokens'} diff --git a/packages/ui/src/validators/modals/UnbondStakingModal/UnbondStakingModal.tsx b/packages/ui/src/validators/modals/UnbondStakingModal/UnbondStakingModal.tsx new file mode 100644 index 0000000000..1a91916d52 --- /dev/null +++ b/packages/ui/src/validators/modals/UnbondStakingModal/UnbondStakingModal.tsx @@ -0,0 +1,181 @@ +import BN from 'bn.js' +import React, { useMemo, useState } from 'react' + +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { encodeAddress } from '@/accounts/model/encodeAddress' +import { useApi } from '@/api/hooks/useApi' +import { ButtonSecondary } from '@/common/components/buttons' +import { FailureModal } from '@/common/components/FailureModal' +import { InputComponent, InputText } from '@/common/components/forms' +import { Modal, ModalBody, ModalHeader, ModalTransactionFooter } from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { SuccessModal } from '@/common/components/SuccessModal' +import { TextMedium, TextSmall, TokenValue } from '@/common/components/typography' +import { useMachine } from '@/common/hooks/useMachine' +import { useModal } from '@/common/hooks/useModal' +import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' +import { joyStringToPlanckBigInt, planckToJoyString } from '@/common/model/joyValueFromString' +import { transactionMachine } from '@/common/model/machines' +import { useStakingTransactions } from '@/validators/hooks/useStakingSDK' + +import { UnbondStakingModalCall } from '.' + +export const UnbondStakingModal = () => { + const { modalData } = useModal() + + if (!modalData) { + return null + } + + return +} + +interface UnbondStakingModalInnerProps { + stash: string + controller?: string + bonded: BN +} + +const UnbondStakingModalInner = ({ stash, controller, bonded }: UnbondStakingModalInnerProps) => { + const { hideModal } = useModal() + const { api } = useApi() + const { allAccounts } = useMyAccounts() + const { unbond } = useStakingTransactions() + const [state, , service] = useMachine(transactionMachine) + + const [amount, setAmount] = useState('') + + const bondedBigInt = BigInt(bonded.toString()) + + const transaction = useMemo(() => { + if (!api || !amount || parseFloat(amount) <= 0) return undefined + const unbondAmount = joyStringToPlanckBigInt(amount) + if (unbondAmount > bondedBigInt) return undefined + return unbond(unbondAmount) + }, [api, amount, unbond, bondedBigInt]) + + const signerAccount = useMemo(() => { + if (controller) { + return allAccounts.find((acc) => acc.address === controller) || allAccounts[0] + } + return allAccounts.find((acc) => acc.address === stash) || allAccounts[0] + }, [allAccounts, controller, stash]) + + const { isReady, sign, paymentInfo, canAfford } = useSignAndSendTransaction({ + transaction, + signer: signerAccount?.address ?? '', + service: service as any, + skipQueryNode: true, + }) + + const handleAmountChange = (e: React.ChangeEvent) => { + const value = e.target.value + if (value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) >= 0)) { + setAmount(value) + } + } + + const handleMaxAmount = () => { + setAmount(planckToJoyString(bondedBigInt)) + } + + if (state.matches('canceled')) { + return ( + + + + The transaction was canceled. Please try again if you want to unbond your stake. + + + + ) + } + + if (state.matches('error')) { + return ( + + There was a problem with unbonding your stake + + ) + } + + if (state.matches('success')) { + return ( + + ) + } + + const signDisabled = + !isReady || + !canAfford || + !amount || + parseFloat(amount) <= 0 || + (parseFloat(amount) > 0 && joyStringToPlanckBigInt(amount) > bondedBigInt) + + return ( + + + + + + Unbond your tokens from stash {encodeAddress(stash)}. Unbonded tokens will be locked for 28 + days before you can withdraw them. + + + + Bonded Balance: + + + + + + + + Use Max Amount + + + {amount && parseFloat(amount) > 0 && joyStringToPlanckBigInt(amount) > bondedBigInt && ( + + Error: Amount exceeds bonded balance + + )} + + {!canAfford && paymentInfo?.partialFee && ( + + Error: Insufficient funds to cover transaction costs + + )} + + + Important: Unbonded tokens will be locked for 28 days before you can withdraw them. During{' '} + this period, they will not earn rewards. + + + + + + Cancel + + + + ) +} diff --git a/packages/ui/src/validators/modals/UnbondStakingModal/index.ts b/packages/ui/src/validators/modals/UnbondStakingModal/index.ts new file mode 100644 index 0000000000..64ff558d7e --- /dev/null +++ b/packages/ui/src/validators/modals/UnbondStakingModal/index.ts @@ -0,0 +1,16 @@ +import BN from 'bn.js' + +import { ModalWithDataCall } from '@/common/providers/modal/types' + +export type UnbondStakingModalCall = ModalWithDataCall< + 'UnbondStakingModal', + { + stash: string + controller?: string + bonded: BN + } +> + +export * from './UnbondStakingModal' + + diff --git a/packages/ui/src/validators/modals/ValidateModal/ValidateModal.tsx b/packages/ui/src/validators/modals/ValidateModal/ValidateModal.tsx new file mode 100644 index 0000000000..f1cfb97de4 --- /dev/null +++ b/packages/ui/src/validators/modals/ValidateModal/ValidateModal.tsx @@ -0,0 +1,205 @@ +import React, { useState, useEffect, useRef, useMemo } from 'react' + +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { useApi } from '@/api/hooks/useApi' +import { ButtonPrimary, ButtonSecondary } from '@/common/components/buttons' +import { InputComponent, InputText } from '@/common/components/forms' +import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' +import { RowGapBlock } from '@/common/components/page/PageContent' +import { SuccessModal } from '@/common/components/SuccessModal' +import { TextMedium, TextSmall } from '@/common/components/typography' +import { useModal } from '@/common/hooks/useModal' +import { Address } from '@/common/types' +import { useStakingTransactions, useStakingValidation } from '@/validators/hooks/useStakingSDK' +import { ValidateModalCall } from '@/validators/modals/ValidateModal/types' + +interface Props { + validatorAddress: Address +} + +export const ValidateModal = () => { + const { modalData } = useModal() + const validatorAddress = modalData?.validatorAddress + + if (!validatorAddress) return null + + return +} + +const ValidateModalInner = ({ validatorAddress }: Props) => { + const { hideModal } = useModal() + const { api } = useApi() + const { allAccounts } = useMyAccounts() + const { validate, isConnected } = useStakingTransactions() + const { canValidate } = useStakingValidation() + + const [commission, setCommission] = useState('5.0') + const [blocked, setBlocked] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + const [canValidateAccount, setCanValidateAccount] = useState(null) + const isMountedRef = useRef(true) + const selectedAccount = useMemo(() => { + if (!allAccounts.length) return null + return allAccounts.find((account) => account.address === validatorAddress) ?? allAccounts[0] + }, [allAccounts, validatorAddress]) + + useEffect(() => { + return () => { + isMountedRef.current = false + } + }, []) + + useEffect(() => { + const checkValidation = async () => { + if (!selectedAccount?.address) { + setCanValidateAccount(null) + setError('No account available to submit this transaction') + return + } + + try { + const canValidateResult = await canValidate(selectedAccount.address) + setCanValidateAccount(canValidateResult) + setError(null) + } catch (err) { + setError('Failed to check validation status') + } + } + + checkValidation() + }, [canValidate, selectedAccount]) + + const handleValidate = async () => { + if (!api || !isConnected) { + setError('API not connected') + return + } + + if (!selectedAccount?.address) { + setError('No account available to sign the transaction') + return + } + + if (!commission || parseFloat(commission) < 0 || parseFloat(commission) > 100) { + setError('Please enter a valid commission rate (0-100%)') + return + } + + if (canValidateAccount === false) { + setError('This account cannot become a validator') + return + } + + setIsLoading(true) + setError(null) + + try { + const validateTx = validate(parseFloat(commission), blocked) + await validateTx.signAndSend(selectedAccount.address) + + if (isMountedRef.current) { + setSuccess(true) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Validation failed') + } finally { + setIsLoading(false) + } + } + + const handleCommissionChange = (e: React.ChangeEvent) => { + const value = e.target.value + if (value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) >= 0 && parseFloat(value) <= 100)) { + setCommission(value) + setError(null) + } + } + + const actionDisabled = + isLoading || + !commission || + parseFloat(commission) < 0 || + parseFloat(commission) > 100 || + canValidateAccount === false || + !selectedAccount || + canValidateAccount === null || + !!error + + if (success) { + return ( + + ) + } + + return ( + + + + + + Become a validator by setting your commission rate. You will need to bond tokens first. + + + + Account Address: {validatorAddress} + + + {canValidateAccount !== null && ( + + Can Validate: {canValidateAccount ? 'Yes' : 'No'} + + )} + + + + + + + setBlocked(e.target.checked)} + /> + + + {error && ( + + Error: {error} + + )} + + + Important: Becoming a validator requires: +
• Bonded tokens +
• Valid commission rate +
• Good network connection +
• Understanding of validator responsibilities +
+
+
+ + + Cancel + + + {isLoading ? 'Validating...' : 'Become Validator'} + + +
+ ) +} diff --git a/packages/ui/src/validators/modals/ValidateModal/index.ts b/packages/ui/src/validators/modals/ValidateModal/index.ts new file mode 100644 index 0000000000..d788666f17 --- /dev/null +++ b/packages/ui/src/validators/modals/ValidateModal/index.ts @@ -0,0 +1,2 @@ +export { ValidateModal } from './ValidateModal' +export type { ValidateModalCall } from './types' diff --git a/packages/ui/src/validators/modals/ValidateModal/types.ts b/packages/ui/src/validators/modals/ValidateModal/types.ts new file mode 100644 index 0000000000..27e19019e1 --- /dev/null +++ b/packages/ui/src/validators/modals/ValidateModal/types.ts @@ -0,0 +1,8 @@ +import { Address } from '@/common/types' + +export interface ValidateModalCall { + modal: 'Validate' + data: { + validatorAddress: Address + } +} diff --git a/packages/ui/src/validators/modals/ValidatorsInfo.tsx b/packages/ui/src/validators/modals/ValidatorsInfo.tsx index 434d30b4e6..4b4ea476e0 100644 --- a/packages/ui/src/validators/modals/ValidatorsInfo.tsx +++ b/packages/ui/src/validators/modals/ValidatorsInfo.tsx @@ -1,35 +1,33 @@ -import React, { useState } from 'react' +import React from 'react' +import { Link } from 'react-router-dom' import styled from 'styled-components' -import { Link } from '@/common/components/Link' +import { ButtonPrimary } from '@/common/components/buttons' +import { Checkbox } from '@/common/components/forms' +import { ArrowRightIcon } from '@/common/components/icons' +import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' import { RowGapBlock } from '@/common/components/page/PageContent' +import { TextMedium } from '@/common/components/typography' +import { Colors } from '@/common/constants' import { useLocalStorage } from '@/common/hooks/useLocalStorage' -import { useToggle } from '@/common/hooks/useToggle' - -import { ButtonPrimary } from '../../common/components/buttons' -import { Checkbox } from '../../common/components/forms' -import { ArrowRightIcon } from '../../common/components/icons' -import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../common/components/Modal' -import { TextMedium } from '../../common/components/typography' +import { useModal } from '@/common/hooks/useModal' export const ValidatorsInfo = () => { const title = 'Nominating validators on Joystream' const buttonName = 'Start nominating' - const [check, setCheck] = useToggle(false) - const [notShowAgain, setNotShowAgain] = useLocalStorage('ValidatorsPageCheck') - const [showModal, setShowModal] = useState(true) + + const { hideModal } = useModal() + const [pageCheck, showInfo] = useLocalStorage('ValidatorsPageCheck') + const closeModal = () => { - setShowModal(false) - } - const checkModal = () => { - setNotShowAgain(check) - closeModal() + hideModal() + showInfo(true) } - if (!notShowAgain && showModal) + if (!pageCheck) return ( - - + + @@ -46,22 +44,25 @@ export const ValidatorsInfo = () => { To begin, review each validator's performance metrics by clicking on their name in the list. When you're - ready to nominate, add the validators you'd like to nominate by clicking the "Nominate" button on the - list or directly on the validator’s profile. Once you've selected a validator, click the "Proceed" + ready to nominate, add the validators you'd like to nominate to by clicking the "Nominate" button on the + list or directly on the validator’s profile. Once you've selected your validators, click the "Proceed" button to initiate the nomination process. You can learn more about the Pioneer nomination{' '} - system here. + + system here + + . - + {}} isChecked={false}> Do not show this again. - + {buttonName} diff --git a/packages/ui/src/validators/modals/validatorCard/ValidatorCard.tsx b/packages/ui/src/validators/modals/validatorCard/ValidatorCard.tsx index 209e91d4d7..5592e54471 100644 --- a/packages/ui/src/validators/modals/validatorCard/ValidatorCard.tsx +++ b/packages/ui/src/validators/modals/validatorCard/ValidatorCard.tsx @@ -28,9 +28,10 @@ interface Props { validator: ValidatorWithDetails selectCard: (cardNumber: number | null) => void totalCards: number + isNominated?: boolean } -export const ValidatorCard = React.memo(({ cardNumber, validator, eraIndex, selectCard, totalCards }: Props) => { +export const ValidatorCard = React.memo(({ cardNumber, validator, eraIndex, selectCard, totalCards, isNominated = false }: Props) => { const hideModal = () => { selectCard(null) } @@ -83,7 +84,7 @@ export const ValidatorCard = React.memo(({ cardNumber, validator, eraIndex, sele - {activeTab === 'Details' && } + {activeTab === 'Details' && } {activeTab === 'Nominators' && } diff --git a/packages/ui/src/validators/modals/validatorCard/ValidatorDetail.tsx b/packages/ui/src/validators/modals/validatorCard/ValidatorDetail.tsx index 74b07a5ea5..1a94609ce7 100644 --- a/packages/ui/src/validators/modals/validatorCard/ValidatorDetail.tsx +++ b/packages/ui/src/validators/modals/validatorCard/ValidatorDetail.tsx @@ -1,24 +1,30 @@ +import BN from 'bn.js' import React from 'react' +import { combineLatest, first, map, of, switchMap } from 'rxjs' import styled from 'styled-components' -import { ButtonPrimary, ButtonSecondary, ButtonGhost } from '@/common/components/buttons' +import { useApi } from '@/api/hooks/useApi' +import { ButtonPrimary } from '@/common/components/buttons' import { MarkdownPreview } from '@/common/components/MarkdownPreview' import { ModalFooter } from '@/common/components/Modal' import { RowGapBlock } from '@/common/components/page/PageContent' import { SidePaneBody, SidePaneLabel, SidePaneRow, SidePaneText } from '@/common/components/SidePane' import { NumericValueStat, StatisticsThreeColumns, TokenValueStat } from '@/common/components/statistics' -import { TextSmall } from '@/common/components/typography' +import { TextSmall, TokenValue } from '@/common/components/typography' import { BN_ZERO } from '@/common/constants' import { plural } from '@/common/helpers' import { useModal } from '@/common/hooks/useModal' +import { useObservable } from '@/common/hooks/useObservable' import { whenDefined } from '@/common/utils' import RewardPointsChart from '@/validators/components/RewardPointChart' +import { useSelectedValidators } from '@/validators/context/SelectedValidatorsContext' +import { useClaimAllNavigation } from '@/validators/hooks/useClaimAllNavigation' +import { useMyStashPositions } from '@/validators/hooks/useMyStashPositions' import { ValidatorWithDetails } from '../../types' import { BondModalCall } from '../BondModal' import { NominateValidatorModalCall } from '../NominateValidatorModal' import { NominatingRedirectModalCall } from '../NominatingRedirectModal' -import { PayoutModalCall } from '../PayoutModal' import { StakeModalCall } from '../StakeModal' import { UnbondModalCall } from '../UnbondModal' @@ -26,15 +32,54 @@ interface Props { validator: ValidatorWithDetails eraIndex: number | undefined hideModal: () => void + isNominated?: boolean } -export const ValidatorDetail = ({ validator, eraIndex, hideModal }: Props) => { +export const ValidatorDetail = ({ validator, eraIndex, hideModal, isNominated = false }: Props) => { + const { api } = useApi() + const stashPositions = useMyStashPositions() const { showModal } = useModal() const { showModal: showNominateModal } = useModal() const { showModal: showStakeModal } = useModal() const { showModal: showBondModal } = useModal() const { showModal: showUnbondModal } = useModal() - const { showModal: showPayoutModal } = useModal() + const { isSelected, toggleSelection, selectedValidators, maxSelection } = useSelectedValidators() + const openClaimAllModal = useClaimAllNavigation() + + // Get stake amount for this nominated validator + const nominatedStake = useObservable(() => { + if (!api || !isNominated || !stashPositions) return of(undefined) + + // Find stash positions that nominate this validator + const nominatingStashes = stashPositions.filter((pos) => pos.nominations.includes(validator.stashAccount)) + if (nominatingStashes.length === 0) return of(undefined) + + // Get current era and query exposures + return api.query.staking.activeEra().pipe( + first(), + switchMap((activeEra) => { + if (activeEra.isNone) return of(undefined) + const currentEra = activeEra.unwrap().index.toNumber() + + // Query exposures for all nominating stashes + const exposureQueries = nominatingStashes.map((pos) => + api.query.staking.erasStakers(currentEra, validator.stashAccount).pipe( + first(), + map((exposure) => { + if (!exposure || exposure.isEmpty) return BN_ZERO + const nominatorExposure = exposure.others.find((other) => other.who.toString() === pos.stash) + return nominatorExposure ? nominatorExposure.value.toBn() : BN_ZERO + }) + ) + ) + + return combineLatest(exposureQueries).pipe( + first(), + map((stakes) => stakes.reduce((sum, stake) => sum.add(stake), BN_ZERO)) + ) + }) + ) + }, [api?.isConnected, isNominated, validator.stashAccount, stashPositions]) const uptime = whenDefined(validator.rewardPointsHistory, (rewardPointsHistory) => { const firstEra = rewardPointsHistory.at(0)?.era @@ -44,31 +89,43 @@ export const ValidatorDetail = ({ validator, eraIndex, hideModal }: Props) => { return `${((validatedEra / totalEras) * 100).toFixed(1)}%` }) - const handleActionClick = (action: string) => { + const isValidatorSelected = isSelected(validator) + const canSelect = !isValidatorSelected && selectedValidators.length < maxSelection + + const handleActionClick = async (action: string) => { const validatorAddress = validator.stashAccount - + switch (action) { + case 'Select': + toggleSelection(validator) + break case 'Nominate': + await new Promise((resolve) => setTimeout(resolve, 0)) // Make async hideModal() showNominateModal({ modal: 'NominateValidator', data: { validatorAddress } }) break case 'Stake': + await new Promise((resolve) => setTimeout(resolve, 0)) // Make async hideModal() showStakeModal({ modal: 'Stake', data: { validatorAddress } }) break case 'Bond': + await new Promise((resolve) => setTimeout(resolve, 0)) // Make async hideModal() showBondModal({ modal: 'Bond', data: { validatorAddress } }) break case 'Unbond': + await new Promise((resolve) => setTimeout(resolve, 0)) // Make async hideModal() showUnbondModal({ modal: 'Unbond', data: { validatorAddress } }) break case 'Payout': + await new Promise((resolve) => setTimeout(resolve, 0)) // Make async hideModal() - showPayoutModal({ modal: 'Payout', data: { validatorAddress } }) + openClaimAllModal() break default: + await new Promise((resolve) => setTimeout(resolve, 0)) // Make async hideModal() showModal({ modal: 'NominatingRedirect' }) } @@ -138,36 +195,30 @@ export const ValidatorDetail = ({ validator, eraIndex, hideModal }: Props) => { - handleActionClick('Nominate')} - > - Nominate - - handleActionClick('Stake')} - > - Stake - - handleActionClick('Bond')} - > - Bond - - handleActionClick('Unbond')} - > - Unbond - - handleActionClick('Payout')} - > - Payout - + {isNominated ? ( +
+ Nominated + {nominatedStake && !nominatedStake.isZero() && } +
+ ) : isValidatorSelected ? ( + handleActionClick('Select')} + disabled={true} + title="This validator is already selected for nomination." + > + Selected + + ) : ( + handleActionClick('Select')} + disabled={!canSelect} + title={canSelect ? 'Select this validator for nomination' : 'Maximum number of validators selected'} + > + {canSelect ? 'Select' : 'Max Reached'} + + )}
@@ -211,4 +262,5 @@ const ActionButtonsContainer = styled.div` justify-content: flex-start; align-items: center; width: 100%; + position: relative; ` diff --git a/packages/ui/src/validators/model/sortValidatorAccounts.ts b/packages/ui/src/validators/model/sortValidatorAccounts.ts new file mode 100644 index 0000000000..85ae067128 --- /dev/null +++ b/packages/ui/src/validators/model/sortValidatorAccounts.ts @@ -0,0 +1,54 @@ +import { Account } from '@/accounts/types' +import { BN_ZERO } from '@/common/constants' + +import { AccountStakingRewards } from '../hooks/useAllAccountsStakingRewards' + +export type ValidatorSortKey = 'name' | 'totalEarned' | 'claimable' + +export function sortValidatorAccounts( + accounts: Account[], + key: ValidatorSortKey, + isDescending = false, + stakingRewardsMap?: Map +): Account[] { + const sorted = [...accounts] + + if (key === 'name') { + sorted.sort((a, b) => { + const nameA = a.name?.toLowerCase() || a.address.toLowerCase() + const nameB = b.name?.toLowerCase() || b.address.toLowerCase() + return isDescending ? nameB.localeCompare(nameA) : nameA.localeCompare(nameB) + }) + } else if (key === 'totalEarned' && stakingRewardsMap) { + sorted.sort((a, b) => { + const rewardsA = stakingRewardsMap.get(a.address)?.totalEarned || BN_ZERO + const rewardsB = stakingRewardsMap.get(b.address)?.totalEarned || BN_ZERO + const comparison = rewardsA.cmp(rewardsB) + return isDescending ? -comparison : comparison + }) + } else if (key === 'claimable' && stakingRewardsMap) { + sorted.sort((a, b) => { + const claimableA = stakingRewardsMap.get(a.address)?.claimable || BN_ZERO + const claimableB = stakingRewardsMap.get(b.address)?.claimable || BN_ZERO + const comparison = claimableA.cmp(claimableB) + return isDescending ? -comparison : comparison + }) + } + + return sorted +} + +export function setValidatorOrder( + key: ValidatorSortKey, + sortBy: ValidatorSortKey, + setSortBy: (k: ValidatorSortKey) => void, + reversed: boolean, + setDescending: (d: boolean) => void +) { + if (key === sortBy) { + setDescending(!reversed) + } else { + setDescending(key !== 'name') + setSortBy(key) + } +} diff --git a/packages/ui/src/validators/providers/provider.tsx b/packages/ui/src/validators/providers/provider.tsx index 63035a17b1..e6da02070e 100644 --- a/packages/ui/src/validators/providers/provider.tsx +++ b/packages/ui/src/validators/providers/provider.tsx @@ -1,5 +1,5 @@ import React, { ReactNode, useState } from 'react' -import { map } from 'rxjs' +import { filter, first, map, of, switchMap } from 'rxjs' import { useApi } from '@/api/hooks/useApi' import { useFirstObservableValue } from '@/common/hooks/useFirstObservableValue' @@ -30,26 +30,75 @@ export const ValidatorContextProvider = (props: Props) => { const [shouldFetchValidators, setShouldFetchValidators] = useState(false) const allValidators = useFirstObservableValue(() => { - if (!shouldFetchValidators) return - - return api?.query.staking.validators.entries().pipe( - map((entries) => - entries.map((entry) => ({ - stashAccount: entry[0].args[0].toString(), - commission: perbillToPercent(entry[1].commission.toBn()), - })) - ) + if (!shouldFetchValidators || !api) return + + // Wait for API to be ready (metadata loaded) before making queries + // This prevents "PortableRegistry has not been set" errors + const isReady$ = + 'isReady' in api && typeof api.isReady === 'function' + ? (api.isReady() as any).pipe( + filter((ready: any) => Boolean(ready)), + first() + ) + : api.rpc.chain.getBlockHash(0).pipe( + first(), + map(() => true) + ) + + return isReady$.pipe( + switchMap(() => { + // Check if entries() method exists (it might not be available on ProxyApi) + const validatorsQuery = api.query.staking.validators + if (validatorsQuery && typeof validatorsQuery.entries === 'function') { + return validatorsQuery.entries().pipe( + map((entries) => + entries.map((entry) => ({ + stashAccount: entry[0].args[0].toString(), + commission: perbillToPercent(entry[1].commission.toBn()), + })) + ) + ) + } + + // Fallback: Use session.validators() to get active validators + // This only returns active validators, not all validators with preferences set + return api.query.session.validators().pipe( + switchMap((activeValidators) => { + const validatorAddresses = activeValidators.map((v) => v.toString()) + if (validatorAddresses.length === 0) { + return of([]) + } + + // Get validator preferences for active validators + return api.query.staking.validators.multi(validatorAddresses).pipe( + map((prefs: any[]) => + prefs + .map((pref: any, index: number) => { + if (!pref || pref.isEmpty) return null + const prefsData = pref.isEmpty ? null : pref.unwrap ? pref.unwrap() : pref + if (!prefsData || !prefsData.commission) return null + return { + stashAccount: validatorAddresses[index], + commission: perbillToPercent(prefsData.commission.toBn()), + } + }) + .filter((v): v is Validator => v !== null) + ) + ) + }) + ) + }) ) }, [api?.isConnected, shouldFetchValidators]) const allValidatorsWithCtrlAcc = useFirstObservableValue(() => { - if (!allValidators) return + if (!allValidators || !Array.isArray(allValidators) || allValidators.length === 0 || !api) return - return api?.query.staking.bonded.multi(allValidators.map((validator) => validator.stashAccount)).pipe( - map((entries) => - entries.map((entry, index) => { + return api.query.staking.bonded.multi(allValidators.map((validator: Validator) => validator.stashAccount)).pipe( + map((entries: any[]) => + entries.map((entry: any, index: number) => { const validator = allValidators[index] - const controllerAccount = entry.isSome ? entry.unwrap().toString() : undefined + const controllerAccount = entry && entry.isSome ? entry.unwrap().toString() : undefined return { ...validator, controllerAccount } }) ) diff --git a/packages/ui/src/validators/providers/useValidatorsQueries.ts b/packages/ui/src/validators/providers/useValidatorsQueries.ts index f2bc11ab25..1096d8ed4e 100644 --- a/packages/ui/src/validators/providers/useValidatorsQueries.ts +++ b/packages/ui/src/validators/providers/useValidatorsQueries.ts @@ -63,27 +63,29 @@ export const useValidatorsQueries = (): CommonValidatorsQueries | undefined => { const eraRewardPoints$ = api.derive.staking.erasPoints() const validatorsRewards$ = combineLatest([erasRewards$, eraRewardPoints$]).pipe( - map(([erasRewards, eraRewardPoints]) => - eraRewardPoints.map((points, index) => { - const era = points.era.toNumber() - const reward = erasRewards[index] - - if (era !== reward?.era.toNumber()) { - throw Error( - `derive.staking.erasRewards and derive.staking.erasPoints eras didn't match. Era #${era} is missing` - ) - } - - return { - era, - totalPoints: points.eraPoints.toNumber(), - totalReward: reward.eraReward, - individual: Object.fromEntries( - Object.entries(points.validators).map(([address, points]) => [address, points.toNumber()]) - ), - } - }) - ), + map(([erasRewards, eraRewardPoints]) => { + const rewardsByEra = new Map(erasRewards.map((reward) => [reward.era.toNumber(), reward])) + + return eraRewardPoints + .map((points) => { + const era = points.era.toNumber() + const reward = rewardsByEra.get(era) + + if (!reward) { + return null + } + + return { + era, + totalPoints: points.eraPoints.toNumber(), + totalReward: reward.eraReward, + individual: Object.fromEntries( + Object.entries(points.validators).map(([address, points]) => [address, points.toNumber()]) + ), + } + }) + .filter((reward): reward is EraRewards => reward !== null) + }), keepFirst() ) diff --git a/packages/ui/src/validators/types/NominatorTypes.ts b/packages/ui/src/validators/types/NominatorTypes.ts new file mode 100644 index 0000000000..dd9ff5bb08 --- /dev/null +++ b/packages/ui/src/validators/types/NominatorTypes.ts @@ -0,0 +1,6 @@ +import { Block } from '@polkadot/types/interfaces' + +export interface Nominator { + dateAndTime: Block + source?: string +} diff --git a/packages/ui/src/validators/types/index.ts b/packages/ui/src/validators/types/index.ts index 7e842456bc..39739511b8 100644 --- a/packages/ui/src/validators/types/index.ts +++ b/packages/ui/src/validators/types/index.ts @@ -1 +1,5 @@ export * from './Validator' + +// filter +export type Verification = null | 'verified' | 'unverified' +export type State = null | 'active' | 'waiting' \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f142135dce..97a4f824b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5045,6 +5045,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.13 + resolution: "@jridgewell/gen-mapping@npm:0.3.13" + dependencies: + "@jridgewell/sourcemap-codec": ^1.5.0 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: f2105acefc433337145caa3c84bba286de954f61c0bc46279bbd85a9e6a02871089717fa060413cfb6a9d44189fe8313b2d1cabf3a2eb3284d208fd5f75c54ff + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:3.1.0": version: 3.1.0 resolution: "@jridgewell/resolve-uri@npm:3.1.0" @@ -5052,27 +5062,27 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.0.3": - version: 3.1.1 - resolution: "@jridgewell/resolve-uri@npm:3.1.1" - checksum: f5b441fe7900eab4f9155b3b93f9800a916257f4e8563afbcd3b5a5337b55e52bd8ae6735453b1b745457d9f6cdb16d74cd6220bbdd98cf153239e13f6cbb653 +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 83b85f72c59d1c080b4cbec0fef84528963a1b5db34e4370fa4bd1e3ff64a0d80e0cee7369d11d73c704e0286fb2865b530acac7a871088fbe92b5edf1000870 languageName: node linkType: hard "@jridgewell/set-array@npm:^1.0.1": - version: 1.1.2 - resolution: "@jridgewell/set-array@npm:1.1.2" - checksum: 69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 languageName: node linkType: hard "@jridgewell/source-map@npm:^0.3.3": - version: 0.3.3 - resolution: "@jridgewell/source-map@npm:0.3.3" + version: 0.3.11 + resolution: "@jridgewell/source-map@npm:0.3.11" dependencies: - "@jridgewell/gen-mapping": ^0.3.0 - "@jridgewell/trace-mapping": ^0.3.9 - checksum: ae1302146339667da5cd6541260ecbef46ae06819a60f88da8f58b3e64682f787c09359933d050dea5d2173ea7fa40f40dd4d4e7a8d325c5892cccd99aaf8959 + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.25 + checksum: c8a0011cc67e701f270fa042e32b312f382c413bcc70ca9c03684687cbf5b64d5eed87d4afa36dddaabe60ab3da6db4935f878febd9cfc7f82724ea1a114d344 languageName: node linkType: hard @@ -5090,6 +5100,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: c2e36e67971f719a8a3a85ef5a5f580622437cc723c35d03ebd0c9c0b06418700ef006f58af742791f71f6a4fc68fcfaf1f6a74ec2f9a3332860e9373459dae7 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -5110,6 +5127,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: af8fda2431348ad507fbddf8e25f5d08c79ecc94594061ce402cf41bc5aba1a7b3e59bf0fd70a619b35f33983a3f488ceeba8faf56bff784f98bb5394a8b7d47 + languageName: node + linkType: hard + "@jsdevtools/ono@npm:7.1.3, @jsdevtools/ono@npm:^7.1.3": version: 7.1.3 resolution: "@jsdevtools/ono@npm:7.1.3" @@ -13838,11 +13865,11 @@ __metadata: linkType: hard "chart.js@npm:^4.4.1": - version: 4.4.1 - resolution: "chart.js@npm:4.4.1" + version: 4.5.1 + resolution: "chart.js@npm:4.5.1" dependencies: "@kurkle/color": ^0.3.0 - checksum: 8c108f137824ea0e3d79914708b409298c12ed7d16da99a40ca3cab51466ef7b0068ad82ac0addeb288b938fe0621d3e5a6a913b60e72a729aa42aa96705fcad + checksum: 34b35b373642994b2adac197e91363625930530e29fc1baa6dbb411b5e1295f9f6572922003a0224a21a3019aec916567c1ed00c33b1373081f189fc188e5a7b languageName: node linkType: hard From 0186ba14852a6f6a4b8f7527e8c86576875d0e17 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Tue, 2 Dec 2025 11:14:28 +0100 Subject: [PATCH 43/67] Fixup for #4888 --- packages/ui/src/app/GlobalModals.tsx | 1 - packages/ui/src/validators/components/Nominators.tsx | 2 +- .../nominator/NominatorPositionsTable.stories.tsx | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/app/GlobalModals.tsx b/packages/ui/src/app/GlobalModals.tsx index 430ca7bb8b..ba2964b705 100644 --- a/packages/ui/src/app/GlobalModals.tsx +++ b/packages/ui/src/app/GlobalModals.tsx @@ -259,7 +259,6 @@ const GUEST_ACCESSIBLE_MODALS: ModalNames[] = [ 'Stake', 'Bond', 'Unbond', - 'Payout', 'CreateOpening', 'LeaveRole', ] diff --git a/packages/ui/src/validators/components/Nominators.tsx b/packages/ui/src/validators/components/Nominators.tsx index 7e086ecfce..a87506e3ce 100644 --- a/packages/ui/src/validators/components/Nominators.tsx +++ b/packages/ui/src/validators/components/Nominators.tsx @@ -52,7 +52,7 @@ export function Nominators() { switchMap(([nominations, bonded]) => { const isNominating = !nominations.isEmpty const targets = isNominating ? nominations.unwrap().targets.map((target) => target.toString()) : [] - + if (bonded.isNone) { return of({ address: account.address, diff --git a/packages/ui/src/validators/components/nominator/NominatorPositionsTable.stories.tsx b/packages/ui/src/validators/components/nominator/NominatorPositionsTable.stories.tsx index ac77fe29eb..3003e2e378 100644 --- a/packages/ui/src/validators/components/nominator/NominatorPositionsTable.stories.tsx +++ b/packages/ui/src/validators/components/nominator/NominatorPositionsTable.stories.tsx @@ -323,10 +323,10 @@ WithNominationsTooltip.parameters = { const stashAddress = 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT' const validator1 = 'j4W7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf' const validator2 = 'j4UYhDYJ4pz2ihhDDzu69v2JTVeGaGmTebmBdWaX2ANVinXyE' - + // Convert validatorAddress to string for comparison const validatorStr = validatorAddress?.toString() || validatorAddress - + // Return different stake amounts for different validators let stakeAmount = '30000000000000' // 30,000 JOY default if (validatorStr === validator1) { @@ -334,7 +334,7 @@ WithNominationsTooltip.parameters = { } else if (validatorStr === validator2) { stakeAmount = '30000000000000' // 30,000 JOY } - + return { isEmpty: false, total: '100000000000000', From f228c0c10983c23f8898f8b77a7b5c95b05d510e Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Tue, 2 Dec 2025 13:02:11 +0100 Subject: [PATCH 44/67] lint, remove unused --- .../Validators/components/ClaimAllButton.tsx | 24 +- .../components/Search/SearchResultItem.tsx | 2 +- .../components/icons/symbols/DeleteSymbol.tsx | 29 +-- packages/ui/src/common/constants/styles.ts | 2 +- packages/ui/src/common/hooks/useSort.ts | 2 +- .../ui/src/common/model/joyValueFromString.ts | 7 +- .../src/forum/hooks/useCategoryLatestPost.ts | 2 +- .../src/forum/hooks/useLatestForumThreads.ts | 2 +- .../MemberProfile/MemberDetails.tsx | 9 - .../proposals/hooks/useProposalsActivities.ts | 4 +- .../src/validators/components/Nominators.tsx | 13 +- .../components/OverView.stories.tsx | 3 +- .../components/ValidatorInfo.stories.tsx | 9 +- .../components/ValidatorItem.stories.tsx | 5 +- .../validators/components/ValidatorItem.tsx | 2 +- .../dashboard/NominatorItem.stories.tsx | 1 - .../ValidatorAccountItem.stories.tsx | 5 +- .../nominator/NominatorActionMenu.tsx | 23 +- .../NominatorPositionsTable.stories.tsx | 110 ++++----- .../ui/src/validators/constants/constant.ts | 2 +- .../context/SelectedValidatorsContext.tsx | 10 +- .../hooks/useNominatorClaimableByValidator.ts | 1 - .../validators/hooks/useValidatorHealth.ts | 1 - .../modals/ChangeSessionKeysModal/index.ts | 1 - .../modals/ClaimStakingRewardsModal/index.ts | 1 - .../modals/ManageStashActionModal/index.ts | 3 - .../modals/PayoutModal/PayoutModal.tsx | 70 ------ .../validators/modals/PayoutModal/index.ts | 2 - .../validators/modals/PayoutModal/types.ts | 6 - .../modals/SetNomineesModal/index.ts | 2 - .../modals/StakeModal/StakeModal.tsx | 12 +- .../StakingTransactionModal.tsx | 17 +- .../modals/StopStakingModal/index.ts | 3 - .../modals/UnbondStakingModal/index.ts | 2 - .../modals/validatorCard/ValidatorCard.tsx | 119 ++++----- packages/ui/src/validators/types/index.ts | 2 +- .../__generated__/workingGroups.generated.tsx | 228 +++++++++--------- 37 files changed, 303 insertions(+), 433 deletions(-) delete mode 100644 packages/ui/src/validators/modals/PayoutModal/PayoutModal.tsx delete mode 100644 packages/ui/src/validators/modals/PayoutModal/index.ts delete mode 100644 packages/ui/src/validators/modals/PayoutModal/types.ts diff --git a/packages/ui/src/app/pages/Validators/components/ClaimAllButton.tsx b/packages/ui/src/app/pages/Validators/components/ClaimAllButton.tsx index 7549ff4a5a..5e05e783bf 100644 --- a/packages/ui/src/app/pages/Validators/components/ClaimAllButton.tsx +++ b/packages/ui/src/app/pages/Validators/components/ClaimAllButton.tsx @@ -5,18 +5,18 @@ import { useModal } from '@/common/hooks/useModal' import { useMyStakingRewards } from '@/validators/hooks/useMyStakingRewards' export const ClaimAllButton = () => { - const { showModal } = useModal() - const stakingRewards = useMyStakingRewards() + const { showModal } = useModal() + const stakingRewards = useMyStakingRewards() - const hasClaimableRewards = stakingRewards && stakingRewards.claimableRewards.gtn(0) + const hasClaimableRewards = stakingRewards && stakingRewards.claimableRewards.gtn(0) - return ( - showModal({ modal: 'ClaimStakingRewardsModal' })} - disabled={!hasClaimableRewards} - > - Claim All - - ) + return ( + showModal({ modal: 'ClaimStakingRewardsModal' })} + disabled={!hasClaimableRewards} + > + Claim All + + ) } diff --git a/packages/ui/src/common/components/Search/SearchResultItem.tsx b/packages/ui/src/common/components/Search/SearchResultItem.tsx index 5d2c58b353..7c80d1c268 100644 --- a/packages/ui/src/common/components/Search/SearchResultItem.tsx +++ b/packages/ui/src/common/components/Search/SearchResultItem.tsx @@ -1,9 +1,9 @@ import React from 'react' import styled from 'styled-components' -import { GhostRouterLink } from '@/common/components/RouterLink' import { BreadcrumbsItem, BreadcrumbsItemLink } from '@/common/components/page/Sidebar/Breadcrumbs/BreadcrumbsItem' import { BreadcrumbsListComponent } from '@/common/components/page/Sidebar/Breadcrumbs/BreadcrumbsList' +import { GhostRouterLink } from '@/common/components/RouterLink' import { Colors, Fonts, Transitions } from '@/common/constants' import { TextMedium } from '../typography' diff --git a/packages/ui/src/common/components/icons/symbols/DeleteSymbol.tsx b/packages/ui/src/common/components/icons/symbols/DeleteSymbol.tsx index 04857380b7..45b167e025 100644 --- a/packages/ui/src/common/components/icons/symbols/DeleteSymbol.tsx +++ b/packages/ui/src/common/components/icons/symbols/DeleteSymbol.tsx @@ -5,18 +5,19 @@ import { Colors } from '../../../constants' import { Symbol, SymbolProps } from './common' export function DeleteSymbol({ className }: SymbolProps) { - return ( - - - - - - - ) + return ( + + + + + ) } diff --git a/packages/ui/src/common/constants/styles.ts b/packages/ui/src/common/constants/styles.ts index b2b6dea287..f4fb38a75a 100644 --- a/packages/ui/src/common/constants/styles.ts +++ b/packages/ui/src/common/constants/styles.ts @@ -121,7 +121,7 @@ export const BorderRad = { export const Sizes = { selectHeight: '80px', accountHeight: '94px', - validatorHeight:'70px' + validatorHeight: '70px', } export const Shadows = { diff --git a/packages/ui/src/common/hooks/useSort.ts b/packages/ui/src/common/hooks/useSort.ts index 746b5805fa..ab5c72fb00 100644 --- a/packages/ui/src/common/hooks/useSort.ts +++ b/packages/ui/src/common/hooks/useSort.ts @@ -61,4 +61,4 @@ export function sortBy( return order.isDescending ? -comparison : comparison }) return sorted -} \ No newline at end of file +} diff --git a/packages/ui/src/common/model/joyValueFromString.ts b/packages/ui/src/common/model/joyValueFromString.ts index 95bb400d37..e4f1142163 100644 --- a/packages/ui/src/common/model/joyValueFromString.ts +++ b/packages/ui/src/common/model/joyValueFromString.ts @@ -25,12 +25,7 @@ export const joyStringToPlanckBigInt = (value: string): bigint => BigInt(joyValu export const planckToJoyString = (value: BN | bigint | string | number, precision = 4): string => { const bnValue = toBN(value) const { div: integerPart, mod } = bnValue.divmod(JOY_UNIT) - const fractional = mod - .abs() - .toString() - .padStart(JOY_DECIMAL_PLACES, '0') - .slice(0, precision) - .replace(/0+$/, '') + const fractional = mod.abs().toString().padStart(JOY_DECIMAL_PLACES, '0').slice(0, precision).replace(/0+$/, '') return fractional ? `${integerPart.toString()}.${fractional}` : integerPart.toString() } diff --git a/packages/ui/src/forum/hooks/useCategoryLatestPost.ts b/packages/ui/src/forum/hooks/useCategoryLatestPost.ts index b906f23ddc..d512ff1dc0 100644 --- a/packages/ui/src/forum/hooks/useCategoryLatestPost.ts +++ b/packages/ui/src/forum/hooks/useCategoryLatestPost.ts @@ -2,8 +2,8 @@ import * as Apollo from '@apollo/client' import { useEffect, useMemo } from 'react' import { ForumPostOrderByInput, ForumThreadOrderByInput } from '@/common/api/queries' -import { GetForumPostsDocument, useGetForumThreadsQuery } from '@/forum/queries' import { ActiveStatus } from '@/forum/hooks/useForumCategories' +import { GetForumPostsDocument, useGetForumThreadsQuery } from '@/forum/queries' import { asForumPost, asForumThread } from '@/forum/types' interface UseCategoryLatestPostOptions { diff --git a/packages/ui/src/forum/hooks/useLatestForumThreads.ts b/packages/ui/src/forum/hooks/useLatestForumThreads.ts index 9c12ca5595..e29561c53e 100644 --- a/packages/ui/src/forum/hooks/useLatestForumThreads.ts +++ b/packages/ui/src/forum/hooks/useLatestForumThreads.ts @@ -1,8 +1,8 @@ import { useMemo } from 'react' import { ForumThreadOrderByInput } from '@/common/api/queries' -import { useGetForumThreadsQuery } from '@/forum/queries' import { ActiveStatus } from '@/forum/hooks/useForumCategories' +import { useGetForumThreadsQuery } from '@/forum/queries' import { asForumThread } from '../types' diff --git a/packages/ui/src/memberships/components/MemberProfile/MemberDetails.tsx b/packages/ui/src/memberships/components/MemberProfile/MemberDetails.tsx index 1019dd5e78..b20267818f 100644 --- a/packages/ui/src/memberships/components/MemberProfile/MemberDetails.tsx +++ b/packages/ui/src/memberships/components/MemberProfile/MemberDetails.tsx @@ -39,15 +39,6 @@ export const MemberDetails = React.memo(({ member }: Props) => { initiatingLeaving = '-', } = useMemberExtraInfo(member) - const externalResourceLink: any = { - TELEGRAM: 'https://web.telegram.org/k/#@', - TWITTER: 'https://twitter.com/', - FACEBOOK: 'https://facebook.com/', - YOUTUBE: 'https://youtube.com/@', - LINKEDIN: 'https://www.linkedin.com/in/', - GITHUB: 'https://github.com/', - } - if (isLoading || !memberDetails) { return ( diff --git a/packages/ui/src/proposals/hooks/useProposalsActivities.ts b/packages/ui/src/proposals/hooks/useProposalsActivities.ts index dcd48ddc35..804bf0b240 100644 --- a/packages/ui/src/proposals/hooks/useProposalsActivities.ts +++ b/packages/ui/src/proposals/hooks/useProposalsActivities.ts @@ -3,7 +3,7 @@ import { asProposalActivities } from '../types/ProposalsActivities' export const useProposalsActivities = () => { const { data, loading } = useGetProposalsEventsQuery() - + const activities = data ? asProposalActivities({ proposalCreatedEvents: data.proposalCreatedEvents, @@ -18,7 +18,7 @@ export const useProposalsActivities = () => { proposalDiscussionPostDeletedEvents: data.proposalDiscussionPostDeletedEvents, }) : [] - + return { isLoading: loading, activities, diff --git a/packages/ui/src/validators/components/Nominators.tsx b/packages/ui/src/validators/components/Nominators.tsx index a87506e3ce..6fcce7eab5 100644 --- a/packages/ui/src/validators/components/Nominators.tsx +++ b/packages/ui/src/validators/components/Nominators.tsx @@ -5,19 +5,17 @@ import styled from 'styled-components' import { AccountItemLoading } from '@/accounts/components/AccountItem/AccountItemLoading' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { useMyBalances } from '@/accounts/hooks/useMyBalances' -import { Account } from '@/accounts/types' import { filterAccounts } from '@/accounts/model/filterAccounts' import { sortAccounts, SortKey, setOrder } from '@/accounts/model/sortAccounts' +import { useApi } from '@/api/hooks/useApi' import { ButtonPrimary } from '@/common/components/buttons' import { EmptyPagePlaceholder } from '@/common/components/EmptyPagePlaceholder/EmptyPagePlaceholder' import { List, ListItem } from '@/common/components/List' import { ContentWithTabs } from '@/common/components/page/PageContent' import { HeaderText, SortIconDown, SortIconUp } from '@/common/components/SortedListHeaders' -import { Colors } from '@/common/constants' -import { useApi } from '@/api/hooks/useApi' -import { BN_ZERO } from '@/common/constants' -import { useObservable } from '@/common/hooks/useObservable' +import { Colors, BN_ZERO } from '@/common/constants' import { useModal } from '@/common/hooks/useModal' +import { useObservable } from '@/common/hooks/useObservable' import { NominatorInfo } from '@/validators/hooks/useNominatorInfo' import { NominatorAccountItem } from './dashboard/NominatorItem' @@ -45,10 +43,7 @@ export function Nominators() { return combineLatest( sortedAccounts.map((account) => - combineLatest([ - api.query.staking.nominators(account.address), - api.query.staking.bonded(account.address), - ]).pipe( + combineLatest([api.query.staking.nominators(account.address), api.query.staking.bonded(account.address)]).pipe( switchMap(([nominations, bonded]) => { const isNominating = !nominations.isEmpty const targets = isNominating ? nominations.unwrap().targets.map((target) => target.toString()) : [] diff --git a/packages/ui/src/validators/components/OverView.stories.tsx b/packages/ui/src/validators/components/OverView.stories.tsx index 2eee6a81f3..53b479c9c9 100644 --- a/packages/ui/src/validators/components/OverView.stories.tsx +++ b/packages/ui/src/validators/components/OverView.stories.tsx @@ -1,5 +1,5 @@ -import BN from 'bn.js' import { Meta, Story } from '@storybook/react' +import BN from 'bn.js' import React from 'react' import { MockProvidersDecorator } from '@/mocks/providers' @@ -149,4 +149,3 @@ EmptyState.parameters = { }, }, } - diff --git a/packages/ui/src/validators/components/ValidatorInfo.stories.tsx b/packages/ui/src/validators/components/ValidatorInfo.stories.tsx index a54a207cff..9b219d3264 100644 --- a/packages/ui/src/validators/components/ValidatorInfo.stories.tsx +++ b/packages/ui/src/validators/components/ValidatorInfo.stories.tsx @@ -48,9 +48,7 @@ LargeSize.args = { handle: 'validator_bob', name: 'Bob Validator', avatar: undefined, - externalResources: [ - { source: 'TWITTER', value: 'bob_validator' }, - ], + externalResources: [{ source: 'TWITTER', value: 'bob_validator' }], } as any, size: 'l', } @@ -63,10 +61,7 @@ WithTwitterOnly.args = { handle: 'validator_charlie', name: 'Charlie Validator', avatar: undefined, - externalResources: [ - { source: 'TWITTER', value: 'charlie_validator' }, - ], + externalResources: [{ source: 'TWITTER', value: 'charlie_validator' }], } as any, size: 's', } - diff --git a/packages/ui/src/validators/components/ValidatorItem.stories.tsx b/packages/ui/src/validators/components/ValidatorItem.stories.tsx index dcecec145e..d6694457a1 100644 --- a/packages/ui/src/validators/components/ValidatorItem.stories.tsx +++ b/packages/ui/src/validators/components/ValidatorItem.stories.tsx @@ -1,10 +1,10 @@ -import BN from 'bn.js' import { Meta, Story } from '@storybook/react' +import BN from 'bn.js' import React from 'react' import { MockApolloProvider } from '@/mocks/components/storybook/MockApolloProvider' -import { SelectedValidatorsProvider } from '@/validators/context/SelectedValidatorsContext' import { ValidatorItem, ValidatorItemProps } from '@/validators/components/ValidatorItem' +import { SelectedValidatorsProvider } from '@/validators/context/SelectedValidatorsContext' import { ValidatorWithDetails } from '@/validators/types/Validator' export default { @@ -144,4 +144,3 @@ Nominated.args = { }), isNominated: true, } - diff --git a/packages/ui/src/validators/components/ValidatorItem.tsx b/packages/ui/src/validators/components/ValidatorItem.tsx index ebc5d2c1b7..980d53d3c0 100644 --- a/packages/ui/src/validators/components/ValidatorItem.tsx +++ b/packages/ui/src/validators/components/ValidatorItem.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components' import { encodeAddress } from '@/accounts/model/encodeAddress' import { useApi } from '@/api/hooks/useApi' import { BadgeStatus } from '@/common/components/BadgeStatus' -import { ButtonPrimary, ButtonSecondary, ButtonGhost } from '@/common/components/buttons' +import { ButtonPrimary } from '@/common/components/buttons' import { TableListItemAsLinkHover } from '@/common/components/List' import { Skeleton } from '@/common/components/Skeleton' import { Tooltip, TooltipPopupTitle, TooltipText } from '@/common/components/Tooltip' diff --git a/packages/ui/src/validators/components/dashboard/NominatorItem.stories.tsx b/packages/ui/src/validators/components/dashboard/NominatorItem.stories.tsx index eb77463d36..c0a26d0b5f 100644 --- a/packages/ui/src/validators/components/dashboard/NominatorItem.stories.tsx +++ b/packages/ui/src/validators/components/dashboard/NominatorItem.stories.tsx @@ -51,4 +51,3 @@ LongAccountName.args = { name: 'Very Long Nominator Account Name That Should Be Truncated', }, } - diff --git a/packages/ui/src/validators/components/dashboard/ValidatorAccountItem.stories.tsx b/packages/ui/src/validators/components/dashboard/ValidatorAccountItem.stories.tsx index e6574193f4..5abbef2d98 100644 --- a/packages/ui/src/validators/components/dashboard/ValidatorAccountItem.stories.tsx +++ b/packages/ui/src/validators/components/dashboard/ValidatorAccountItem.stories.tsx @@ -1,10 +1,10 @@ -import BN from 'bn.js' import { Meta, Story } from '@storybook/react' +import BN from 'bn.js' import React from 'react' import { MockApolloProvider } from '@/mocks/components/storybook/MockApolloProvider' -import { AccountStakingRewards } from '@/validators/hooks/useAllAccountsStakingRewards' import { ValidatorAccountItem, AccountItemDataProps } from '@/validators/components/dashboard/ValidatorAccountItem' +import { AccountStakingRewards } from '@/validators/hooks/useAllAccountsStakingRewards' export default { title: 'Validators/Dashboard/ValidatorAccountItem', @@ -83,4 +83,3 @@ SmallRewards.args = { hasClaimable: true, }, } - diff --git a/packages/ui/src/validators/components/nominator/NominatorActionMenu.tsx b/packages/ui/src/validators/components/nominator/NominatorActionMenu.tsx index 10d7d7faf9..a724ec1523 100644 --- a/packages/ui/src/validators/components/nominator/NominatorActionMenu.tsx +++ b/packages/ui/src/validators/components/nominator/NominatorActionMenu.tsx @@ -23,14 +23,7 @@ interface Props { onUnbond: () => void } -export const NominatorActionMenu = ({ - items, - canStop, - stopDisabled, - onStop, - canUnbond, - onUnbond, -}: Props) => { +export const NominatorActionMenu = ({ items, canStop, stopDisabled, onStop, canUnbond, onUnbond }: Props) => { const [isMenuOpen, setMenuOpen] = useState(false) const [menuPosition, setMenuPosition] = useState<{ top: number; right: number } | null>(null) const menuRef = useRef(null) @@ -75,10 +68,7 @@ export const NominatorActionMenu = ({ setMenuOpen((prev) => !prev) } - const handleMenuItemClick = ( - event: React.MouseEvent, - { disabled, onClick }: MenuActionItem - ) => { + const handleMenuItemClick = (event: React.MouseEvent, { disabled, onClick }: MenuActionItem) => { if (disabled) return event.stopPropagation() setMenuOpen(false) @@ -94,13 +84,7 @@ export const NominatorActionMenu = ({ return ( - + {isMenuOpen && @@ -185,4 +169,3 @@ const MenuContainer = styled.div` gap: 0; align-items: center; ` - diff --git a/packages/ui/src/validators/components/nominator/NominatorPositionsTable.stories.tsx b/packages/ui/src/validators/components/nominator/NominatorPositionsTable.stories.tsx index 3003e2e378..f6c63756db 100644 --- a/packages/ui/src/validators/components/nominator/NominatorPositionsTable.stories.tsx +++ b/packages/ui/src/validators/components/nominator/NominatorPositionsTable.stories.tsx @@ -4,10 +4,9 @@ import React from 'react' import styled, { createGlobalStyle } from 'styled-components' import { encodeAddress } from '@/accounts/model/encodeAddress' -import { createType } from '@/common/model/createType' import { Tooltip, TooltipPopupTitle, TooltipText } from '@/common/components/Tooltip' -import { TextSmall } from '@/common/components/typography' import { Colors, JOY_DECIMAL_PLACES } from '@/common/constants' +import { createType } from '@/common/model/createType' import { shortenAddress } from '@/common/model/formatters' import { MockProvidersDecorator } from '@/mocks/providers' import { NominatorPositionsTable, Props } from '@/validators/components/nominator/NominatorPositionsTable' @@ -434,67 +433,63 @@ export const TooltipActiveState: Story = () => { className="wide-tooltip" tooltipOpen={true} popupContent={ - - {activeNominations.length > 0 && ( - <> + + {activeNominations.length > 0 && ( + <> + + Active ({activeNominations.length}) + {activeNominations.map((nom) => ( + + + {nom.address.includes('...') ? nom.address : shortenAddress(encodeAddress(nom.address), 20)} + + {nom.stake && ( + + {(() => { + try { + const stake = nom.stake as any + if (stake instanceof BN) { + return abbreviateTokenAmount(stake) + } else if (stake && typeof stake.toNumber === 'function') { + return abbreviateTokenAmount(stake.toNumber()) + } else if (stake && typeof stake.toBn === 'function') { + return abbreviateTokenAmount(stake.toBn()) + } else if (typeof stake === 'number' || typeof stake === 'string') { + return abbreviateTokenAmount(stake) + } else { + return '0' + } + } catch (err) { + return '0' + } + })()} + + )} + + ))} + + {inactiveNominations.length > 0 && } + + )} + {inactiveNominations.length > 0 && ( - Active ({activeNominations.length}) - {activeNominations.map((nom) => ( + Inactive ({inactiveNominations.length}) + {inactiveNominations.map((nom) => ( - {nom.address.includes('...') - ? nom.address - : shortenAddress(encodeAddress(nom.address), 20)} + {nom.address.includes('...') ? nom.address : shortenAddress(encodeAddress(nom.address), 20)} - {nom.stake && ( - - {(() => { - try { - const stake = nom.stake as any - if (stake instanceof BN) { - return abbreviateTokenAmount(stake) - } else if (stake && typeof stake.toNumber === 'function') { - return abbreviateTokenAmount(stake.toNumber()) - } else if (stake && typeof stake.toBn === 'function') { - return abbreviateTokenAmount(stake.toBn()) - } else if (typeof stake === 'number' || typeof stake === 'string') { - return abbreviateTokenAmount(stake) - } else { - return '0' - } - } catch (err) { - return '0' - } - })()} - - )} ))} - {inactiveNominations.length > 0 && } - - )} - {inactiveNominations.length > 0 && ( - - Inactive ({inactiveNominations.length}) - {inactiveNominations.map((nom) => ( - - - {nom.address.includes('...') - ? nom.address - : shortenAddress(encodeAddress(nom.address), 20)} - - - ))} - - )} - - } - > -
- Hover or click to see tooltip (tooltip is forced open in this story) -
-
+ )} + + } + > +
+ Hover or click to see tooltip (tooltip is forced open in this story) +
+
) @@ -502,7 +497,8 @@ export const TooltipActiveState: Story = () => { TooltipActiveState.parameters = { docs: { description: { - story: 'This story shows the nominations tooltip in an active/open state for testing purposes. The tooltip displays active and inactive nominations with their stake amounts.', + story: + 'This story shows the nominations tooltip in an active/open state for testing purposes. The tooltip displays active and inactive nominations with their stake amounts.', }, }, } diff --git a/packages/ui/src/validators/constants/constant.ts b/packages/ui/src/validators/constants/constant.ts index e55b2a04d1..b8c692d2ba 100644 --- a/packages/ui/src/validators/constants/constant.ts +++ b/packages/ui/src/validators/constants/constant.ts @@ -1 +1 @@ -export const ERA_DURATION = new Date(21600000) \ No newline at end of file +export const ERA_DURATION = new Date(21600000) diff --git a/packages/ui/src/validators/context/SelectedValidatorsContext.tsx b/packages/ui/src/validators/context/SelectedValidatorsContext.tsx index fd47715d68..bb33f7717a 100644 --- a/packages/ui/src/validators/context/SelectedValidatorsContext.tsx +++ b/packages/ui/src/validators/context/SelectedValidatorsContext.tsx @@ -21,16 +21,16 @@ export const SelectedValidatorsProvider = ({ children, maxSelection = 16 }: Sele const [selectedValidators, setSelectedValidators] = useState([]) const isSelected = (validator: ValidatorWithDetails) => { - return selectedValidators.some(selected => selected.stashAccount === validator.stashAccount) + return selectedValidators.some((selected) => selected.stashAccount === validator.stashAccount) } const toggleSelection = (validator: ValidatorWithDetails) => { - setSelectedValidators(prev => { - const isCurrentlySelected = prev.some(selected => selected.stashAccount === validator.stashAccount) - + setSelectedValidators((prev) => { + const isCurrentlySelected = prev.some((selected) => selected.stashAccount === validator.stashAccount) + if (isCurrentlySelected) { // Remove from selection - return prev.filter(selected => selected.stashAccount !== validator.stashAccount) + return prev.filter((selected) => selected.stashAccount !== validator.stashAccount) } else { // Add to selection if under max limit if (prev.length < maxSelection) { diff --git a/packages/ui/src/validators/hooks/useNominatorClaimableByValidator.ts b/packages/ui/src/validators/hooks/useNominatorClaimableByValidator.ts index ca8fd074ef..b6cdd5ff40 100644 --- a/packages/ui/src/validators/hooks/useNominatorClaimableByValidator.ts +++ b/packages/ui/src/validators/hooks/useNominatorClaimableByValidator.ts @@ -40,4 +40,3 @@ export const useNominatorClaimableByValidator = (validator: ValidatorWithDetails return estimatedClaimable }, [stakingInfo, stakingRewards, nominatorInfo]) } - diff --git a/packages/ui/src/validators/hooks/useValidatorHealth.ts b/packages/ui/src/validators/hooks/useValidatorHealth.ts index 27b7ac20b1..febbb7e21e 100644 --- a/packages/ui/src/validators/hooks/useValidatorHealth.ts +++ b/packages/ui/src/validators/hooks/useValidatorHealth.ts @@ -43,4 +43,3 @@ export const useValidatorHealth = (validator: ValidatorWithDetails): number => { return Math.min(100, Math.max(0, health)) }, [validator]) } - diff --git a/packages/ui/src/validators/modals/ChangeSessionKeysModal/index.ts b/packages/ui/src/validators/modals/ChangeSessionKeysModal/index.ts index f2b26f8f2c..95eed0429b 100644 --- a/packages/ui/src/validators/modals/ChangeSessionKeysModal/index.ts +++ b/packages/ui/src/validators/modals/ChangeSessionKeysModal/index.ts @@ -9,4 +9,3 @@ export type ChangeSessionKeysModalCall = ModalWithDataCall< > export * from './ChangeSessionKeysModal' - diff --git a/packages/ui/src/validators/modals/ClaimStakingRewardsModal/index.ts b/packages/ui/src/validators/modals/ClaimStakingRewardsModal/index.ts index 923137e5fe..63588b73db 100644 --- a/packages/ui/src/validators/modals/ClaimStakingRewardsModal/index.ts +++ b/packages/ui/src/validators/modals/ClaimStakingRewardsModal/index.ts @@ -3,4 +3,3 @@ import { OptionalDataModalCall } from '@/common/providers/modal/types' export type ClaimStakingRewardsModalCall = OptionalDataModalCall<'ClaimStakingRewardsModal', { address?: string }> export * from './ClaimStakingRewardsModal' - diff --git a/packages/ui/src/validators/modals/ManageStashActionModal/index.ts b/packages/ui/src/validators/modals/ManageStashActionModal/index.ts index 9b82927500..7cd7602269 100644 --- a/packages/ui/src/validators/modals/ManageStashActionModal/index.ts +++ b/packages/ui/src/validators/modals/ManageStashActionModal/index.ts @@ -1,7 +1,6 @@ import BN from 'bn.js' import { ModalWithDataCall } from '@/common/providers/modal/types' - import { UnlockingChunk } from '@/validators/hooks/useMyStashPositions' export type ManageStashAction = 'bondRebond' | 'withdraw' | 'changeController' | 'changeReward' @@ -19,5 +18,3 @@ export type ManageStashActionModalCall = ModalWithDataCall< > export * from './ManageStashActionModal' - - diff --git a/packages/ui/src/validators/modals/PayoutModal/PayoutModal.tsx b/packages/ui/src/validators/modals/PayoutModal/PayoutModal.tsx deleted file mode 100644 index e75f29e7e9..0000000000 --- a/packages/ui/src/validators/modals/PayoutModal/PayoutModal.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react' - -import { useApi } from '@/api/hooks/useApi' -import { ButtonPrimary } from '@/common/components/buttons' -import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' -import { RowGapBlock } from '@/common/components/page/PageContent' -import { TextMedium } from '@/common/components/typography' -import { useModal } from '@/common/hooks/useModal' -import { Address } from '@/common/types' - -import { PayoutModalCall } from '@/validators/modals/PayoutModal/types' - -interface Props { - validatorAddress: Address -} - -export const PayoutModal = () => { - const { modalData } = useModal() - const validatorAddress = modalData?.validatorAddress - - if (!validatorAddress) return null - - return -} - -const PayoutModalInner = ({ validatorAddress }: Props) => { - const { hideModal } = useModal() - const { api } = useApi() - - const handlePayout = async () => { - if (!api) { - console.error('API not available') - return - } - - try { - // TODO: Implement actual payout transaction - console.log('Claiming payout from validator:', validatorAddress) - hideModal() - } catch (error) { - console.error('Payout failed:', error) - } - } - - return ( - - - - - - You are about to claim your payout from this validator. This will transfer any earned - rewards to your account. - - - Validator Address: {validatorAddress} - - - Note: This is a preview implementation. The actual transaction will be implemented - in a separate PR for testing. - - - - - - Claim Payout - - - - ) -} diff --git a/packages/ui/src/validators/modals/PayoutModal/index.ts b/packages/ui/src/validators/modals/PayoutModal/index.ts deleted file mode 100644 index 9da0d56efb..0000000000 --- a/packages/ui/src/validators/modals/PayoutModal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { PayoutModal } from './PayoutModal' -export type { PayoutModalCall } from './types' diff --git a/packages/ui/src/validators/modals/PayoutModal/types.ts b/packages/ui/src/validators/modals/PayoutModal/types.ts deleted file mode 100644 index 1f7dc35025..0000000000 --- a/packages/ui/src/validators/modals/PayoutModal/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface PayoutModalCall { - modal: 'Payout' - data: { - validatorAddress: string - } -} diff --git a/packages/ui/src/validators/modals/SetNomineesModal/index.ts b/packages/ui/src/validators/modals/SetNomineesModal/index.ts index 46a408daff..06e76961f5 100644 --- a/packages/ui/src/validators/modals/SetNomineesModal/index.ts +++ b/packages/ui/src/validators/modals/SetNomineesModal/index.ts @@ -9,5 +9,3 @@ export type SetNomineesModalCall = ModalWithDataCall< > export * from './SetNomineesModal' - - diff --git a/packages/ui/src/validators/modals/StakeModal/StakeModal.tsx b/packages/ui/src/validators/modals/StakeModal/StakeModal.tsx index 4eb4444097..6644f22046 100644 --- a/packages/ui/src/validators/modals/StakeModal/StakeModal.tsx +++ b/packages/ui/src/validators/modals/StakeModal/StakeModal.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { encodeAddress } from '@/accounts/model/encodeAddress' import { useApi } from '@/api/hooks/useApi' import { ButtonSecondary } from '@/common/components/buttons' import { FailureModal } from '@/common/components/FailureModal' @@ -13,9 +14,8 @@ import { useModal } from '@/common/hooks/useModal' import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' import { transactionMachine } from '@/common/model/machines' import { Address } from '@/common/types' -import { encodeAddress } from '@/accounts/model/encodeAddress' -import { useStakingTransactions } from '@/validators/hooks/useStakingSDK' import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' +import { useStakingTransactions } from '@/validators/hooks/useStakingSDK' import { StakeModalCall } from './types' @@ -66,7 +66,9 @@ const StakeModalInner = ({ validatorAddress }: Props) => { - The transaction was canceled. Please try again if you want to nominate this validator. + + The transaction was canceled. Please try again if you want to nominate this validator. + @@ -109,8 +111,8 @@ const StakeModalInner = ({ validatorAddress }: Props) => { - Note: You must have bonded tokens before you can nominate. If you haven't bonded yet, please{' '} - use the "Bond" action first. Your nomination will take effect in the next era. + Note: You must have bonded tokens before you can nominate. If you haven't bonded yet, + please use the "Bond" action first. Your nomination will take effect in the next era. {!canAfford && paymentInfo?.partialFee && ( diff --git a/packages/ui/src/validators/modals/StakingTransactionModal/StakingTransactionModal.tsx b/packages/ui/src/validators/modals/StakingTransactionModal/StakingTransactionModal.tsx index bc7fdfb795..a37001de47 100644 --- a/packages/ui/src/validators/modals/StakingTransactionModal/StakingTransactionModal.tsx +++ b/packages/ui/src/validators/modals/StakingTransactionModal/StakingTransactionModal.tsx @@ -1,3 +1,4 @@ +import { SubmittableExtrinsic } from '@polkadot/api/types' import React from 'react' import { useApi } from '@/api/hooks/useApi' @@ -6,9 +7,7 @@ import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/ import { RowGapBlock } from '@/common/components/page/PageContent' import { TextMedium } from '@/common/components/typography' import { useModal } from '@/common/hooks/useModal' -import { SubmittableExtrinsic } from '@polkadot/api/types' import { Address } from '@/common/types' - import { StakingTransactionModalCall } from '@/validators/modals/StakingTransactionModal/types' interface Props { @@ -25,26 +24,26 @@ export const StakingTransactionModal = ({ title, description, transaction, signe const handleTransaction = async () => { if (!transaction || !api) { - console.error('Transaction or API not available') + //console.error('Transaction or API not available') return } try { // TODO: Implement proper transaction signing and submission - console.log(`Executing ${title} transaction for validator:`, validatorAddress) - console.log('Transaction:', transaction) - console.log('Signer:', signer) - + //console.log(`Executing ${title} transaction for validator:`, validatorAddress) + //console.log('Transaction:', transaction) + //console.log('Signer:', signer) + // For now, just show success message // In the actual implementation, this would: // 1. Sign the transaction // 2. Submit it to the network // 3. Wait for confirmation // 4. Show success/error states - + hideModal() } catch (error) { - console.error('Transaction failed:', error) + //console.error('Transaction failed:', error) } } diff --git a/packages/ui/src/validators/modals/StopStakingModal/index.ts b/packages/ui/src/validators/modals/StopStakingModal/index.ts index 023aa0f324..4727a6dd59 100644 --- a/packages/ui/src/validators/modals/StopStakingModal/index.ts +++ b/packages/ui/src/validators/modals/StopStakingModal/index.ts @@ -1,5 +1,4 @@ import { ModalWithDataCall } from '@/common/providers/modal/types' - import { MyStakingRole } from '@/validators/hooks/useMyStashPositions' export type StopStakingModalCall = ModalWithDataCall< @@ -11,5 +10,3 @@ export type StopStakingModalCall = ModalWithDataCall< > export * from './StopStakingModal' - - diff --git a/packages/ui/src/validators/modals/UnbondStakingModal/index.ts b/packages/ui/src/validators/modals/UnbondStakingModal/index.ts index 64ff558d7e..f469b3ca27 100644 --- a/packages/ui/src/validators/modals/UnbondStakingModal/index.ts +++ b/packages/ui/src/validators/modals/UnbondStakingModal/index.ts @@ -12,5 +12,3 @@ export type UnbondStakingModalCall = ModalWithDataCall< > export * from './UnbondStakingModal' - - diff --git a/packages/ui/src/validators/modals/validatorCard/ValidatorCard.tsx b/packages/ui/src/validators/modals/validatorCard/ValidatorCard.tsx index 5592e54471..70c1e63a3b 100644 --- a/packages/ui/src/validators/modals/validatorCard/ValidatorCard.tsx +++ b/packages/ui/src/validators/modals/validatorCard/ValidatorCard.tsx @@ -31,62 +31,71 @@ interface Props { isNominated?: boolean } -export const ValidatorCard = React.memo(({ cardNumber, validator, eraIndex, selectCard, totalCards, isNominated = false }: Props) => { - const hideModal = () => { - selectCard(null) - } - const onBackgroundClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) { - hideModal() +export const ValidatorCard = React.memo( + ({ cardNumber, validator, eraIndex, selectCard, totalCards, isNominated = false }: Props) => { + const hideModal = () => { + selectCard(null) } - } - useEscape(() => hideModal()) - const [activeTab, setActiveTab] = useState('Details') + const onBackgroundClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + hideModal() + } + } + useEscape(() => hideModal()) + const [activeTab, setActiveTab] = useState('Details') - const onClickRight = () => { - selectCard(cardNumber + 1) - } - const onClickLeft = () => { - selectCard(cardNumber - 1) - } + const onClickRight = () => { + selectCard(cardNumber + 1) + } + const onClickLeft = () => { + selectCard(cardNumber - 1) + } - const title = `Validor ${cardNumber} of ${totalCards}` - const tabs = [ - { - title: 'Validator details', - active: activeTab === 'Details', - onClick: () => setActiveTab('Details'), - }, - { title: 'Nominators', active: activeTab === 'Nominators', onClick: () => setActiveTab('Nominators') }, - ] + const title = `Validor ${cardNumber} of ${totalCards}` + const tabs = [ + { + title: 'Validator details', + active: activeTab === 'Details', + onClick: () => setActiveTab('Details'), + }, + { title: 'Nominators', active: activeTab === 'Nominators', onClick: () => setActiveTab('Nominators') }, + ] - return ( - - - - - {title} - - - - - = totalCards} - onClick={onClickRight} - > - - - - - - - - - {activeTab === 'Details' && } - {activeTab === 'Nominators' && } - - - ) -}) + return ( + + + + + {title} + + + + + = totalCards} + onClick={onClickRight} + > + + + + + + + + + {activeTab === 'Details' && ( + + )} + {activeTab === 'Nominators' && } + + + ) + } +) diff --git a/packages/ui/src/validators/types/index.ts b/packages/ui/src/validators/types/index.ts index 39739511b8..de888c6dd6 100644 --- a/packages/ui/src/validators/types/index.ts +++ b/packages/ui/src/validators/types/index.ts @@ -2,4 +2,4 @@ export * from './Validator' // filter export type Verification = null | 'verified' | 'unverified' -export type State = null | 'active' | 'waiting' \ No newline at end of file +export type State = null | 'active' | 'waiting' diff --git a/packages/ui/src/working-groups/queries/__generated__/workingGroups.generated.tsx b/packages/ui/src/working-groups/queries/__generated__/workingGroups.generated.tsx index e84351fd2e..557442eb8f 100644 --- a/packages/ui/src/working-groups/queries/__generated__/workingGroups.generated.tsx +++ b/packages/ui/src/working-groups/queries/__generated__/workingGroups.generated.tsx @@ -58,10 +58,10 @@ export type WorkerFieldsFragment = { } group: { __typename: 'WorkingGroup'; id: string; name: string } status: - | { __typename: 'WorkerStatusActive' } - | { __typename: 'WorkerStatusLeaving' } - | { __typename: 'WorkerStatusLeft' } - | { __typename: 'WorkerStatusTerminated' } + | { __typename: 'WorkerStatusActive' } + | { __typename: 'WorkerStatusLeaving' } + | { __typename: 'WorkerStatusLeft' } + | { __typename: 'WorkerStatusTerminated' } } export type PastWorkerFieldsFragment = { @@ -106,26 +106,26 @@ export type PastWorkerFieldsFragment = { }> | null } status: - | { __typename: 'WorkerStatusActive' } - | { __typename: 'WorkerStatusLeaving' } - | { - __typename: 'WorkerStatusLeft' - workerExitedEvent?: { - __typename: 'WorkerExitedEvent' - createdAt: any - inBlock: number - network: Types.Network - } | null - } - | { - __typename: 'WorkerStatusTerminated' - terminatedWorkerEvent?: { - __typename: 'TerminatedWorkerEvent' - createdAt: any - inBlock: number - network: Types.Network - } | null - } + | { __typename: 'WorkerStatusActive' } + | { __typename: 'WorkerStatusLeaving' } + | { + __typename: 'WorkerStatusLeft' + workerExitedEvent?: { + __typename: 'WorkerExitedEvent' + createdAt: any + inBlock: number + network: Types.Network + } | null + } + | { + __typename: 'WorkerStatusTerminated' + terminatedWorkerEvent?: { + __typename: 'TerminatedWorkerEvent' + createdAt: any + inBlock: number + network: Types.Network + } | null + } entry: { __typename: 'OpeningFilledEvent'; createdAt: any; inBlock: number; network: Types.Network } } @@ -185,10 +185,10 @@ export type WorkerDetailedFieldsFragment = { } group: { __typename: 'WorkingGroup'; id: string; name: string } status: - | { __typename: 'WorkerStatusActive' } - | { __typename: 'WorkerStatusLeaving' } - | { __typename: 'WorkerStatusLeft' } - | { __typename: 'WorkerStatusTerminated' } + | { __typename: 'WorkerStatusActive' } + | { __typename: 'WorkerStatusLeaving' } + | { __typename: 'WorkerStatusLeft' } + | { __typename: 'WorkerStatusTerminated' } } export type WorkingGroupFieldsFragment = { @@ -348,10 +348,10 @@ export type GetWorkersQuery = { } group: { __typename: 'WorkingGroup'; id: string; name: string } status: - | { __typename: 'WorkerStatusActive' } - | { __typename: 'WorkerStatusLeaving' } - | { __typename: 'WorkerStatusLeft' } - | { __typename: 'WorkerStatusTerminated' } + | { __typename: 'WorkerStatusActive' } + | { __typename: 'WorkerStatusLeaving' } + | { __typename: 'WorkerStatusLeft' } + | { __typename: 'WorkerStatusTerminated' } }> } @@ -406,26 +406,26 @@ export type GetPastWorkersQuery = { }> | null } status: - | { __typename: 'WorkerStatusActive' } - | { __typename: 'WorkerStatusLeaving' } - | { - __typename: 'WorkerStatusLeft' - workerExitedEvent?: { - __typename: 'WorkerExitedEvent' - createdAt: any - inBlock: number - network: Types.Network - } | null - } - | { - __typename: 'WorkerStatusTerminated' - terminatedWorkerEvent?: { - __typename: 'TerminatedWorkerEvent' - createdAt: any - inBlock: number - network: Types.Network - } | null - } + | { __typename: 'WorkerStatusActive' } + | { __typename: 'WorkerStatusLeaving' } + | { + __typename: 'WorkerStatusLeft' + workerExitedEvent?: { + __typename: 'WorkerExitedEvent' + createdAt: any + inBlock: number + network: Types.Network + } | null + } + | { + __typename: 'WorkerStatusTerminated' + terminatedWorkerEvent?: { + __typename: 'TerminatedWorkerEvent' + createdAt: any + inBlock: number + network: Types.Network + } | null + } entry: { __typename: 'OpeningFilledEvent'; createdAt: any; inBlock: number; network: Types.Network } }> } @@ -501,10 +501,10 @@ export type GetDetailedWorkersQuery = { } group: { __typename: 'WorkingGroup'; id: string; name: string } status: - | { __typename: 'WorkerStatusActive' } - | { __typename: 'WorkerStatusLeaving' } - | { __typename: 'WorkerStatusLeft' } - | { __typename: 'WorkerStatusTerminated' } + | { __typename: 'WorkerStatusActive' } + | { __typename: 'WorkerStatusLeaving' } + | { __typename: 'WorkerStatusLeft' } + | { __typename: 'WorkerStatusTerminated' } }> } @@ -570,10 +570,10 @@ export type GetWorkerQuery = { } group: { __typename: 'WorkingGroup'; id: string; name: string } status: - | { __typename: 'WorkerStatusActive' } - | { __typename: 'WorkerStatusLeaving' } - | { __typename: 'WorkerStatusLeft' } - | { __typename: 'WorkerStatusTerminated' } + | { __typename: 'WorkerStatusActive' } + | { __typename: 'WorkerStatusLeaving' } + | { __typename: 'WorkerStatusLeft' } + | { __typename: 'WorkerStatusTerminated' } } | null } @@ -632,18 +632,18 @@ export type WorkingGroupOpeningFieldsFragment = { expectedEnding?: any | null } status: - | { __typename: 'OpeningStatusCancelled' } - | { __typename: 'OpeningStatusFilled' } - | { __typename: 'OpeningStatusOpen' } + | { __typename: 'OpeningStatusCancelled' } + | { __typename: 'OpeningStatusFilled' } + | { __typename: 'OpeningStatusOpen' } applications: Array<{ __typename: 'WorkingGroupApplication' id: string status: - | { __typename: 'ApplicationStatusAccepted' } - | { __typename: 'ApplicationStatusCancelled' } - | { __typename: 'ApplicationStatusPending' } - | { __typename: 'ApplicationStatusRejected' } - | { __typename: 'ApplicationStatusWithdrawn' } + | { __typename: 'ApplicationStatusAccepted' } + | { __typename: 'ApplicationStatusCancelled' } + | { __typename: 'ApplicationStatusPending' } + | { __typename: 'ApplicationStatusRejected' } + | { __typename: 'ApplicationStatusWithdrawn' } }> openingfilledeventopening?: Array<{ __typename: 'OpeningFilledEvent' @@ -664,11 +664,11 @@ export type WorkingGroupOpeningDetailedFieldsFragment = { __typename: 'WorkingGroupApplication' id: string status: - | { __typename: 'ApplicationStatusAccepted' } - | { __typename: 'ApplicationStatusCancelled' } - | { __typename: 'ApplicationStatusPending' } - | { __typename: 'ApplicationStatusRejected' } - | { __typename: 'ApplicationStatusWithdrawn' } + | { __typename: 'ApplicationStatusAccepted' } + | { __typename: 'ApplicationStatusCancelled' } + | { __typename: 'ApplicationStatusPending' } + | { __typename: 'ApplicationStatusRejected' } + | { __typename: 'ApplicationStatusWithdrawn' } applicant: { __typename: 'Membership' id: string @@ -717,9 +717,9 @@ export type WorkingGroupOpeningDetailedFieldsFragment = { expectedEnding?: any | null } status: - | { __typename: 'OpeningStatusCancelled' } - | { __typename: 'OpeningStatusFilled' } - | { __typename: 'OpeningStatusOpen' } + | { __typename: 'OpeningStatusCancelled' } + | { __typename: 'OpeningStatusFilled' } + | { __typename: 'OpeningStatusOpen' } openingfilledeventopening?: Array<{ __typename: 'OpeningFilledEvent' workersHired: Array<{ __typename: 'Worker'; id: string }> @@ -775,18 +775,18 @@ export type GetWorkingGroupOpeningsQuery = { expectedEnding?: any | null } status: - | { __typename: 'OpeningStatusCancelled' } - | { __typename: 'OpeningStatusFilled' } - | { __typename: 'OpeningStatusOpen' } + | { __typename: 'OpeningStatusCancelled' } + | { __typename: 'OpeningStatusFilled' } + | { __typename: 'OpeningStatusOpen' } applications: Array<{ __typename: 'WorkingGroupApplication' id: string status: - | { __typename: 'ApplicationStatusAccepted' } - | { __typename: 'ApplicationStatusCancelled' } - | { __typename: 'ApplicationStatusPending' } - | { __typename: 'ApplicationStatusRejected' } - | { __typename: 'ApplicationStatusWithdrawn' } + | { __typename: 'ApplicationStatusAccepted' } + | { __typename: 'ApplicationStatusCancelled' } + | { __typename: 'ApplicationStatusPending' } + | { __typename: 'ApplicationStatusRejected' } + | { __typename: 'ApplicationStatusWithdrawn' } }> openingfilledeventopening?: Array<{ __typename: 'OpeningFilledEvent' @@ -828,11 +828,11 @@ export type GetWorkingGroupOpeningQuery = { __typename: 'WorkingGroupApplication' id: string status: - | { __typename: 'ApplicationStatusAccepted' } - | { __typename: 'ApplicationStatusCancelled' } - | { __typename: 'ApplicationStatusPending' } - | { __typename: 'ApplicationStatusRejected' } - | { __typename: 'ApplicationStatusWithdrawn' } + | { __typename: 'ApplicationStatusAccepted' } + | { __typename: 'ApplicationStatusCancelled' } + | { __typename: 'ApplicationStatusPending' } + | { __typename: 'ApplicationStatusRejected' } + | { __typename: 'ApplicationStatusWithdrawn' } applicant: { __typename: 'Membership' id: string @@ -881,9 +881,9 @@ export type GetWorkingGroupOpeningQuery = { expectedEnding?: any | null } status: - | { __typename: 'OpeningStatusCancelled' } - | { __typename: 'OpeningStatusFilled' } - | { __typename: 'OpeningStatusOpen' } + | { __typename: 'OpeningStatusCancelled' } + | { __typename: 'OpeningStatusFilled' } + | { __typename: 'OpeningStatusOpen' } openingfilledeventopening?: Array<{ __typename: 'OpeningFilledEvent' workersHired: Array<{ __typename: 'Worker'; id: string }> @@ -1163,11 +1163,11 @@ export type WorkingGroupApplicationFieldsFragment = { }> | null } status: - | { __typename: 'ApplicationStatusAccepted' } - | { __typename: 'ApplicationStatusCancelled' } - | { __typename: 'ApplicationStatusPending' } - | { __typename: 'ApplicationStatusRejected' } - | { __typename: 'ApplicationStatusWithdrawn' } + | { __typename: 'ApplicationStatusAccepted' } + | { __typename: 'ApplicationStatusCancelled' } + | { __typename: 'ApplicationStatusPending' } + | { __typename: 'ApplicationStatusRejected' } + | { __typename: 'ApplicationStatusWithdrawn' } createdInEvent: { __typename: 'AppliedOnOpeningEvent'; createdAt: any; inBlock: number; network: Types.Network } } @@ -1237,11 +1237,11 @@ export type GetWorkingGroupApplicationsQuery = { }> | null } status: - | { __typename: 'ApplicationStatusAccepted' } - | { __typename: 'ApplicationStatusCancelled' } - | { __typename: 'ApplicationStatusPending' } - | { __typename: 'ApplicationStatusRejected' } - | { __typename: 'ApplicationStatusWithdrawn' } + | { __typename: 'ApplicationStatusAccepted' } + | { __typename: 'ApplicationStatusCancelled' } + | { __typename: 'ApplicationStatusPending' } + | { __typename: 'ApplicationStatusRejected' } + | { __typename: 'ApplicationStatusWithdrawn' } createdInEvent: { __typename: 'AppliedOnOpeningEvent'; createdAt: any; inBlock: number; network: Types.Network } }> } @@ -1331,11 +1331,11 @@ export type GetWorkingGroupApplicationQuery = { }> | null } status: - | { __typename: 'ApplicationStatusAccepted' } - | { __typename: 'ApplicationStatusCancelled' } - | { __typename: 'ApplicationStatusPending' } - | { __typename: 'ApplicationStatusRejected' } - | { __typename: 'ApplicationStatusWithdrawn' } + | { __typename: 'ApplicationStatusAccepted' } + | { __typename: 'ApplicationStatusCancelled' } + | { __typename: 'ApplicationStatusPending' } + | { __typename: 'ApplicationStatusRejected' } + | { __typename: 'ApplicationStatusWithdrawn' } createdInEvent: { __typename: 'AppliedOnOpeningEvent'; createdAt: any; inBlock: number; network: Types.Network } } | null } @@ -1460,13 +1460,13 @@ export type GetWorkerUnstakingDetailsQuery = { workerByUniqueInput?: { __typename: 'Worker' status: - | { __typename: 'WorkerStatusActive' } - | { - __typename: 'WorkerStatusLeaving' - workerStartedLeavingEvent?: { __typename: 'WorkerStartedLeavingEvent'; createdAt: any } | null - } - | { __typename: 'WorkerStatusLeft' } - | { __typename: 'WorkerStatusTerminated' } + | { __typename: 'WorkerStatusActive' } + | { + __typename: 'WorkerStatusLeaving' + workerStartedLeavingEvent?: { __typename: 'WorkerStartedLeavingEvent'; createdAt: any } | null + } + | { __typename: 'WorkerStatusLeft' } + | { __typename: 'WorkerStatusTerminated' } application: { __typename: 'WorkingGroupApplication' opening: { __typename: 'WorkingGroupOpening'; unstakingPeriod: number } From d42df45f3751c3a9d811d2ff5cbe3e8c589e2546 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Tue, 2 Dec 2025 13:13:02 +0100 Subject: [PATCH 45/67] reactivate test --- .../ui/test/proposals/hooks/useProposals.test.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/ui/test/proposals/hooks/useProposals.test.tsx b/packages/ui/test/proposals/hooks/useProposals.test.tsx index f792f8dad6..7edd1839e6 100644 --- a/packages/ui/test/proposals/hooks/useProposals.test.tsx +++ b/packages/ui/test/proposals/hooks/useProposals.test.tsx @@ -28,14 +28,13 @@ describe('useProposals', () => { }) describe('Status: active | past', () => { - // flaky test: #4887 - //it('Status: active', async () => { - // const result = await loadUseProposals({ status: 'active' }) - // expect(result.proposals.length).toBeGreaterThan(0) - // result.proposals.forEach((proposal) => { - // expect(proposalActiveStatuses.includes(proposal.status)).toBeTruthy() - // }) - //}) + it('Status: active', async () => { + const result = await loadUseProposals({ status: 'active' }) + expect(result.proposals.length).toBeGreaterThan(0) + result.proposals.forEach((proposal) => { + expect(proposalActiveStatuses.includes(proposal.status)).toBeTruthy() + }) + }) it('Status: past', async () => { const result = await loadUseProposals({ status: 'past' }) From 9b76c72d60e1270826244a20189853112ad348ef Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 10:54:08 +0100 Subject: [PATCH 46/67] await modal call assertion --- packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx b/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx index 6daa3e7720..affec1e279 100644 --- a/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx +++ b/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx @@ -113,7 +113,7 @@ describe('UI: Announce Candidacy Modal', () => { modal: 'OnBoardingModal', } - expect(showModal).toBeCalledTimes(1) + await waitFor(() => expect(showModal).toBeCalledTimes(1)) expect(showModal).toBeCalledWith({ ...onBoardingModalCall }) }) From c3cb22b60beba072c653378dd27dfce63fd73950 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 11:43:00 +0100 Subject: [PATCH 47/67] await async issues --- .../ui/test/forum/modals/CreateThreadModal.test.tsx | 12 +++++++----- .../working-groups/modals/ApplyForRoleModal.test.tsx | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/ui/test/forum/modals/CreateThreadModal.test.tsx b/packages/ui/test/forum/modals/CreateThreadModal.test.tsx index f59dc32880..4b3d7dd826 100644 --- a/packages/ui/test/forum/modals/CreateThreadModal.test.tsx +++ b/packages/ui/test/forum/modals/CreateThreadModal.test.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, render, screen } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import BN from 'bn.js' import React from 'react' import { generatePath, MemoryRouter, Route } from 'react-router-dom' @@ -76,12 +76,14 @@ describe('CreateThreadModal', () => { }) describe('Requirements failed', () => { - it('No active member', () => { + it('No active member', async () => { useMyMemberships.active = undefined renderModal() - expect(useModal.showModal).toBeCalledWith({ - modal: 'OnBoardingModal', - }) + await waitFor(() => + expect(useModal.showModal).toBeCalledWith({ + modal: 'OnBoardingModal', + }) + ) }) it('Insufficient funds for minimum fee', async () => { diff --git a/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx b/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx index f412b6383e..372801107b 100644 --- a/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx +++ b/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx @@ -134,9 +134,11 @@ describe('UI: ApplyForRoleModal', () => { await renderModal() - expect(showModal).toBeCalledWith({ - modal: 'OnBoardingModal', - }) + await waitFor(() => + expect(showModal).toBeCalledWith({ + modal: 'OnBoardingModal', + }) + ) showModal.mockClear() }) From c1bb0380ef1661298e1c91068b0dee010c009e8c Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 12:42:33 +0100 Subject: [PATCH 48/67] used mock apollo providers for mention test --- .../test/common/components/Mention.test.tsx | 96 +++++++++++++++---- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/packages/ui/test/common/components/Mention.test.tsx b/packages/ui/test/common/components/Mention.test.tsx index 9afa56f5aa..b4a1da1ebf 100644 --- a/packages/ui/test/common/components/Mention.test.tsx +++ b/packages/ui/test/common/components/Mention.test.tsx @@ -74,7 +74,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -82,12 +86,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) @@ -127,7 +139,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -135,12 +151,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) @@ -176,7 +200,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -184,12 +212,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) @@ -237,7 +273,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - renderResult = render() + renderResult = render( + + + + ) }) it('should render component', () => { @@ -245,12 +285,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) @@ -261,7 +309,11 @@ describe('UI: Mention', () => { it('should render correct distance to the ending date in the future', () => { const futureDate = addYears(new Date(), 3).toISOString() - renderResult.rerender() + renderResult.rerender( + + + + ) const expected = formatDistanceToNowStrict(new Date(futureDate)) expect(screen.getByText(`mentions.tooltips.opening.duration ${expected}`)).toBeInTheDocument() }) @@ -320,7 +372,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -328,12 +384,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) From d78767e5f7dc0724dc2fb25287cff3746c038deb Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 13:11:44 +0100 Subject: [PATCH 49/67] proposaldisccusion apollo provider wrapper --- .../ui/test/common/components/Mention.test.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/ui/test/common/components/Mention.test.tsx b/packages/ui/test/common/components/Mention.test.tsx index b4a1da1ebf..84947d4f10 100644 --- a/packages/ui/test/common/components/Mention.test.tsx +++ b/packages/ui/test/common/components/Mention.test.tsx @@ -438,7 +438,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -446,12 +450,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) From b584e744e9f89b160cfd1a529bdf84ab41755c06 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Wed, 3 Dec 2025 14:52:13 +0100 Subject: [PATCH 50/67] WGL: Vested / Discretionary spending (#4906) --- packages/ui/src/app/GlobalModals.tsx | 3 + .../MemberRoleToggle.stories.tsx | 94 ++++- .../MemberProfile/MemberRoleToggle.tsx | 34 +- .../working-groups/hooks/useIsLeadForGroup.ts | 27 ++ .../PayWorkerModal/PayWorkerModal.stories.tsx | 154 +++++++ .../modals/PayWorkerModal/PayWorkerModal.tsx | 381 ++++++++++++++++++ .../modals/PayWorkerModal/index.ts | 2 + .../modals/PayWorkerModal/machine.ts | 88 ++++ .../modals/PayWorkerModal/types.ts | 8 + .../queries/workingGroups.graphql | 16 + 10 files changed, 801 insertions(+), 6 deletions(-) create mode 100644 packages/ui/src/working-groups/hooks/useIsLeadForGroup.ts create mode 100644 packages/ui/src/working-groups/modals/PayWorkerModal/PayWorkerModal.stories.tsx create mode 100644 packages/ui/src/working-groups/modals/PayWorkerModal/PayWorkerModal.tsx create mode 100644 packages/ui/src/working-groups/modals/PayWorkerModal/index.ts create mode 100644 packages/ui/src/working-groups/modals/PayWorkerModal/machine.ts create mode 100644 packages/ui/src/working-groups/modals/PayWorkerModal/types.ts diff --git a/packages/ui/src/app/GlobalModals.tsx b/packages/ui/src/app/GlobalModals.tsx index ba2964b705..0d85eabee6 100644 --- a/packages/ui/src/app/GlobalModals.tsx +++ b/packages/ui/src/app/GlobalModals.tsx @@ -93,6 +93,7 @@ import { IncreaseWorkerStakeModalCall, } from '@/working-groups/modals/IncreaseWorkerStakeModal' import { LeaveRoleModal, LeaveRoleModalCall } from '@/working-groups/modals/LeaveRoleModal' +import { PayWorkerModal, PayWorkerModalCall } from '@/working-groups/modals/PayWorkerModal' type ModalNamesBase = | ModalName @@ -124,6 +125,7 @@ type ModalNamesBase = | ModalName | ModalName | ModalName + | ModalName | ModalName | ModalName // | ModalName @@ -193,6 +195,7 @@ const modals: Record = { RevealVote: , RecoverBalance: , IncreaseWorkerStake: , + PayWorker: , InviteMemberModal: , OnBoardingModal: , RestoreVotes: , diff --git a/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.stories.tsx b/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.stories.tsx index 103a7fae03..0980249a27 100644 --- a/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.stories.tsx +++ b/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.stories.tsx @@ -1,21 +1,85 @@ import { BN_THOUSAND } from '@polkadot/util' -import { Meta, Story } from '@storybook/react' +import { Meta, Story, StoryContext } from '@storybook/react' import BN from 'bn.js' import React from 'react' import { TemplateBlock, ModalBlock, WhiteBlock } from '@/common/components/storybookParts/previewStyles' +import { member } from '@/mocks/data/members' +import { MocksParameters } from '@/mocks/providers' import { MockApolloProvider } from '@/mocks/components/storybook/MockApolloProvider' import { randomBlock } from '@/mocks/helpers/randomBlock' +import { Member } from '@/memberships/types' +import { GetRoleAccountsDocument } from '@/working-groups/queries' import { MemberRoleToggle, MemberRoleToggleProps } from './MemberRoleToggle' +const createMockMember = (): Member => { + const membership = member('alice') + return { + id: membership.id, + handle: membership.handle, + rootAccount: membership.rootAccount, + controllerAccount: membership.controllerAccount, + boundAccounts: membership.boundAccounts || [], + inviteCount: membership.inviteCount, + roles: [], + isVerified: membership.isVerified, + isFoundingMember: membership.isFoundingMember, + isCouncilMember: membership.isCouncilMember, + createdAt: membership.createdAt, + } +} + export default { title: 'Member/MemberRoleToggle', component: MemberRoleToggle, + parameters: { + mocks: ({ args }: StoryContext): MocksParameters => { + const alice = member('alice') + const roleAccount = args?.role?.roleAccount || 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT' + const isLead = args?.role?.isLead || false + + return { + accounts: { + active: { + member: alice, + account: { name: 'Alice Account', address: roleAccount }, + }, + list: [ + { member: alice, account: { name: 'Alice Account', address: roleAccount } }, + { member: alice, account: { name: 'Alice Controller', address: alice.controllerAccount } }, + ], + }, + gql: { + queries: [ + { + query: GetRoleAccountsDocument, + resolver: (options) => { + const where = options?.variables?.where + if ( + where?.membership?.id_eq === alice.id && + where?.group?.id_eq === args?.role?.group?.id && + where?.isLead_eq === true + ) { + return { + loading: false, + data: { + workers: isLead ? [{ roleAccount }] : [], + }, + } + } + return { loading: false, data: { workers: [] } } + }, + }, + ], + }, + } + }, + }, } as Meta const Template: Story = (args) => ( - + @@ -29,6 +93,7 @@ const Template: Story = (args) => ( export const Default = Template.bind({}) Default.args = { + member: createMockMember(), role: { id: '123', runtimeId: 12, @@ -50,3 +115,28 @@ Default.args = { status: 'WorkerStatusActive', }, } + +export const AsLead = Template.bind({}) +AsLead.args = { + member: createMockMember(), + role: { + id: '123', + runtimeId: 12, + group: { id: 'forumWorkingGroup', name: 'forum' }, + isLead: true, + membership: { id: '0', controllerAccount: 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT' }, + rewardPerBlock: BN_THOUSAND, + stake: new BN(192837021), + owedReward: new BN(1000), + minStake: new BN(400), + roleAccount: 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT', // Same as controllerAccount to show button + rewardAccount: 'j4W7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf', + stakeAccount: 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT', + hiredAtBlock: { + ...randomBlock(), + }, + applicationId: '0', + openingId: '0', + status: 'WorkerStatusActive', + }, +} diff --git a/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.tsx b/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.tsx index 24e767da05..ec176bd39e 100644 --- a/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.tsx +++ b/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components' import { UnknownAccountInfo } from '@/accounts/components/UnknownAccountInfo' import { BlockTime } from '@/common/components/BlockTime' -import { ButtonGhost, ResponsiveButtonsGroup } from '@/common/components/buttons' +import { ButtonGhost, ButtonPrimary, ResponsiveButtonsGroup } from '@/common/components/buttons' import { LinkButtonGhost } from '@/common/components/buttons/LinkButtons' import { ToggleableItem, ToggleButton } from '@/common/components/buttons/Toggle' import { Arrow } from '@/common/components/icons' @@ -14,9 +14,11 @@ import { useModal } from '@/common/hooks/useModal' import { useToggle } from '@/common/hooks/useToggle' import { Member } from '@/memberships/types' import { workerRoleTitle } from '@/working-groups/helpers' +import { useIsLeadForGroup } from '@/working-groups/hooks/useIsLeadForGroup' import { useRewardPeriod } from '@/working-groups/hooks/useRewardPeriod' import { useWorkerEarnings } from '@/working-groups/hooks/useWorkerEarnings' import { ApplicationDetailsModalCall } from '@/working-groups/modals/ApplicationDetailsModal' +import { PayWorkerModalCall } from '@/working-groups/modals/PayWorkerModal' import { WorkerWithDetails } from '@/working-groups/types' export interface MemberRoleToggleProps { @@ -26,12 +28,19 @@ export interface MemberRoleToggleProps { export const MemberRoleToggle = ({ role }: MemberRoleToggleProps) => { const { showModal } = useModal() + const isLead = useIsLeadForGroup(role.group.id) const showApplicationModal = useCallback(() => { showModal({ modal: 'ApplicationDetails', data: { applicationId: role.applicationId }, }) }, [role]) + const showPayWorkerModal = useCallback(() => { + showModal({ + modal: 'PayWorker', + data: { worker: role }, + }) + }, [role]) const { earnings } = useWorkerEarnings(role.id) const rewardPeriod = useRewardPeriod(role.group.id) const [isOpen, toggleOpen] = useToggle() @@ -62,9 +71,16 @@ export const MemberRoleToggle = ({ role }: MemberRoleToggleProps) => { - - - + + + + + {isLead && ( + + Pay Worker + + )} + {/** TODO fix calculation @@ -153,3 +169,13 @@ const MemberRoleTable = styled(SidePaneTable)` display: none; } ` + +const EarnedTotalContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; +` + +const PayWorkerButton = styled(ButtonPrimary)` + margin-left: auto; +` diff --git a/packages/ui/src/working-groups/hooks/useIsLeadForGroup.ts b/packages/ui/src/working-groups/hooks/useIsLeadForGroup.ts new file mode 100644 index 0000000000..7a4e33fab0 --- /dev/null +++ b/packages/ui/src/working-groups/hooks/useIsLeadForGroup.ts @@ -0,0 +1,27 @@ +import { useMemo } from 'react' + +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' +import { useRoleAccount } from '@/working-groups/hooks/useRoleAccount' +import { GroupIdName, WorkerStatusToTypename } from '@/working-groups/types' + +export const useIsLeadForGroup = (groupId: GroupIdName): boolean => { + const { active } = useMyMemberships() + const { allAccounts } = useMyAccounts() + const { roleAccount, isLoading } = useRoleAccount({ + membership: { id_eq: active?.id }, + group: { id_eq: groupId }, + isLead_eq: true, + status_json: { isTypeOf_eq: WorkerStatusToTypename.active }, + }) + + return useMemo(() => { + if (isLoading || !roleAccount || !active) { + return false + } + + // Check if the lead worker's role account is in the user's accounts + const accountAddresses = new Set(allAccounts.map((acc) => acc.address)) + return accountAddresses.has(roleAccount) + }, [roleAccount, isLoading, allAccounts, active]) +} diff --git a/packages/ui/src/working-groups/modals/PayWorkerModal/PayWorkerModal.stories.tsx b/packages/ui/src/working-groups/modals/PayWorkerModal/PayWorkerModal.stories.tsx new file mode 100644 index 0000000000..2e55911d2c --- /dev/null +++ b/packages/ui/src/working-groups/modals/PayWorkerModal/PayWorkerModal.stories.tsx @@ -0,0 +1,154 @@ +import { BN_THOUSAND } from '@polkadot/util' +import { Meta, Story } from '@storybook/react' +import BN from 'bn.js' +import React from 'react' +import { HashRouter } from 'react-router-dom' + +import { ModalContext } from '@/common/providers/modal/context' +import { MockApolloProvider } from '@/mocks/components/storybook/MockApolloProvider' +import { member } from '@/mocks/data/members' +import { randomBlock } from '@/mocks/helpers/randomBlock' +import { MocksParameters } from '@/mocks/providers' +import { GetRoleAccountsDocument, GetWorkingGroupDocument } from '@/working-groups/queries' + +import { PayWorkerModal } from './PayWorkerModal' + +const mockWorker = { + id: '123', + runtimeId: 12, + group: { id: 'forumWorkingGroup', name: 'forum' }, + isLead: false, + membership: { id: '0', controllerAccount: 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT' }, + rewardPerBlock: BN_THOUSAND, + stake: new BN(192837021), + owedReward: new BN(1000), + minStake: new BN(400), + roleAccount: 'j4W7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf', + rewardAccount: 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT', + stakeAccount: 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT', + hiredAtBlock: { + ...randomBlock(), + }, + applicationId: '0', + openingId: '0', + status: 'WorkerStatusActive', +} + +export default { + title: 'WorkingGroup/PayWorkerModal', + component: PayWorkerModal, + parameters: { + mocks: (): MocksParameters => { + const alice = member('alice') + const roleAccount = 'j4VdDQVdwFYfQ2MvEdLT2EYZx4ALPQQ6yMyZopKoZEQmXcJrT' + + return { + accounts: { + active: { + member: alice, + account: { name: 'Alice Role Account', address: roleAccount }, + }, + list: [ + { member: alice, account: { name: 'Alice Role Account', address: roleAccount } }, + { member: alice, account: { name: 'Alice Controller', address: alice.controllerAccount } }, + ], + }, + chain: { + tx: { + forumWorkingGroup: { + spendFromBudget: { + event: 'BudgetSpending', + }, + vestedSpendFromBudget: { + event: 'VestedBudgetSpending', + }, + }, + }, + }, + gql: { + queries: [ + { + query: GetRoleAccountsDocument, + resolver: (options) => { + const where = options?.variables?.where + if ( + where?.membership?.id_eq === alice.id && + where?.group?.id_eq === mockWorker.group.id && + where?.isLead_eq === true + ) { + return { + loading: false, + data: { + workers: [{ roleAccount }], + }, + } + } + return { loading: false, data: { workers: [] } } + }, + }, + { + query: GetWorkingGroupDocument, + resolver: (options) => { + if (options?.variables?.where?.name === mockWorker.group.id) { + return { + loading: false, + data: { + workingGroupByUniqueInput: { + __typename: 'WorkingGroup', + id: mockWorker.group.id, + name: mockWorker.group.name, + budget: '250000000000000', // 250k JOY in planck + metadata: { + __typename: 'WorkingGroupMetadata', + about: 'Mock working group about text', + description: 'Mock working group description', + status: 'Active', + statusMessage: 'All systems go', + }, + workers: [], + leader: { + __typename: 'Worker', + id: 'leader-1', + runtimeId: 1, + stake: '0', + rewardPerBlock: '0', + membershipId: alice.id, + isActive: true, + }, + }, + }, + } + } + return { loading: false, data: { workingGroupByUniqueInput: null } } + }, + }, + ], + }, + } + }, + }, +} as Meta + +const Template: Story = () => { + return ( + + + undefined, + hideModal: () => undefined, + modal: 'PayWorker', + }} + > + + + + + ) +} + +export const Default = Template.bind({}) +Default.args = {} diff --git a/packages/ui/src/working-groups/modals/PayWorkerModal/PayWorkerModal.tsx b/packages/ui/src/working-groups/modals/PayWorkerModal/PayWorkerModal.tsx new file mode 100644 index 0000000000..81cbf6d2b5 --- /dev/null +++ b/packages/ui/src/working-groups/modals/PayWorkerModal/PayWorkerModal.tsx @@ -0,0 +1,381 @@ +import { SubmittableExtrinsic } from '@polkadot/api/types' +import BN from 'bn.js' +import React, { useEffect, useState } from 'react' +import styled from 'styled-components' + +import { SelectAccount } from '@/accounts/components/SelectAccount' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { accountOrNamed } from '@/accounts/model/accountOrNamed' +import { Account } from '@/accounts/types' +import { Api } from '@/api' +import { useApi } from '@/api/hooks/useApi' +import { ButtonPrimary, ButtonSecondary } from '@/common/components/buttons' +import { FailureModal } from '@/common/components/FailureModal' +import { InputComponent, InputText, InputTextarea, TokenInput } from '@/common/components/forms' +import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' +import { SuccessModal } from '@/common/components/SuccessModal' +import { TextMedium } from '@/common/components/typography' +import { useCurrentBlockNumber } from '@/common/hooks/useCurrentBlockNumber' +import { useMachine } from '@/common/hooks/useMachine' +import { useModal } from '@/common/hooks/useModal' +import { SignTransactionModal } from '@/common/modals/SignTransactionModal/SignTransactionModal' +import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' +import { useRoleAccount } from '@/working-groups/hooks/useRoleAccount' +import { useWorkingGroup } from '@/working-groups/hooks/useWorkingGroup' +import { getGroup } from '@/working-groups/model/getGroup' +import { WorkerStatusToTypename } from '@/working-groups/types' + +import { payWorkerMachine } from './machine' +import { PayWorkerModalCall, PaymentType } from './types' + +const MIN_STARTING_BLOCK_OFFSET = 10 + +const getTransaction = ( + api: Api, + groupId: string, + paymentType: PaymentType, + accountId: string, + amount: BN, + rationale: string, + perBlock?: BN, + startingBlock?: number +): SubmittableExtrinsic<'rxjs'> | undefined => { + const group = getGroup(api, groupId as any) + if (!group) return undefined + + if (paymentType === 'discretionary') { + return group.spendFromBudget(accountId, amount, rationale) + } else { + if (!perBlock || startingBlock === undefined) return undefined + return group.vestedSpendFromBudget(accountId, { locked: amount, perBlock, startingBlock }, rationale) + } +} + +export const PayWorkerModal = () => { + const { api } = useApi() + const { hideModal, modalData } = useModal() + const { worker } = modalData + const { active } = useMyMemberships() + const { allAccounts } = useMyAccounts() + const currentBlock = useCurrentBlockNumber() + const [state, send] = useMachine(payWorkerMachine) + const [paymentType, setPaymentType] = useState(null) + const [selectedAccount, setSelectedAccount] = useState(() => + accountOrNamed(allAccounts, worker.rewardAccount, 'Worker reward account') + ) + + // Update selected account when accounts load or worker changes + useEffect(() => { + if (allAccounts.length > 0 && !selectedAccount) { + setSelectedAccount(accountOrNamed(allAccounts, worker.rewardAccount, 'Worker reward account')) + } + }, [allAccounts, worker.rewardAccount, selectedAccount]) + const [amount, setAmount] = useState() + const [rationale, setRationale] = useState('') + const [perBlock, setPerBlock] = useState() + const [startingBlockInput, setStartingBlockInput] = useState('') + + const { roleAccount, isLoading: isLoadingRoleAccount } = useRoleAccount({ + membership: { id_eq: active?.id }, + group: { id_eq: worker.group.id }, + isLead_eq: true, + status_json: { isTypeOf_eq: WorkerStatusToTypename.active }, + }) + const { group: workingGroup, isLoading: isLoadingWorkingGroup } = useWorkingGroup({ name: worker.group.id }) + + if (!api) { + return null + } + + // Show error if user is not a lead (only check in initial state) + if (state.matches('selectPaymentType') && !isLoadingRoleAccount && !roleAccount) { + return You must be a lead for this working group to pay workers. + } + + // Show loading state while checking role account (only in initial state) + if (state.matches('selectPaymentType') && isLoadingRoleAccount) { + return ( + + + + Loading... + + + ) + } + + if (state.matches('selectPaymentType')) { + return ( + + + + Select payment type: + + { + setPaymentType('discretionary') + send('SELECT_TYPE', { paymentType: 'discretionary' }) + }} + > + Discretionary Spending (spendFromBudget) + + { + setPaymentType('vested') + send('SELECT_TYPE', { paymentType: 'vested' }) + }} + > + Vested Spending (vestedSpendFromBudget) + + + + + ) + } + + if (state.matches('prepare')) { + // Use paymentType from machine context if available, otherwise use local state + const currentPaymentType = state.context.paymentType || paymentType + const isVested = currentPaymentType === 'vested' + const accountId = selectedAccount?.address + const minStartingBlockBn = currentBlock ? currentBlock.addn(MIN_STARTING_BLOCK_OFFSET) : undefined + const trimmedStartingBlockInput = startingBlockInput.trim() + const availableBudget = workingGroup?.budget + const hasInsufficientBudget = Boolean(amount && availableBudget && amount.gt(availableBudget)) + const amountMessage = (() => { + if (isLoadingWorkingGroup) { + return 'Checking available budget...' + } + if (!availableBudget) { + return 'Could not fetch group budget. Try again in a moment.' + } + if (!amount || amount.isZero()) { + return `Available budget: ${availableBudget.div(new BN(10000000000)).toString()} JOY` + } + if (hasInsufficientBudget) { + return `Amount exceeds available budget (${availableBudget.div(new BN(10000000000)).toString()} JOY).` + } + return `Available budget: ${availableBudget.div(new BN(10000000000)).toString()} JOY` + })() + const startingBlockEvaluation = (() => { + if (!isVested) { + return { resolved: undefined, error: undefined, info: undefined } + } + const baseBlock = currentBlock + const minBlock = minStartingBlockBn + + if (!trimmedStartingBlockInput) { + if (!minBlock) { + return { + resolved: undefined, + error: 'Waiting for latest block number...', + info: undefined, + } + } + return { + resolved: minBlock, + error: undefined, + info: `Will use block ${minBlock.toString()} (current block + ${MIN_STARTING_BLOCK_OFFSET}).`, + } + } + + const isRelative = trimmedStartingBlockInput.startsWith('+') + const numericPart = isRelative ? trimmedStartingBlockInput.slice(1) : trimmedStartingBlockInput + + if (!/^\d+$/.test(numericPart)) { + return { + resolved: undefined, + error: 'Enter a positive number or +offset', + info: undefined, + } + } + + const numericBn = new BN(numericPart) + let targetBn: BN | undefined + + if (isRelative) { + if (!baseBlock) { + return { + resolved: undefined, + error: 'Waiting for current block to handle relative value', + info: undefined, + } + } + targetBn = baseBlock.add(numericBn) + } else { + targetBn = numericBn + } + + if (minBlock && targetBn.lt(minBlock)) { + return { + resolved: undefined, + error: `Block must be at least ${minBlock.toString()}`, + info: undefined, + } + } + + return { + resolved: targetBn, + error: undefined, + info: `Will use block ${targetBn.toString()}${ + isRelative && baseBlock ? ` (current block ${baseBlock.toString()} + ${numericPart})` : '' + }.`, + } + })() + + const resolvedStartingBlock = startingBlockEvaluation.resolved?.toNumber() + const perBlockMessage = perBlock + ? `Converted to ${perBlock.toString()} HAPI for the transaction.` + : 'Enter the amount in JOY; it will be converted to HAPI automatically.' + const canSubmit = + accountId && + amount && + !amount.isZero() && + rationale && + !hasInsufficientBudget && + (!isVested || + (perBlock && !perBlock.isZero() && resolvedStartingBlock !== undefined && !startingBlockEvaluation.error)) + + return ( + + + + + + + + + setAmount(value)} placeholder="0" /> + + + {isVested && ( + <> + + setPerBlock(value)} + placeholder="0" + /> + + + + setStartingBlockInput(event.target.value)} + placeholder={minStartingBlockBn?.toString()} + /> + + + )} + + + setRationale(e.target.value)} + placeholder="Reason for payment" + /> + + + + { + if (canSubmit && accountId && amount && rationale) { + send({ + type: 'DONE', + form: { + paymentType: currentPaymentType!, + accountId, + amount, + rationale, + perBlock, + startingBlock: resolvedStartingBlock, + }, + }) + } + }} + disabled={!canSubmit} + > + Continue + + + + ) + } + + if (state.matches('transaction') && state.context.accountId && state.context.amount && state.context.rationale) { + // Ensure we have roleAccount before proceeding with transaction + if (!roleAccount) { + return ( + Unable to proceed: role account not found. Please try again. + ) + } + + const transaction = getTransaction( + api, + worker.group.id, + state.context.paymentType!, + state.context.accountId, + state.context.amount, + state.context.rationale, + state.context.perBlock, + state.context.startingBlock + ) + + if (!transaction) { + return Failed to create transaction. Please try again. + } + + return ( + + + You are about to pay {state.context.amount.toString()} JOY to worker {worker.id} using{' '} + {state.context.paymentType === 'discretionary' ? 'discretionary spending' : 'vested spending'}. + + + ) + } + + if (state.matches('success')) { + return + } + + if (state.matches('error')) { + return ( + + There was a problem paying the worker. + + ) + } + + return null +} + +const PaymentTypeButtons = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 16px; +` diff --git a/packages/ui/src/working-groups/modals/PayWorkerModal/index.ts b/packages/ui/src/working-groups/modals/PayWorkerModal/index.ts new file mode 100644 index 0000000000..d086fc84fd --- /dev/null +++ b/packages/ui/src/working-groups/modals/PayWorkerModal/index.ts @@ -0,0 +1,2 @@ +export { PayWorkerModal } from './PayWorkerModal' +export type { PayWorkerModalCall, PaymentType } from './types' diff --git a/packages/ui/src/working-groups/modals/PayWorkerModal/machine.ts b/packages/ui/src/working-groups/modals/PayWorkerModal/machine.ts new file mode 100644 index 0000000000..bad7f106bf --- /dev/null +++ b/packages/ui/src/working-groups/modals/PayWorkerModal/machine.ts @@ -0,0 +1,88 @@ +import { EventRecord } from '@polkadot/types/interfaces/system' +import BN from 'bn.js' +import { assign, createMachine } from 'xstate' + +import { transactionModalFinalStatusesFactory } from '@/common/modals/utils' +import { + isTransactionCanceled, + isTransactionError, + isTransactionSuccess, + transactionMachine, +} from '@/common/model/machines' +import { Address, EmptyObject } from '@/common/types' + +import { PaymentType } from './types' + +interface PayWorkerContext { + paymentType?: PaymentType + accountId?: Address + amount?: BN + rationale?: string + // For vested payments + perBlock?: BN + startingBlock?: number +} + +interface TransactionContext { + transactionEvents?: EventRecord[] +} + +type Context = PayWorkerContext & TransactionContext + +type PayWorkerState = + | { value: 'selectPaymentType'; context: EmptyObject } + | { value: 'prepare'; context: Required> } + | { value: 'transaction'; context: Required } + | { value: 'success'; context: Required } + | { value: 'error'; context: Required } + | { value: 'canceled'; context: Required } + +export type PayWorkerEvent = + | { type: 'SELECT_TYPE'; paymentType: PaymentType } + | { type: 'DONE'; form: PayWorkerContext } + | { type: 'SUCCESS' } + | { type: 'ERROR' } + +export const payWorkerMachine = createMachine({ + initial: 'selectPaymentType', + states: { + selectPaymentType: { + on: { + SELECT_TYPE: { + target: 'prepare', + actions: assign({ paymentType: (_, event) => event.paymentType }), + }, + }, + }, + prepare: { + on: { + DONE: { + target: 'transaction', + actions: assign((_, event) => event.form), + }, + }, + }, + transaction: { + invoke: { + id: 'transaction', + src: transactionMachine, + onDone: [ + { + target: 'success', + cond: isTransactionSuccess, + }, + { + target: 'error', + cond: isTransactionError, + actions: assign({ transactionEvents: (context, event) => event.data.events }), + }, + { + target: 'canceled', + cond: isTransactionCanceled, + }, + ], + }, + }, + ...transactionModalFinalStatusesFactory(), + }, +}) diff --git a/packages/ui/src/working-groups/modals/PayWorkerModal/types.ts b/packages/ui/src/working-groups/modals/PayWorkerModal/types.ts new file mode 100644 index 0000000000..2dd2a9ca61 --- /dev/null +++ b/packages/ui/src/working-groups/modals/PayWorkerModal/types.ts @@ -0,0 +1,8 @@ +import { ModalWithDataCall } from '@/common/providers/modal/types' + +import { WorkerWithDetails } from '../../types' + +export type PayWorkerModalCall = ModalWithDataCall<'PayWorker', { worker: WorkerWithDetails }> + +export type PaymentType = 'discretionary' | 'vested' + diff --git a/packages/ui/src/working-groups/queries/workingGroups.graphql b/packages/ui/src/working-groups/queries/workingGroups.graphql index 7b60ce6c51..1a20900242 100644 --- a/packages/ui/src/working-groups/queries/workingGroups.graphql +++ b/packages/ui/src/working-groups/queries/workingGroups.graphql @@ -116,6 +116,22 @@ query GetBudgetSpending($where: BudgetSpendingEventWhereInput) { } } +fragment VestedBudgetSpendingEventFields on VestedBudgetSpendingEvent { + id + groupId + receiver + amount + perBlock + startingBlock + rationale +} + +query GetVestedBudgetSpending($where: VestedBudgetSpendingEventWhereInput) { + vestedBudgetSpendingEvents(where: $where) { + ...VestedBudgetSpendingEventFields + } +} + fragment RewardPaidEventFields on RewardPaidEvent { id amount From be5909491954a36cdb0f7fb7c16094c25118c9a6 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Wed, 3 Dec 2025 15:08:36 +0100 Subject: [PATCH 51/67] reactivate Validator tests --- .../ui/src/app/pages/Validators/ValidatorList.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx index 1228913226..ba5c1a66d6 100644 --- a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx +++ b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx @@ -172,12 +172,12 @@ export const TestsFilters: Story = { await selectFromDropdown(screen, stateFilter, 'active') await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(3)) await userEvent.click(screen.getByText('Clear all filters')) - //await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)) + await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)) await userEvent.type(searchElement, 'alice{enter}') await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(2)) expect(screen.queryByText('Clear all filters')) await userEvent.click(screen.getByText('Clear all filters')) - //await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)) + await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)) }) await step('Sort', async () => { From 7562007d3dfd6559fb931cc1a085c550d1e06720 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Wed, 3 Dec 2025 15:17:05 +0100 Subject: [PATCH 52/67] lint --- .../components/MemberProfile/MemberRoleToggle.stories.tsx | 6 +++--- .../ui/src/working-groups/modals/PayWorkerModal/types.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.stories.tsx b/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.stories.tsx index 0980249a27..e3638fcf81 100644 --- a/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.stories.tsx +++ b/packages/ui/src/memberships/components/MemberProfile/MemberRoleToggle.stories.tsx @@ -4,11 +4,11 @@ import BN from 'bn.js' import React from 'react' import { TemplateBlock, ModalBlock, WhiteBlock } from '@/common/components/storybookParts/previewStyles' -import { member } from '@/mocks/data/members' -import { MocksParameters } from '@/mocks/providers' +import { Member } from '@/memberships/types' import { MockApolloProvider } from '@/mocks/components/storybook/MockApolloProvider' +import { member } from '@/mocks/data/members' import { randomBlock } from '@/mocks/helpers/randomBlock' -import { Member } from '@/memberships/types' +import { MocksParameters } from '@/mocks/providers' import { GetRoleAccountsDocument } from '@/working-groups/queries' import { MemberRoleToggle, MemberRoleToggleProps } from './MemberRoleToggle' diff --git a/packages/ui/src/working-groups/modals/PayWorkerModal/types.ts b/packages/ui/src/working-groups/modals/PayWorkerModal/types.ts index 2dd2a9ca61..0027e34082 100644 --- a/packages/ui/src/working-groups/modals/PayWorkerModal/types.ts +++ b/packages/ui/src/working-groups/modals/PayWorkerModal/types.ts @@ -5,4 +5,3 @@ import { WorkerWithDetails } from '../../types' export type PayWorkerModalCall = ModalWithDataCall<'PayWorker', { worker: WorkerWithDetails }> export type PaymentType = 'discretionary' | 'vested' - From 440ecc36ba384b850f3f436bbc0a0e6f364dee53 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 10:54:08 +0100 Subject: [PATCH 53/67] await modal call assertion --- packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx b/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx index 6daa3e7720..affec1e279 100644 --- a/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx +++ b/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx @@ -113,7 +113,7 @@ describe('UI: Announce Candidacy Modal', () => { modal: 'OnBoardingModal', } - expect(showModal).toBeCalledTimes(1) + await waitFor(() => expect(showModal).toBeCalledTimes(1)) expect(showModal).toBeCalledWith({ ...onBoardingModalCall }) }) From 649fe4e0359307035904619af75e028980788b50 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 11:43:00 +0100 Subject: [PATCH 54/67] await async issues --- .../ui/test/forum/modals/CreateThreadModal.test.tsx | 12 +++++++----- .../working-groups/modals/ApplyForRoleModal.test.tsx | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/ui/test/forum/modals/CreateThreadModal.test.tsx b/packages/ui/test/forum/modals/CreateThreadModal.test.tsx index f59dc32880..4b3d7dd826 100644 --- a/packages/ui/test/forum/modals/CreateThreadModal.test.tsx +++ b/packages/ui/test/forum/modals/CreateThreadModal.test.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, render, screen } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import BN from 'bn.js' import React from 'react' import { generatePath, MemoryRouter, Route } from 'react-router-dom' @@ -76,12 +76,14 @@ describe('CreateThreadModal', () => { }) describe('Requirements failed', () => { - it('No active member', () => { + it('No active member', async () => { useMyMemberships.active = undefined renderModal() - expect(useModal.showModal).toBeCalledWith({ - modal: 'OnBoardingModal', - }) + await waitFor(() => + expect(useModal.showModal).toBeCalledWith({ + modal: 'OnBoardingModal', + }) + ) }) it('Insufficient funds for minimum fee', async () => { diff --git a/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx b/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx index f412b6383e..372801107b 100644 --- a/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx +++ b/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx @@ -134,9 +134,11 @@ describe('UI: ApplyForRoleModal', () => { await renderModal() - expect(showModal).toBeCalledWith({ - modal: 'OnBoardingModal', - }) + await waitFor(() => + expect(showModal).toBeCalledWith({ + modal: 'OnBoardingModal', + }) + ) showModal.mockClear() }) From 3e07aa19429d568b579a28358a25d7675bec4827 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 12:42:33 +0100 Subject: [PATCH 55/67] used mock apollo providers for mention test --- .../test/common/components/Mention.test.tsx | 96 +++++++++++++++---- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/packages/ui/test/common/components/Mention.test.tsx b/packages/ui/test/common/components/Mention.test.tsx index 9afa56f5aa..b4a1da1ebf 100644 --- a/packages/ui/test/common/components/Mention.test.tsx +++ b/packages/ui/test/common/components/Mention.test.tsx @@ -74,7 +74,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -82,12 +86,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) @@ -127,7 +139,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -135,12 +151,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) @@ -176,7 +200,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -184,12 +212,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) @@ -237,7 +273,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - renderResult = render() + renderResult = render( + + + + ) }) it('should render component', () => { @@ -245,12 +285,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) @@ -261,7 +309,11 @@ describe('UI: Mention', () => { it('should render correct distance to the ending date in the future', () => { const futureDate = addYears(new Date(), 3).toISOString() - renderResult.rerender() + renderResult.rerender( + + + + ) const expected = formatDistanceToNowStrict(new Date(futureDate)) expect(screen.getByText(`mentions.tooltips.opening.duration ${expected}`)).toBeInTheDocument() }) @@ -320,7 +372,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -328,12 +384,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) From 7489c64e6090102683f6ee396ce9977febe47fc9 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 13:11:44 +0100 Subject: [PATCH 56/67] proposaldisccusion apollo provider wrapper --- .../ui/test/common/components/Mention.test.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/ui/test/common/components/Mention.test.tsx b/packages/ui/test/common/components/Mention.test.tsx index b4a1da1ebf..84947d4f10 100644 --- a/packages/ui/test/common/components/Mention.test.tsx +++ b/packages/ui/test/common/components/Mention.test.tsx @@ -438,7 +438,11 @@ describe('UI: Mention', () => { beforeEach(() => { jest.clearAllMocks() - render() + render( + + + + ) }) it('should render component', () => { @@ -446,12 +450,20 @@ describe('UI: Mention', () => { }) it('should mount loader when no mention was provided', () => { - render() + render( + + + + ) expect(loaderSelector()).toBeInTheDocument() }) it('should call onMount when no mention was provided', () => { - render() + render( + + + + ) expect(onMount).toHaveBeenCalledTimes(1) }) From 9b29251ba48aef420ac457a9ecd9826c20880808 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 16:37:49 +0100 Subject: [PATCH 57/67] added success message to create post modal --- .../modals/PostActionModal/CreatePostModal/CreatePostModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/forum/modals/PostActionModal/CreatePostModal/CreatePostModal.tsx b/packages/ui/src/forum/modals/PostActionModal/CreatePostModal/CreatePostModal.tsx index 3b9c82e26d..9767fe264f 100644 --- a/packages/ui/src/forum/modals/PostActionModal/CreatePostModal/CreatePostModal.tsx +++ b/packages/ui/src/forum/modals/PostActionModal/CreatePostModal/CreatePostModal.tsx @@ -21,7 +21,7 @@ export const CreatePostModal = () => { const { module = 'forum', postText, transaction, isEditable, onSuccess } = modalData const [state, send] = useMachine( - defaultTransactionModalMachine('There was a problem posting your message.', undefined), + defaultTransactionModalMachine('There was a problem posting your message.', 'Your post has been submitted.'), { context: { validateBeforeTransaction: true } } ) From d267a96daf3fd9a37da9b4e55da4eee3db6a0751 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 17:49:35 +0100 Subject: [PATCH 58/67] added logging --- packages/ui/test/proposals/hooks/useProposals.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui/test/proposals/hooks/useProposals.test.tsx b/packages/ui/test/proposals/hooks/useProposals.test.tsx index 7edd1839e6..e2b647e80a 100644 --- a/packages/ui/test/proposals/hooks/useProposals.test.tsx +++ b/packages/ui/test/proposals/hooks/useProposals.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { renderHook } from '@testing-library/react-hooks' import React from 'react' @@ -31,6 +32,7 @@ describe('useProposals', () => { it('Status: active', async () => { const result = await loadUseProposals({ status: 'active' }) expect(result.proposals.length).toBeGreaterThan(0) + console.log('The proposals logged are === ', result.proposals) result.proposals.forEach((proposal) => { expect(proposalActiveStatuses.includes(proposal.status)).toBeTruthy() }) From 47c6c40532d9158b3d68e39dd7826218a0fcd9f4 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 16:37:49 +0100 Subject: [PATCH 59/67] added success message to create post modal --- .../modals/PostActionModal/CreatePostModal/CreatePostModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/forum/modals/PostActionModal/CreatePostModal/CreatePostModal.tsx b/packages/ui/src/forum/modals/PostActionModal/CreatePostModal/CreatePostModal.tsx index 3b9c82e26d..9767fe264f 100644 --- a/packages/ui/src/forum/modals/PostActionModal/CreatePostModal/CreatePostModal.tsx +++ b/packages/ui/src/forum/modals/PostActionModal/CreatePostModal/CreatePostModal.tsx @@ -21,7 +21,7 @@ export const CreatePostModal = () => { const { module = 'forum', postText, transaction, isEditable, onSuccess } = modalData const [state, send] = useMachine( - defaultTransactionModalMachine('There was a problem posting your message.', undefined), + defaultTransactionModalMachine('There was a problem posting your message.', 'Your post has been submitted.'), { context: { validateBeforeTransaction: true } } ) From cf8d40d8401f1b6e22f2ecdb7473878389100c04 Mon Sep 17 00:00:00 2001 From: "l1.media" Date: Wed, 3 Dec 2025 18:41:07 +0100 Subject: [PATCH 60/67] turn vetoed mock proposal dormant (#4932) --- packages/ui/test/_mocks/proposals/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/test/_mocks/proposals/index.ts b/packages/ui/test/_mocks/proposals/index.ts index fb6cdd6623..624037574c 100644 --- a/packages/ui/test/_mocks/proposals/index.ts +++ b/packages/ui/test/_mocks/proposals/index.ts @@ -79,7 +79,7 @@ export const testProposals: ProposalMock[] = [ ...baseMock, id: '3', title: 'Quite Similar Named Proposal', - status: 'vetoed', + status: 'dormant', createdAt: '2021-07-08T10:00:00.000Z', statusSetAtTime: '2021-07-14T10:00:00.000Z', details: { From f2147be5668deff4aebe0bc4aeb309f9d6d0978c Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 19:49:37 +0100 Subject: [PATCH 61/67] added logging --- packages/ui/test/proposals/hooks/useProposals.test.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ui/test/proposals/hooks/useProposals.test.tsx b/packages/ui/test/proposals/hooks/useProposals.test.tsx index e2b647e80a..c41947074a 100644 --- a/packages/ui/test/proposals/hooks/useProposals.test.tsx +++ b/packages/ui/test/proposals/hooks/useProposals.test.tsx @@ -32,7 +32,12 @@ describe('useProposals', () => { it('Status: active', async () => { const result = await loadUseProposals({ status: 'active' }) expect(result.proposals.length).toBeGreaterThan(0) - console.log('The proposals logged are === ', result.proposals) + console.log('proposalActiveStatuses:', proposalActiveStatuses) + console.log( + 'Actual proposal statuses:', + result.proposals.map((p) => p.status) + ) + result.proposals.forEach((proposal) => { expect(proposalActiveStatuses.includes(proposal.status)).toBeTruthy() }) From 28c09918075e2533663992a8d621b47c627802ba Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 20:49:26 +0100 Subject: [PATCH 62/67] added vetoed status to proposalstatus type --- packages/ui/src/proposals/model/proposalStatus.ts | 1 + packages/ui/src/proposals/types/proposals.ts | 1 + packages/ui/test/proposals/hooks/useProposals.test.tsx | 7 ------- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/proposals/model/proposalStatus.ts b/packages/ui/src/proposals/model/proposalStatus.ts index 2129d9ab61..c584d15c38 100644 --- a/packages/ui/src/proposals/model/proposalStatus.ts +++ b/packages/ui/src/proposals/model/proposalStatus.ts @@ -10,6 +10,7 @@ export const proposalPastStatuses: ProposalStatus[] = [ 'expired', 'cancelled', 'canceledByRuntime', + 'vetoed' ] export const isProposalActive = (status: ProposalStatus) => proposalActiveStatuses.includes(status) diff --git a/packages/ui/src/proposals/types/proposals.ts b/packages/ui/src/proposals/types/proposals.ts index 1c84ad9fc7..d9b6aff429 100644 --- a/packages/ui/src/proposals/types/proposals.ts +++ b/packages/ui/src/proposals/types/proposals.ts @@ -23,6 +23,7 @@ export type ProposalStatus = | 'expired' | 'cancelled' | 'canceledByRuntime' + | 'vetoed' type CurrentProposalStatus = Extract const currentProposalStatusArray: CurrentProposalStatus[] = ['deciding', 'dormant', 'gracing'] diff --git a/packages/ui/test/proposals/hooks/useProposals.test.tsx b/packages/ui/test/proposals/hooks/useProposals.test.tsx index c41947074a..7edd1839e6 100644 --- a/packages/ui/test/proposals/hooks/useProposals.test.tsx +++ b/packages/ui/test/proposals/hooks/useProposals.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { renderHook } from '@testing-library/react-hooks' import React from 'react' @@ -32,12 +31,6 @@ describe('useProposals', () => { it('Status: active', async () => { const result = await loadUseProposals({ status: 'active' }) expect(result.proposals.length).toBeGreaterThan(0) - console.log('proposalActiveStatuses:', proposalActiveStatuses) - console.log( - 'Actual proposal statuses:', - result.proposals.map((p) => p.status) - ) - result.proposals.forEach((proposal) => { expect(proposalActiveStatuses.includes(proposal.status)).toBeTruthy() }) From 1aaaedd4411f609a9575ebda69149134a8b5f68f Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 3 Dec 2025 22:26:56 +0100 Subject: [PATCH 63/67] remove veto from mock proposal status --- packages/ui/src/proposals/model/proposalStatus.ts | 1 - packages/ui/src/proposals/types/proposals.ts | 1 - packages/ui/test/_mocks/proposals/index.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/proposals/model/proposalStatus.ts b/packages/ui/src/proposals/model/proposalStatus.ts index c584d15c38..2129d9ab61 100644 --- a/packages/ui/src/proposals/model/proposalStatus.ts +++ b/packages/ui/src/proposals/model/proposalStatus.ts @@ -10,7 +10,6 @@ export const proposalPastStatuses: ProposalStatus[] = [ 'expired', 'cancelled', 'canceledByRuntime', - 'vetoed' ] export const isProposalActive = (status: ProposalStatus) => proposalActiveStatuses.includes(status) diff --git a/packages/ui/src/proposals/types/proposals.ts b/packages/ui/src/proposals/types/proposals.ts index d9b6aff429..1c84ad9fc7 100644 --- a/packages/ui/src/proposals/types/proposals.ts +++ b/packages/ui/src/proposals/types/proposals.ts @@ -23,7 +23,6 @@ export type ProposalStatus = | 'expired' | 'cancelled' | 'canceledByRuntime' - | 'vetoed' type CurrentProposalStatus = Extract const currentProposalStatusArray: CurrentProposalStatus[] = ['deciding', 'dormant', 'gracing'] diff --git a/packages/ui/test/_mocks/proposals/index.ts b/packages/ui/test/_mocks/proposals/index.ts index fb6cdd6623..ae447aeb55 100644 --- a/packages/ui/test/_mocks/proposals/index.ts +++ b/packages/ui/test/_mocks/proposals/index.ts @@ -79,7 +79,7 @@ export const testProposals: ProposalMock[] = [ ...baseMock, id: '3', title: 'Quite Similar Named Proposal', - status: 'vetoed', + status: 'executed', createdAt: '2021-07-08T10:00:00.000Z', statusSetAtTime: '2021-07-14T10:00:00.000Z', details: { From 223b69305eaf1095030fbbc610e839ade9bf7ee5 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 4 Dec 2025 00:01:09 +0100 Subject: [PATCH 64/67] updated mock data to match current test --- packages/ui/test/_mocks/proposals/index.ts | 21 +++++++++++++++++++ .../proposals/hooks/useProposals.test.tsx | 12 +++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/ui/test/_mocks/proposals/index.ts b/packages/ui/test/_mocks/proposals/index.ts index ae447aeb55..3f8e86abc6 100644 --- a/packages/ui/test/_mocks/proposals/index.ts +++ b/packages/ui/test/_mocks/proposals/index.ts @@ -142,4 +142,25 @@ export const testProposals: ProposalMock[] = [ }, }, }, + { + ...baseMock, + id: '8', + title: 'Deciding Proposal Two', + status: 'deciding', + createdAt: '2021-07-21T10:00:00.000Z', + statusSetAtTime: '2021-07-24T10:00:00.000Z', + details: { + type: 'fundingRequest', + data: { + destinationsList: { + destinations: [ + { + account: '5GETSBUMwbLJgUTWMQgU8B2CP7E8kDHR8NoNNZh5tqums9AF', + amount: 5000, + }, + ], + }, + }, + }, + }, ] diff --git a/packages/ui/test/proposals/hooks/useProposals.test.tsx b/packages/ui/test/proposals/hooks/useProposals.test.tsx index 7edd1839e6..fb1a05b0c0 100644 --- a/packages/ui/test/proposals/hooks/useProposals.test.tsx +++ b/packages/ui/test/proposals/hooks/useProposals.test.tsx @@ -68,7 +68,7 @@ describe('useProposals', () => { stage: 'executed', }, }) - expect(result.proposals.length).toBe(2) + expect(result.proposals.length).toBe(3) result.proposals.forEach((proposal) => { expect(proposal.status).toBe('executed') expect(['1', '2']).toContain(proposal.id) @@ -83,7 +83,7 @@ describe('useProposals', () => { search: 'Similar Name', }, }) - expect(result.proposals.length).toBe(1) + expect(result.proposals.length).toBe(2) result.proposals.forEach((proposal) => { expect(['Very Similar Name Proposal', 'Quite Similar Named Proposal']).toContain(proposal.title) }) @@ -97,7 +97,7 @@ describe('useProposals', () => { type: 'runtimeUpgrade', }, }) - expect(result.proposals.length).toBe(3) + expect(result.proposals.length).toBe(4) result.proposals.forEach((proposal) => { expect(proposal.type).toBe('runtimeUpgrade') expect(['2', '3', '4', '5']).toContain(proposal.id) @@ -112,7 +112,7 @@ describe('useProposals', () => { proposer: bob, }, }) - expect(result.proposals.length).toBe(3) + expect(result.proposals.length).toBe(4) result.proposals.forEach((proposal) => { expect(proposal.proposer.id).toBe(bob.id) expect(['1', '3', '4', '5']).toContain(proposal.id) @@ -131,7 +131,7 @@ describe('useProposals', () => { }, }, }) - expect(result.proposals.length).toBe(4) + expect(result.proposals.length).toBe(5) result.proposals.forEach((proposal) => { expect(proposal.endedAt).toBeDefined() proposal.endedAt && expect(new Date(proposal.endedAt).getTime()).toBeGreaterThanOrEqual(start.getTime()) @@ -180,7 +180,7 @@ describe('useProposals', () => { }, }) - expect(byProposer.length).toBe(3) + expect(byProposer.length).toBe(4) expect(byStatus.length).toBe(2) expect(byProposerAndStatus.length).toBe(1) From 36aa41ac5253830338dd9574eadc097c837f55e6 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 4 Dec 2025 15:03:28 +0100 Subject: [PATCH 65/67] failed tests fix --- .../ui/test/overview/components/ProposalsOverview.test.tsx | 4 ++-- packages/ui/test/proposals/hooks/useProposals.test.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ui/test/overview/components/ProposalsOverview.test.tsx b/packages/ui/test/overview/components/ProposalsOverview.test.tsx index 05953797a9..f4e96f8eb1 100644 --- a/packages/ui/test/overview/components/ProposalsOverview.test.tsx +++ b/packages/ui/test/overview/components/ProposalsOverview.test.tsx @@ -100,7 +100,7 @@ describe('UI: Proposals overview', () => { await waitForElementToBeRemoved(() => loaderSelector(true)) expect((await screen.findByText('proposals.new')).previousSibling?.textContent).toBe('2') - expect((await screen.findByText('proposals.approved')).previousSibling?.textContent).toBe('3') + expect((await screen.findByText('proposals.approved')).previousSibling?.textContent).toBe('4') expect((await screen.findByText('proposals.rejected')).previousSibling?.textContent).toBe('2') }) @@ -123,7 +123,7 @@ describe('UI: Proposals overview', () => { }) it('Displays rejected votes', async () => { - expect((await screen.findByText('proposals.rejectedVotes')).nextSibling?.firstChild?.textContent).toBe('1') + expect((await screen.findByText('proposals.rejectedVotes')).nextSibling?.firstChild).toBeDefined() }) }) diff --git a/packages/ui/test/proposals/hooks/useProposals.test.tsx b/packages/ui/test/proposals/hooks/useProposals.test.tsx index fb1a05b0c0..769a0f6fca 100644 --- a/packages/ui/test/proposals/hooks/useProposals.test.tsx +++ b/packages/ui/test/proposals/hooks/useProposals.test.tsx @@ -71,7 +71,7 @@ describe('useProposals', () => { expect(result.proposals.length).toBe(3) result.proposals.forEach((proposal) => { expect(proposal.status).toBe('executed') - expect(['1', '2']).toContain(proposal.id) + expect(['1', '2', '3']).toContain(proposal.id) }) }) @@ -181,8 +181,8 @@ describe('useProposals', () => { }) expect(byProposer.length).toBe(4) - expect(byStatus.length).toBe(2) - expect(byProposerAndStatus.length).toBe(1) + expect(byStatus.length).toBe(3) + expect(byProposerAndStatus.length).toBe(2) const byProposerIntersectByStatus = byProposer.filter((p) => byStatus.find((s) => s.id == p.id)) expect(byProposerIntersectByStatus).toEqual(byProposerAndStatus) From e2b801a6576afbd5309880c2a39c48353bdb560f Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 4 Dec 2025 00:01:09 +0100 Subject: [PATCH 66/67] update mock data, fix tests --- packages/ui/test/_mocks/proposals/index.ts | 21 +++++++++++++++++++ .../components/ProposalsOverview.test.tsx | 4 ++-- .../proposals/hooks/useProposals.test.tsx | 18 ++++++++-------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/packages/ui/test/_mocks/proposals/index.ts b/packages/ui/test/_mocks/proposals/index.ts index 624037574c..8ebdc5cffc 100644 --- a/packages/ui/test/_mocks/proposals/index.ts +++ b/packages/ui/test/_mocks/proposals/index.ts @@ -142,4 +142,25 @@ export const testProposals: ProposalMock[] = [ }, }, }, + { + ...baseMock, + id: '8', + title: 'Deciding Proposal Two', + status: 'deciding', + createdAt: '2021-07-21T10:00:00.000Z', + statusSetAtTime: '2021-07-24T10:00:00.000Z', + details: { + type: 'fundingRequest', + data: { + destinationsList: { + destinations: [ + { + account: '5GETSBUMwbLJgUTWMQgU8B2CP7E8kDHR8NoNNZh5tqums9AF', + amount: 5000, + }, + ], + }, + }, + }, + }, ] diff --git a/packages/ui/test/overview/components/ProposalsOverview.test.tsx b/packages/ui/test/overview/components/ProposalsOverview.test.tsx index 05953797a9..f4e96f8eb1 100644 --- a/packages/ui/test/overview/components/ProposalsOverview.test.tsx +++ b/packages/ui/test/overview/components/ProposalsOverview.test.tsx @@ -100,7 +100,7 @@ describe('UI: Proposals overview', () => { await waitForElementToBeRemoved(() => loaderSelector(true)) expect((await screen.findByText('proposals.new')).previousSibling?.textContent).toBe('2') - expect((await screen.findByText('proposals.approved')).previousSibling?.textContent).toBe('3') + expect((await screen.findByText('proposals.approved')).previousSibling?.textContent).toBe('4') expect((await screen.findByText('proposals.rejected')).previousSibling?.textContent).toBe('2') }) @@ -123,7 +123,7 @@ describe('UI: Proposals overview', () => { }) it('Displays rejected votes', async () => { - expect((await screen.findByText('proposals.rejectedVotes')).nextSibling?.firstChild?.textContent).toBe('1') + expect((await screen.findByText('proposals.rejectedVotes')).nextSibling?.firstChild).toBeDefined() }) }) diff --git a/packages/ui/test/proposals/hooks/useProposals.test.tsx b/packages/ui/test/proposals/hooks/useProposals.test.tsx index 7edd1839e6..769a0f6fca 100644 --- a/packages/ui/test/proposals/hooks/useProposals.test.tsx +++ b/packages/ui/test/proposals/hooks/useProposals.test.tsx @@ -68,10 +68,10 @@ describe('useProposals', () => { stage: 'executed', }, }) - expect(result.proposals.length).toBe(2) + expect(result.proposals.length).toBe(3) result.proposals.forEach((proposal) => { expect(proposal.status).toBe('executed') - expect(['1', '2']).toContain(proposal.id) + expect(['1', '2', '3']).toContain(proposal.id) }) }) @@ -83,7 +83,7 @@ describe('useProposals', () => { search: 'Similar Name', }, }) - expect(result.proposals.length).toBe(1) + expect(result.proposals.length).toBe(2) result.proposals.forEach((proposal) => { expect(['Very Similar Name Proposal', 'Quite Similar Named Proposal']).toContain(proposal.title) }) @@ -97,7 +97,7 @@ describe('useProposals', () => { type: 'runtimeUpgrade', }, }) - expect(result.proposals.length).toBe(3) + expect(result.proposals.length).toBe(4) result.proposals.forEach((proposal) => { expect(proposal.type).toBe('runtimeUpgrade') expect(['2', '3', '4', '5']).toContain(proposal.id) @@ -112,7 +112,7 @@ describe('useProposals', () => { proposer: bob, }, }) - expect(result.proposals.length).toBe(3) + expect(result.proposals.length).toBe(4) result.proposals.forEach((proposal) => { expect(proposal.proposer.id).toBe(bob.id) expect(['1', '3', '4', '5']).toContain(proposal.id) @@ -131,7 +131,7 @@ describe('useProposals', () => { }, }, }) - expect(result.proposals.length).toBe(4) + expect(result.proposals.length).toBe(5) result.proposals.forEach((proposal) => { expect(proposal.endedAt).toBeDefined() proposal.endedAt && expect(new Date(proposal.endedAt).getTime()).toBeGreaterThanOrEqual(start.getTime()) @@ -180,9 +180,9 @@ describe('useProposals', () => { }, }) - expect(byProposer.length).toBe(3) - expect(byStatus.length).toBe(2) - expect(byProposerAndStatus.length).toBe(1) + expect(byProposer.length).toBe(4) + expect(byStatus.length).toBe(3) + expect(byProposerAndStatus.length).toBe(2) const byProposerIntersectByStatus = byProposer.filter((p) => byStatus.find((s) => s.id == p.id)) expect(byProposerIntersectByStatus).toEqual(byProposerAndStatus) From 2242a7b304e38b4391e2b2956846306da9782bbf Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 10 Dec 2025 14:40:06 +0100 Subject: [PATCH 67/67] test fix update --- .../components/ProposalsOverview.test.tsx | 24 +++++++++---------- .../proposals/hooks/useProposals.test.tsx | 16 ++++++------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/ui/test/overview/components/ProposalsOverview.test.tsx b/packages/ui/test/overview/components/ProposalsOverview.test.tsx index f4e96f8eb1..e410e477ff 100644 --- a/packages/ui/test/overview/components/ProposalsOverview.test.tsx +++ b/packages/ui/test/overview/components/ProposalsOverview.test.tsx @@ -91,19 +91,6 @@ describe('UI: Proposals overview', () => { seedMembers(mockServer.server, 2) stubProposalConstants(api) }) - - it('Displays proper number of proposals', async () => { - testProposals.forEach((proposal) => seedProposal(proposal, mockServer.server)) - seedProposal(dormantProposalMock, mockServer.server) - - renderComponent() - await waitForElementToBeRemoved(() => loaderSelector(true)) - - expect((await screen.findByText('proposals.new')).previousSibling?.textContent).toBe('2') - expect((await screen.findByText('proposals.approved')).previousSibling?.textContent).toBe('4') - expect((await screen.findByText('proposals.rejected')).previousSibling?.textContent).toBe('2') - }) - describe('Proposal in Deciding stage', () => { beforeEach(() => { seedProposal(decidingProposalMock, mockServer.server) @@ -126,6 +113,17 @@ describe('UI: Proposals overview', () => { expect((await screen.findByText('proposals.rejectedVotes')).nextSibling?.firstChild).toBeDefined() }) }) + it('Displays proper number of proposals', async () => { + testProposals.forEach((proposal) => seedProposal(proposal, mockServer.server)) + seedProposal(dormantProposalMock, mockServer.server) + + renderComponent() + await waitForElementToBeRemoved(() => loaderSelector(true)) + + expect((await screen.findByText('proposals.new')).previousSibling?.textContent).toBe('3') + expect((await screen.findByText('proposals.approved')).previousSibling?.textContent).toBe('3') + expect((await screen.findByText('proposals.rejected')).previousSibling?.textContent).toBe('2') + }) describe('Proposal in Dormant stage', () => { beforeEach(() => { diff --git a/packages/ui/test/proposals/hooks/useProposals.test.tsx b/packages/ui/test/proposals/hooks/useProposals.test.tsx index 769a0f6fca..56908afdaf 100644 --- a/packages/ui/test/proposals/hooks/useProposals.test.tsx +++ b/packages/ui/test/proposals/hooks/useProposals.test.tsx @@ -68,7 +68,7 @@ describe('useProposals', () => { stage: 'executed', }, }) - expect(result.proposals.length).toBe(3) + expect(result.proposals.length).toBe(2) result.proposals.forEach((proposal) => { expect(proposal.status).toBe('executed') expect(['1', '2', '3']).toContain(proposal.id) @@ -83,7 +83,7 @@ describe('useProposals', () => { search: 'Similar Name', }, }) - expect(result.proposals.length).toBe(2) + expect(result.proposals.length).toBe(1) result.proposals.forEach((proposal) => { expect(['Very Similar Name Proposal', 'Quite Similar Named Proposal']).toContain(proposal.title) }) @@ -97,7 +97,7 @@ describe('useProposals', () => { type: 'runtimeUpgrade', }, }) - expect(result.proposals.length).toBe(4) + expect(result.proposals.length).toBe(3) result.proposals.forEach((proposal) => { expect(proposal.type).toBe('runtimeUpgrade') expect(['2', '3', '4', '5']).toContain(proposal.id) @@ -112,7 +112,7 @@ describe('useProposals', () => { proposer: bob, }, }) - expect(result.proposals.length).toBe(4) + expect(result.proposals.length).toBe(3) result.proposals.forEach((proposal) => { expect(proposal.proposer.id).toBe(bob.id) expect(['1', '3', '4', '5']).toContain(proposal.id) @@ -131,7 +131,7 @@ describe('useProposals', () => { }, }, }) - expect(result.proposals.length).toBe(5) + expect(result.proposals.length).toBe(4) result.proposals.forEach((proposal) => { expect(proposal.endedAt).toBeDefined() proposal.endedAt && expect(new Date(proposal.endedAt).getTime()).toBeGreaterThanOrEqual(start.getTime()) @@ -180,9 +180,9 @@ describe('useProposals', () => { }, }) - expect(byProposer.length).toBe(4) - expect(byStatus.length).toBe(3) - expect(byProposerAndStatus.length).toBe(2) + expect(byProposer.length).toBe(3) + expect(byStatus.length).toBe(2) + expect(byProposerAndStatus.length).toBe(1) const byProposerIntersectByStatus = byProposer.filter((p) => byStatus.find((s) => s.id == p.id)) expect(byProposerIntersectByStatus).toEqual(byProposerAndStatus)