diff --git a/pages/api/v1/posts/active-proposals-count.ts b/pages/api/v1/posts/active-proposals-count.ts new file mode 100644 index 0000000000..e79735b998 --- /dev/null +++ b/pages/api/v1/posts/active-proposals-count.ts @@ -0,0 +1,125 @@ +// Copyright 2019-2025 @polkassembly/polkassembly authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { NextApiHandler } from 'next'; +import withErrorHandling from '~src/api-middlewares/withErrorHandling'; +import { isValidNetwork } from '~src/api-utils'; +import { MessageType } from '~src/auth/types'; +import messages from '~src/auth/utils/messages'; + +import storeApiKeyUsage from '~src/api-middlewares/storeApiKeyUsage'; +import apiErrorWithStatusCode from '~src/util/apiErrorWithStatusCode'; +import { isOpenGovSupported } from '~src/global/openGovNetworks'; +import fetchSubsquid from '~src/util/fetchSubsquid'; +import { + GET_NETWORK_TRACK_ACTIVE_PROPOSALS_COUNT, + GOV1_NETWORK_ACTIVE_PROPOSALS_COUNT, + POLYMESH_NETWORK_ACTIVE_PROPOSALS_COUNT, + ZEITGEIST_NETWORK_ACTIVE_PROPOSALS_COUNT +} from '~src/queries'; +import { IActiveProposalCount } from '~src/types'; + +export const getNetworkTrackActiveProposalsCount = async ({ network }: { network: string }) => { + try { + if (!network || !isValidNetwork(network)) throw apiErrorWithStatusCode(messages.INVALID_NETWORK, 400); + + if (isOpenGovSupported(network)) { + const subsquidRes = await fetchSubsquid({ + network, + query: GET_NETWORK_TRACK_ACTIVE_PROPOSALS_COUNT + }); + + const proposals = subsquidRes?.['data']?.proposals || []; + let proposalsCount: { [key: string]: number } = {}; + + proposals.map((proposal: { trackNumber: number }) => { + if (proposalsCount[proposal?.trackNumber] === undefined) { + proposalsCount[proposal?.trackNumber] = 1; + } else { + proposalsCount[proposal?.trackNumber] += 1; + } + }); + proposalsCount = { + ...proposalsCount, + allCount: subsquidRes?.['data']?.all?.totalCount || 0, + bountiesCount: subsquidRes?.['data']?.bountiesCount?.totalCount || 0, + childBountiesCount: subsquidRes?.['data']?.childBountiesCount?.totalCount || 0 + }; + + return { + data: proposalsCount, + error: null, + status: 200 + }; + } else { + let query = GOV1_NETWORK_ACTIVE_PROPOSALS_COUNT; + if (network === 'zeitgeist') { + query = ZEITGEIST_NETWORK_ACTIVE_PROPOSALS_COUNT; + } + if (network === 'polymesh') { + query = POLYMESH_NETWORK_ACTIVE_PROPOSALS_COUNT; + } + const subsquidRes = await fetchSubsquid({ + network, + query + }); + const data = subsquidRes['data']; + let proposalsCount: IActiveProposalCount; + if (network === 'polymesh') { + proposalsCount = { + communityPipsCount: data?.communityPips?.totalCount || 0, + technicalPipsCount: data?.technicalPips.totalCount || 0, + upgradePipsCount: data?.upgradePips?.totalCount || 0 + }; + } else { + proposalsCount = { + councilMotionsCount: data?.councilMotions.totalCount || 0, + democracyProposalsCount: data?.democracyProposals.totalCount || 0, + referendumsCount: data?.referendums.totalCount || 0, + techCommetteeProposalsCount: data?.techCommitteeProposals.totalCount || 0, + tipsCount: data?.tips.totalCount || 0, + treasuryProposalsCount: data?.treasuryProposals.totalCount || 0 + }; + if (network === 'zeitgeist') { + proposalsCount = { ...proposalsCount, advisoryCommitteeMotionsCount: data?.advisoryCommitteeMotions.totalCount || 0 }; + } else { + proposalsCount = { + ...proposalsCount, + bountiesCount: data?.bounties?.totalCount || 0, + childBountiesCount: data?.childBounties.totalCount || 0 + }; + } + } + return { + data: proposalsCount, + error: null, + status: 200 + }; + } + } catch (error) { + return { + data: null, + error: error.message || messages.API_FETCH_ERROR, + status: Number(error.name) || 500 + }; + } +}; + +const handler: NextApiHandler<{ [key: string]: number } | MessageType> = async (req, res) => { + storeApiKeyUsage(req); + + const network = String(req.headers['x-network']); + + const { data, error, status } = await getNetworkTrackActiveProposalsCount({ + network + }); + + if (error || !data) { + return res.status(status).json({ message: error || messages.API_FETCH_ERROR }); + } else { + return res.status(status).json(data); + } +}; + +export default withErrorHandling(handler); diff --git a/src/components/AppLayout/index.tsx b/src/components/AppLayout/index.tsx index c764290e6b..609a86475f 100644 --- a/src/components/AppLayout/index.tsx +++ b/src/components/AppLayout/index.tsx @@ -48,7 +48,7 @@ import { isFellowshipSupported } from '~src/global/fellowshipNetworks'; import { isGrantsSupported } from '~src/global/grantsNetworks'; import { isOpenGovSupported } from '~src/global/openGovNetworks'; import { networkTrackInfo } from '~src/global/post_trackInfo'; -import { PostOrigin } from '~src/types'; +import { IActiveProposalCount, PostOrigin } from '~src/types'; import Footer from './Footer'; import NavHeader from './NavHeader'; @@ -72,6 +72,7 @@ import BigToggleButton from '~src/ui-components/ToggleButton/BigToggleButton'; import TopNudges from '~src/ui-components/TopNudges'; import ImageIcon from '~src/ui-components/ImageIcon'; import { setOpenRemoveIdentityModal, setOpenRemoveIdentitySelectAddressModal } from '~src/redux/removeIdentity'; +import nextApiClientFetch from '~src/util/nextApiClientFetch'; interface IUserDropdown { handleSetIdentityClick: any; @@ -308,8 +309,20 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { const [isGood, setIsGood] = useState(false); const [mainDisplay, setMainDisplay] = useState(''); const dispatch = useDispatch(); - // const [notificationVisible, setNotificationVisible] = useState(true); + const [totalActiveProposalsCount, setTotalActiveProposalsCount] = useState(); + + const getTotalActiveProposalsCount = async () => { + if (!network) return; + + const { data, error } = await nextApiClientFetch('/api/v1/posts/active-proposals-count'); + if (data) { + setTotalActiveProposalsCount(data); + } else if (error) { + console.log(error); + } + }; + useEffect(() => { const handleRouteChange = () => { if (router.asPath.split('/')[1] !== 'discussions' && router.asPath.split('/')[1] !== 'post') { @@ -341,6 +354,11 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { }); }, []); + useEffect(() => { + getTotalActiveProposalsCount(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [network]); + useEffect(() => { if (!api || !apiReady) return; @@ -413,24 +431,50 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { ], democracyItems: chainProperties[network]?.subsquidUrl ? [ - getSiderMenuItem('Proposals', '/proposals', ), - getSiderMenuItem('Referenda', '/referenda', ) + getSiderMenuItem( + `Proposals ${totalActiveProposalsCount?.democracyProposalsCount ? totalActiveProposalsCount['democracyProposalsCount'] : ''}`, + '/proposals', + + ), + getSiderMenuItem( + `Referenda ${totalActiveProposalsCount?.referendumsCount ? totalActiveProposalsCount['referendumsCount'] : ''}`, + '/referenda', + + ) ] : [], councilItems: chainProperties[network]?.subsquidUrl ? [ - getSiderMenuItem('Motions', '/motions', ), + getSiderMenuItem( + `Motions ${totalActiveProposalsCount?.councilMotionsCount ? totalActiveProposalsCount['councilMotionsCount'] : ''}`, + '/motions', + + ), getSiderMenuItem('Members', '/council', ) ] : [], treasuryItems: chainProperties[network]?.subsquidUrl ? [ - getSiderMenuItem('Proposals', '/treasury-proposals', ), - getSiderMenuItem('Tips', '/tips', ) + getSiderMenuItem( + `Proposals ${totalActiveProposalsCount?.treasuryProposalsCount ? totalActiveProposalsCount['treasuryProposalsCount'] : ''}`, + '/treasury-proposals', + + ), + getSiderMenuItem( + `Tips ${totalActiveProposalsCount?.tips ? totalActiveProposalsCount['tips'] : ''}`, + '/tips', + + ) ] : [], techCommItems: chainProperties[network]?.subsquidUrl - ? [getSiderMenuItem('Proposals', '/tech-comm-proposals', )] + ? [ + getSiderMenuItem( + `Proposals ${totalActiveProposalsCount?.techCommetteeProposalsCount ? totalActiveProposalsCount['techCommetteeProposalsCount'] : ''}`, + '/tech-comm-proposals', + + ) + ] : [], allianceItems: chainProperties[network]?.subsquidUrl ? [ @@ -443,15 +487,31 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { PIPsItems: chainProperties[network]?.subsquidUrl && network === AllNetworks.POLYMESH ? [ - getSiderMenuItem('Technical Committee', '/technical', ), - getSiderMenuItem('Upgrade Committee', '/upgrade', ), - getSiderMenuItem('Community', '/community', ) + getSiderMenuItem( + `Technical Committee ${totalActiveProposalsCount?.technicalPipsCount ? totalActiveProposalsCount['technicalPipsCount'] : ''}`, + '/technical', + + ), + getSiderMenuItem( + `Upgrade Committee ${totalActiveProposalsCount?.upgradePipsCount ? totalActiveProposalsCount['upgradePipsCount'] : ''}`, + '/upgrade', + + ), + getSiderMenuItem( + `Community ${totalActiveProposalsCount?.communityPipsCount ? totalActiveProposalsCount['communityPipsCount'] : ''}`, + '/community', + + ) ] : [], AdvisoryCommittee: chainProperties[network]?.subsquidUrl && network === AllNetworks.ZEITGEIST ? [ - getSiderMenuItem('Motions', '/advisory-committee/motions', ), + getSiderMenuItem( + `Motions ${totalActiveProposalsCount?.advisoryCommitteeMotionsCount ? totalActiveProposalsCount['advisoryCommitteeMotionsCount'] : ''}`, + '/advisory-committee/motions', + + ), getSiderMenuItem('Members', '/advisory-committee/members', ) ] : [] @@ -572,13 +632,20 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { } if (network && networkTrackInfo[network]) { - gov2TrackItems.mainItems.push(getSiderMenuItem('All', '/all-posts', )); - + gov2TrackItems.mainItems.push( + getSiderMenuItem( + `All ${totalActiveProposalsCount?.allCount ? totalActiveProposalsCount?.allCount : ''}`, + '/all-posts', + + ) + ); for (const trackName of Object.keys(networkTrackInfo[network])) { if (!networkTrackInfo[network][trackName] || !('group' in networkTrackInfo[network][trackName])) continue; + const activeProposal = totalActiveProposalsCount?.[networkTrackInfo[network][trackName]?.trackId]; + const menuItem = getSiderMenuItem( - trackName.split(/(?=[A-Z])/).join(' '), + `${trackName.split(/(?=[A-Z])/).join(' ')} ${activeProposal ? activeProposal : ''}`, `/${trackName .split(/(?=[A-Z])/) .join('-') @@ -592,7 +659,7 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { case 'Treasury': gov2TrackItems.treasuryItems.push( getSiderMenuItem( - trackName.split(/(?=[A-Z])/).join(' '), + `${trackName.split(/(?=[A-Z])/).join(' ')} ${activeProposal ? activeProposal : ''}`, `/${trackName .split(/(?=[A-Z])/) .join('-') @@ -603,7 +670,7 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { case 'Whitelist': gov2TrackItems.fellowshipItems.push( getSiderMenuItem( - trackName.split(/(?=[A-Z])/).join(' '), + `${trackName.split(/(?=[A-Z])/).join(' ')} ${activeProposal ? activeProposal : ''}`, `/${trackName .split(/(?=[A-Z])/) .join('-') @@ -624,7 +691,7 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { ); gov2TrackItems.mainItems.push( getSiderMenuItem( - trackName.split(/(?=[A-Z])/).join(' '), + `${trackName.split(/(?=[A-Z])/).join(' ')} ${activeProposal ? activeProposal : ''}`, `/${trackName .split(/(?=[A-Z])/) .join('-') @@ -723,7 +790,10 @@ const AppLayout = ({ className, Component, pageProps }: Props) => { if (![AllNetworks.MOONBASE, AllNetworks.MOONBEAM, AllNetworks.MOONRIVER, AllNetworks.PICASSO].includes(network)) { let items = [...gov2TrackItems.treasuryItems]; if (isOpenGovSupported(network)) { - items = items.concat(getSiderMenuItem('Bounties', '/bounties', null), getSiderMenuItem('Child Bounties', '/child_bounties', null)); + items = items.concat( + getSiderMenuItem(`Bounties ${totalActiveProposalsCount?.['bountiesCount'] ? totalActiveProposalsCount?.['bountiesCount'] : ''}`, '/bounties', null), + getSiderMenuItem(`Child Bounties ${totalActiveProposalsCount?.['childBountiesCount'] ? totalActiveProposalsCount?.['childBountiesCount'] : ''}`, '/child_bounties', null) + ); } gov2Items.splice( -1, diff --git a/src/components/Post/GovernanceSideBar/PIPs/PIPsVote.tsx b/src/components/Post/GovernanceSideBar/PIPs/PIPsVote.tsx index 38c4ef6634..9573f7e197 100644 --- a/src/components/Post/GovernanceSideBar/PIPs/PIPsVote.tsx +++ b/src/components/Post/GovernanceSideBar/PIPs/PIPsVote.tsx @@ -38,6 +38,7 @@ import CustomButton from '~src/basic-components/buttons/CustomButton'; import ImageIcon from '~src/ui-components/ImageIcon'; import Alert from '~src/basic-components/Alert'; import SelectOption from '~src/basic-components/Select/SelectOption'; +import classNames from 'classnames'; const ZERO_BN = new BN(0); @@ -553,7 +554,7 @@ const PIPsVote = ({ className, referendumId, onAccountChange, lastVote, setLastV if ([ProposalType.TECHNICAL_PIPS, ProposalType.UPGRADE_PIPS].includes(proposalType)) { if (isPolymeshCommitteeMember) return VoteUI; - return
Only Polymesh Committee members may vote.
; + return
Only Polymesh Committee members may vote.
; } return VoteUI; diff --git a/src/queries.ts b/src/queries.ts index 350feaa61f..c695634129 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -2377,6 +2377,87 @@ query AllVotesForProposalIndex($type_eq: VoteType = ReferendumV2, $index_eq: Int } }`; +export const GET_NETWORK_TRACK_ACTIVE_PROPOSALS_COUNT = `query getNetworkTrackActiveProposalsCount { + proposals(where:{type_eq:ReferendumV2, status_in:[Started, DecisionDepositPlaced, Deciding,Submitted, ConfirmStarted]}){ + trackNumber + } + all: proposalsConnection(where:{type_eq:ReferendumV2, status_in:[Started, DecisionDepositPlaced, Deciding,Submitted, ConfirmStarted]} , orderBy:id_ASC){ + totalCount + } + bountiesCount: proposalsConnection(where:{type_in:Bounty, status_in:[Active, Proposed, Extended]}, orderBy:id_ASC) { + totalCount + } + childBountiesCount: proposalsConnection(where:{type_eq:ChildBounty, status_in:[Awarded,Added, Active]}, orderBy:id_ASC) { + totalCount + } +} +`; + +export const GOV1_NETWORK_ACTIVE_PROPOSALS_COUNT = `query gov1ActiveProposalsCount { + bounties: proposalsConnection(where:{type_in:Bounty, status_in:[Active, Proposed, Extended]}, orderBy:id_ASC) { + totalCount + } + childBounties: proposalsConnection(where:{type_eq:ChildBounty, status_in:[Awarded,Added, Active]}, orderBy:id_ASC) { + totalCount + } + councilMotions: proposalsConnection(where:{type_eq:CouncilMotion, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } + democracyProposals:proposalsConnection(where:{type_eq:DemocracyProposal, status_in:[Proposed ]}, orderBy:id_ASC) { + totalCount + } + referendums:proposalsConnection(where:{type_eq:Referendum, status_in:[Submitted, Started, ConfirmStarted,Deciding]}, orderBy:id_ASC) { + totalCount + } + tips: proposalsConnection(where:{type_eq:Tip, status_in:[Opened]}, orderBy:id_ASC) { + totalCount + } + treasuryProposals: proposalsConnection(where:{type_eq:TreasuryProposal, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } + techCommetteeProposals: proposalsConnection(where:{type_eq:TechCommitteeProposal, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } +}`; + +export const ZEITGEIST_NETWORK_ACTIVE_PROPOSALS_COUNT = ` +query zeitgeistActiveProposalsCount { + councilMotions: proposalsConnection(where:{type_eq:CouncilMotion, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } + democracyProposals:proposalsConnection(where:{type_eq:DemocracyProposal, status_in:[Proposed ]}, orderBy:id_ASC) { + totalCount + } + referendums:proposalsConnection(where:{type_eq:Referendum, status_in:[Submitted, Started, ConfirmStarted,Deciding]}, orderBy:id_ASC) { + totalCount + } + tips: proposalsConnection(where:{type_eq:Tip, status_in:[Opened]}, orderBy:id_ASC) { + totalCount + } + treasuryProposals: proposalsConnection(where:{type_eq:TreasuryProposal, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } + techCommitteeProposals: proposalsConnection(where:{type_eq:TechCommitteeProposal, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } + advisoryCommitteeMotions:proposalsConnection(where:{type_eq:AdvisoryCommittee, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } +}`; + +export const POLYMESH_NETWORK_ACTIVE_PROPOSALS_COUNT = ` +query polymeshActiveProposalsCount { + communityPips: proposalsConnection(where:{type_eq:Community, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } + technicalPips: proposalsConnection(where:{type_eq:TechnicalCommittee, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } + upgradePips: proposalsConnection(where:{type_eq:UpgradeCommittee, status_in:[Proposed]}, orderBy:id_ASC) { + totalCount + } +} +`; export const GET_TRACK_LEVEL_ANALYTICS_STATS = ` query getTrackLevelAnalyticsStats($track_num: Int! = 0, $before: DateTime ="2024-02-01T13:21:30.000000Z") { diffActiveProposals: proposalsConnection(where: { trackNumber_eq: $track_num, status_not_in: [Cancelled, TimedOut, Confirmed, Approved, Rejected, Executed, Killed, ExecutionFailed], createdAt_gt:$before }, orderBy: id_ASC){ diff --git a/src/types.ts b/src/types.ts index 3cadfe78a5..7274c3eaf6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -764,3 +764,23 @@ export interface IUserPostsListingResponse { gov1_total: number; open_gov_total: number; } + +export interface IActiveProposalCount { + [ + key: + | 'allCount' + | 'communityPipsCount' + | 'technicalPipsCount' + | 'upgradePipsCount' + | 'councilMotionsCount' + | 'democracyProposalsCount' + | 'referendumsCount' + | 'techCommetteeProposalsCount' + | 'tipsCount' + | 'treasuryProposalsCount' + | 'bountiesCount' + | 'childBountiesCount' + | 'advisoryCommitteeMotionsCount' + | string + ]: number; +}