From ac2b365aa27cdc2007f92c176ab78ce049944617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 26 Oct 2023 14:01:55 +0200 Subject: [PATCH 01/35] feature/oct-1077 allocation refactor draft --- .../AllocateRewardsBox/AllocateRewardsBox.tsx | 25 +++++++++++------ .../AllocationItem/AllocationItem.tsx | 4 +-- .../dedicated/AllocationItem/types.ts | 3 +- ...yTotalValueOfAllocationsAndAlphabetical.ts | 5 ++-- client/src/utils/getValueFiatToDisplay.ts | 7 ++++- .../views/AllocationView/AllocationView.tsx | 18 ++++++------ client/src/views/AllocationView/utils.ts | 28 ++++++++----------- 7 files changed, 50 insertions(+), 40 deletions(-) diff --git a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx index 6c988f60ea..6875db4ac8 100644 --- a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx +++ b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx @@ -1,5 +1,6 @@ import cx from 'classnames'; -import React, { FC, useState } from 'react'; +import { throttle } from 'lodash'; +import React, { FC, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import BoxRounded from 'components/core/BoxRounded/BoxRounded'; @@ -24,6 +25,19 @@ const AllocateRewardsBox: FC = ({ className, isDisabled setRewardsForProposals: state.setRewardsForProposals, })); + const onSetRewardsForProposals = (index: number) => { + if (!individualReward || isDisabled) { + return; + } + setRewardsForProposals(individualReward?.mul(index).div(100)); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const onSetRewardsForProposalsThrottled = useCallback( + throttle(onSetRewardsForProposals, 150), + [], + ); + if (!individualReward || individualReward.isZero()) { return ( = ({ className, isDisabled }, ]; - const onSetRewardsForProposals = (index: number) => { - if (!individualReward || isDisabled) { - return; - } - setRewardsForProposals(individualReward?.mul(index).div(100)); - }; - return ( = ({ className, isDisabled isDisabled={isDisabled} max={100} min={0} - onChange={onSetRewardsForProposals} + onChange={onSetRewardsForProposalsThrottled} onUnlock={onUnlock} value={percentRewardsForProposals} /> diff --git a/client/src/components/dedicated/AllocationItem/AllocationItem.tsx b/client/src/components/dedicated/AllocationItem/AllocationItem.tsx index 348f505351..58e9447925 100644 --- a/client/src/components/dedicated/AllocationItem/AllocationItem.tsx +++ b/client/src/components/dedicated/AllocationItem/AllocationItem.tsx @@ -1,5 +1,5 @@ import cx from 'classnames'; -import React, { FC, Fragment } from 'react'; +import React, { FC, Fragment, memo } from 'react'; import { useAccount } from 'wagmi'; import BoxRounded from 'components/core/BoxRounded/BoxRounded'; @@ -92,4 +92,4 @@ const AllocationItem: FC = ({ ); }; -export default AllocationItem; +export default memo(AllocationItem); diff --git a/client/src/components/dedicated/AllocationItem/types.ts b/client/src/components/dedicated/AllocationItem/types.ts index 3d5ba07f1b..db03486b2c 100644 --- a/client/src/components/dedicated/AllocationItem/types.ts +++ b/client/src/components/dedicated/AllocationItem/types.ts @@ -2,7 +2,8 @@ import { BigNumber } from 'ethers'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; -export interface AllocationItemWithAllocations extends ProposalIpfsWithRewards { +export interface AllocationItemWithAllocations + extends Pick { isAllocatedTo: boolean; value: BigNumber; } diff --git a/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.ts b/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.ts index 85087ba08d..5b612d342c 100644 --- a/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.ts +++ b/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.ts @@ -1,4 +1,3 @@ -import { AllocationItemWithAllocations } from 'components/dedicated/AllocationItem/types'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; const compareNames = (nameA: string | undefined, nameB: string | undefined) => { @@ -15,8 +14,8 @@ const compareNames = (nameA: string | undefined, nameB: string | undefined) => { }; export default function getSortedElementsByTotalValueOfAllocationsAndAlphabetical( - elements: (ProposalIpfsWithRewards | AllocationItemWithAllocations)[], -): (ProposalIpfsWithRewards | AllocationItemWithAllocations)[] { + elements: ProposalIpfsWithRewards[], +): ProposalIpfsWithRewards[] { return elements.sort( ( { totalValueOfAllocations: totalValueOfAllocationsA, name: nameA }, diff --git a/client/src/utils/getValueFiatToDisplay.ts b/client/src/utils/getValueFiatToDisplay.ts index 9c2060a089..34dc3d6d48 100644 --- a/client/src/utils/getValueFiatToDisplay.ts +++ b/client/src/utils/getValueFiatToDisplay.ts @@ -36,7 +36,12 @@ export default function getValueFiatToDisplay({ * and requesting to see its fiat value immediately. */ // - if (!cryptoCurrency || !cryptoValues || !cryptoValues[cryptoCurrency][displayCurrency] || !valueCrypto) { + if ( + !cryptoCurrency || + !cryptoValues || + !cryptoValues[cryptoCurrency][displayCurrency] || + !valueCrypto + ) { return `${prefix}0.00`; } diff --git a/client/src/views/AllocationView/AllocationView.tsx b/client/src/views/AllocationView/AllocationView.tsx index 40c8b0d965..5a7d17ed30 100644 --- a/client/src/views/AllocationView/AllocationView.tsx +++ b/client/src/views/AllocationView/AllocationView.tsx @@ -270,15 +270,15 @@ const AllocationView = (): ReactElement => { const isEpoch1 = currentEpoch === 1; + const showAllocationBottomNavigation = + !isEpoch1 && areAllocationsAvailableOrAlreadyDone && hasUserIndividualReward && !isLocked; + return ( { )} {areAllocationsAvailableOrAlreadyDone && ( - {allocationsWithRewards!.map((allocation, index) => ( + {allocationsWithRewards!.map(allocation => ( ))} diff --git a/client/src/views/AllocationView/utils.ts b/client/src/views/AllocationView/utils.ts index d0f046e88c..5926a537a6 100644 --- a/client/src/views/AllocationView/utils.ts +++ b/client/src/views/AllocationView/utils.ts @@ -3,7 +3,6 @@ import { BigNumber } from 'ethers'; import { AllocationItemWithAllocations } from 'components/dedicated/AllocationItem/types'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; import { UserAllocationElement } from 'hooks/queries/useUserAllocations'; -import getSortedElementsByTotalValueOfAllocationsAndAlphabetical from 'utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical'; import { AllocationValues } from './types'; @@ -59,10 +58,10 @@ export function getAllocationValuesInitialState({ ? userAllocationsElement.value : BigNumber.from(0); // percentage of rewardsForProposals as part of userAllocationsElementsValuesSum. - const percentage = (rewardsForProposals && !!userAllocationsElementsValuesSum) ? 100 : - rewardsForProposals.mul(100) - .div(userAllocationsElementsValuesSum!) - .toString(); + const percentage = + rewardsForProposals && !!userAllocationsElementsValuesSum + ? 100 + : rewardsForProposals.mul(100).div(userAllocationsElementsValuesSum!).toString(); // value for the project set as valueFromAllocation multiplied by percentage. const value = rewardsForProposals.isZero() ? valueFromAllocation @@ -94,7 +93,7 @@ export function getAllocationsWithRewards({ proposalsIpfsWithRewards && proposalsIpfsWithRewards.length > 0 && areAllocationsAvailableOrAlreadyDone; - let allocationsWithRewards = isDataDefined + const allocationsWithRewards = isDataDefined ? allocationValues!.map(allocationValue => { const proposal = proposalsIpfsWithRewards.find( ({ address }) => address === allocationValue.address, @@ -111,21 +110,18 @@ export function getAllocationsWithRewards({ }) : []; - allocationsWithRewards.sort(({ value: valueA }, { value: valueB }) => { - if (valueA.lt(valueB)) { - return 1; + return allocationsWithRewards.sort(({ name: nameA }, { name: nameB }) => { + if (!nameA || !nameB) { + return 0; } - if (valueA.gt(valueB)) { + if (nameA < nameB) { return -1; } + if (nameA > nameB) { + return 1; + } return 0; }); - - allocationsWithRewards = getSortedElementsByTotalValueOfAllocationsAndAlphabetical( - allocationsWithRewards as AllocationItemWithAllocations[], - ) as AllocationItemWithAllocations[]; - - return allocationsWithRewards; } export function getRestToDistribute({ From afaf2b9a8888d15a450a074817db7fce05e0abd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 26 Oct 2023 14:13:04 +0200 Subject: [PATCH 02/35] oct-1077 fix throttling + useEffect --- .../dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx | 8 ++++---- client/src/views/AllocationView/AllocationView.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx index 6875db4ac8..9bd7a3914b 100644 --- a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx +++ b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx @@ -33,10 +33,10 @@ const AllocateRewardsBox: FC = ({ className, isDisabled }; // eslint-disable-next-line react-hooks/exhaustive-deps - const onSetRewardsForProposalsThrottled = useCallback( - throttle(onSetRewardsForProposals, 150), - [], - ); + const onSetRewardsForProposalsThrottled = useCallback(throttle(onSetRewardsForProposals, 150), [ + isDisabled, + individualReward?.toHexString(), + ]); if (!individualReward || individualReward.isZero()) { return ( diff --git a/client/src/views/AllocationView/AllocationView.tsx b/client/src/views/AllocationView/AllocationView.tsx index 5a7d17ed30..5deb66d98d 100644 --- a/client/src/views/AllocationView/AllocationView.tsx +++ b/client/src/views/AllocationView/AllocationView.tsx @@ -180,7 +180,7 @@ const AllocationView = (): ReactElement => { useEffect(() => { onResetAllocationValues(isResetDone); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rewardsForProposals, isResetDone]); + }, [rewardsForProposals.toHexString(), isResetDone]); const onAllocate = () => { if (userNonce === undefined || proposalsContract === undefined) { From 57e5a0179e3a5cd995263da6d7e8e48b8ea8b65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 26 Oct 2023 14:19:13 +0200 Subject: [PATCH 03/35] oct-1077 type-check fix --- client/src/components/dedicated/AllocationItem/types.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/components/dedicated/AllocationItem/types.ts b/client/src/components/dedicated/AllocationItem/types.ts index db03486b2c..b901669b49 100644 --- a/client/src/components/dedicated/AllocationItem/types.ts +++ b/client/src/components/dedicated/AllocationItem/types.ts @@ -3,7 +3,10 @@ import { BigNumber } from 'ethers'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; export interface AllocationItemWithAllocations - extends Pick { + extends Pick< + ProposalIpfsWithRewards, + 'address' | 'isLoadingError' | 'name' | 'profileImageSmall' + > { isAllocatedTo: boolean; value: BigNumber; } From 710029a378af57858c72d06c5804dce43e7031be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Tue, 31 Oct 2023 09:05:30 +0000 Subject: [PATCH 04/35] oct-1077 AllocationItem memo --- .../dedicated/AllocationItem/types.ts | 9 +++------ ...yTotalValueOfAllocationsAndAlphabetical.ts | 5 +++-- client/src/views/AllocationView/utils.ts | 20 +++++++++++-------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/client/src/components/dedicated/AllocationItem/types.ts b/client/src/components/dedicated/AllocationItem/types.ts index b901669b49..a1c9848f38 100644 --- a/client/src/components/dedicated/AllocationItem/types.ts +++ b/client/src/components/dedicated/AllocationItem/types.ts @@ -2,16 +2,13 @@ import { BigNumber } from 'ethers'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; -export interface AllocationItemWithAllocations - extends Pick< - ProposalIpfsWithRewards, - 'address' | 'isLoadingError' | 'name' | 'profileImageSmall' - > { +export interface AllocationItemWithAllocations extends ProposalIpfsWithRewards { isAllocatedTo: boolean; value: BigNumber; } -export default interface AllocationItemProps extends AllocationItemWithAllocations { +export default interface AllocationItemProps + extends Omit { className?: string; isDisabled: boolean; isLocked: boolean; diff --git a/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.ts b/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.ts index 5b612d342c..85087ba08d 100644 --- a/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.ts +++ b/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.ts @@ -1,3 +1,4 @@ +import { AllocationItemWithAllocations } from 'components/dedicated/AllocationItem/types'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; const compareNames = (nameA: string | undefined, nameB: string | undefined) => { @@ -14,8 +15,8 @@ const compareNames = (nameA: string | undefined, nameB: string | undefined) => { }; export default function getSortedElementsByTotalValueOfAllocationsAndAlphabetical( - elements: ProposalIpfsWithRewards[], -): ProposalIpfsWithRewards[] { + elements: (ProposalIpfsWithRewards | AllocationItemWithAllocations)[], +): (ProposalIpfsWithRewards | AllocationItemWithAllocations)[] { return elements.sort( ( { totalValueOfAllocations: totalValueOfAllocationsA, name: nameA }, diff --git a/client/src/views/AllocationView/utils.ts b/client/src/views/AllocationView/utils.ts index c8e3bbf70b..5a375609a9 100644 --- a/client/src/views/AllocationView/utils.ts +++ b/client/src/views/AllocationView/utils.ts @@ -4,6 +4,7 @@ import { parseUnits } from 'ethers/lib/utils'; import { AllocationItemWithAllocations } from 'components/dedicated/AllocationItem/types'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; import { UserAllocationElement } from 'hooks/queries/useUserAllocations'; +import getSortedElementsByTotalValueOfAllocationsAndAlphabetical from 'utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical'; import { AllocationValues } from './types'; @@ -141,7 +142,7 @@ export function getAllocationsWithRewards({ proposalsIpfsWithRewards && proposalsIpfsWithRewards.length > 0 && areAllocationsAvailableOrAlreadyDone; - const allocationsWithRewards = isDataDefined + let allocationsWithRewards = isDataDefined ? allocationValues!.map(allocationValue => { const proposal = proposalsIpfsWithRewards.find( ({ address }) => address === allocationValue.address, @@ -158,18 +159,21 @@ export function getAllocationsWithRewards({ }) : []; - return allocationsWithRewards.sort(({ name: nameA }, { name: nameB }) => { - if (!nameA || !nameB) { - return 0; + allocationsWithRewards.sort(({ value: valueA }, { value: valueB }) => { + if (valueA.lt(valueB)) { + return 1; } - if (nameA < nameB) { + if (valueA.gt(valueB)) { return -1; } - if (nameA > nameB) { - return 1; - } return 0; }); + + allocationsWithRewards = getSortedElementsByTotalValueOfAllocationsAndAlphabetical( + allocationsWithRewards as AllocationItemWithAllocations[], + ) as AllocationItemWithAllocations[]; + + return allocationsWithRewards; } export function getRestToDistribute({ From 8b96b8243fd889ed7df05882d4daffb281cfd040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 3 Nov 2023 11:51:59 +0000 Subject: [PATCH 05/35] [FIX] Wrong payouts-verification url for merkle_tree --- payouts-verification/src/Comparison/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payouts-verification/src/Comparison/index.tsx b/payouts-verification/src/Comparison/index.tsx index ba941bd3e3..0e55a9323b 100644 --- a/payouts-verification/src/Comparison/index.tsx +++ b/payouts-verification/src/Comparison/index.tsx @@ -21,7 +21,7 @@ const Comparison: FC = ({ uploadedMerkleTree }) => { axios // @ts-expect-error TS does not understand the way vite imports envs. - .get(`${import.meta.env.VITE_SERVER_ENDPOINT}${epoch}`) + .get(`${import.meta.env.VITE_SERVER_ENDPOINT}rewards/merkle_tree/${epoch}`) .then(({ data }) => { const serverMerkleTreeRoot = data.root; setRootsAreTheSame(serverMerkleTreeRoot === uploadedMerkleTree.root); From 116326d1b0bf2f944fb46892d3c6baa74fbf7354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 3 Nov 2023 21:04:18 +0100 Subject: [PATCH 06/35] hotfix: AllocateRewardsBox to be disabled when outside AW --- .../AllocateRewardsBox/AllocateRewardsBox.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx index bb4f9a8aab..e4eabeae95 100644 --- a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx +++ b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx @@ -8,6 +8,7 @@ import BoxRounded from 'components/core/BoxRounded/BoxRounded'; import Slider from 'components/core/Slider/Slider'; import ModalAllocationValuesEdit from 'components/dedicated/ModalAllocationValuesEdit/ModalAllocationValuesEdit'; import useIndividualReward from 'hooks/queries/useIndividualReward'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; import useAllocationsStore from 'store/allocations/store'; import getValueCryptoToDisplay from 'utils/getValueCryptoToDisplay'; @@ -19,6 +20,7 @@ const AllocateRewardsBox: FC = ({ className, isDisabled keyPrefix: 'components.dedicated.allocationRewardsBox', }); const { data: individualReward } = useIndividualReward(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); const [modalMode, setModalMode] = useState<'closed' | 'donate' | 'withdraw'>('closed'); const { rewardsForProposals, setRewardsForProposals } = useAllocationsStore(state => ({ rewardsForProposals: state.data.rewardsForProposals, @@ -26,6 +28,8 @@ const AllocateRewardsBox: FC = ({ className, isDisabled })); const hasUserIndividualReward = !!individualReward && !individualReward.isZero(); + const isDecisionWindowOpenAndHasIndividualReward = + hasUserIndividualReward && isDecisionWindowOpen; const onSetRewardsForProposals = (index: number) => { if (!individualReward || isDisabled) { @@ -40,19 +44,22 @@ const AllocateRewardsBox: FC = ({ className, isDisabled individualReward?.toHexString(), ]); - const percentRewardsForProposals = !hasUserIndividualReward - ? 50 - : rewardsForProposals.mul(100).div(individualReward).toNumber(); + const percentRewardsForProposals = isDecisionWindowOpenAndHasIndividualReward + ? rewardsForProposals.mul(100).div(individualReward).toNumber() + : 50; const percentWithdraw = 100 - percentRewardsForProposals; - const rewardsForWithdraw = !hasUserIndividualReward - ? BigNumber.from(0) - : individualReward.sub(rewardsForProposals); + const rewardsForProposalsFinal = isDecisionWindowOpenAndHasIndividualReward + ? rewardsForProposals + : BigNumber.from(0); + const rewardsForWithdraw = isDecisionWindowOpenAndHasIndividualReward + ? individualReward?.sub(rewardsForProposals) + : BigNumber.from(0); const sections = [ { header: t('donate', { percentRewardsForProposals }), value: getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', - valueCrypto: rewardsForProposals, + valueCrypto: rewardsForProposalsFinal, }), }, { @@ -69,7 +76,7 @@ const AllocateRewardsBox: FC = ({ className, isDisabled className={cx(styles.root, className)} isVertical subtitle={ - hasUserIndividualReward + isDecisionWindowOpen ? t('subtitle', { individualReward: getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', From 96868061df1ea9cd67512bb983ec27fe1c4764fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 3 Nov 2023 21:11:20 +0100 Subject: [PATCH 07/35] hotfix: AllocateRewardsBox to be disabled when outside AW, vol. 2 --- .../dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx index e4eabeae95..428dfc3a50 100644 --- a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx +++ b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx @@ -76,7 +76,7 @@ const AllocateRewardsBox: FC = ({ className, isDisabled className={cx(styles.root, className)} isVertical subtitle={ - isDecisionWindowOpen + isDecisionWindowOpenAndHasIndividualReward ? t('subtitle', { individualReward: getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', From 2f001176e293280a7ad3c9301bceeb395e6d51c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 3 Nov 2023 20:32:16 +0000 Subject: [PATCH 08/35] [TEST] Test cases for getSortedElementsByTotalValueOfAllocationsAndAlphabetical util --- client/src/mocks/subgraph/proposals.ts | 34 ++--- ...lValueOfAllocationsAndAlphabetical.test.ts | 125 ++++++++++++------ 2 files changed, 102 insertions(+), 57 deletions(-) diff --git a/client/src/mocks/subgraph/proposals.ts b/client/src/mocks/subgraph/proposals.ts index 7a84fc5a86..59e84c5ec4 100644 --- a/client/src/mocks/subgraph/proposals.ts +++ b/client/src/mocks/subgraph/proposals.ts @@ -2,7 +2,7 @@ import { BigNumber } from 'ethers'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; -export const mockedProposalA: ProposalIpfsWithRewards = { +export const mockedProposalATotalValueOfAllocations1: ProposalIpfsWithRewards = { address: 'address', isLoadingError: false, name: 'A', @@ -10,45 +10,45 @@ export const mockedProposalA: ProposalIpfsWithRewards = { totalValueOfAllocations: BigNumber.from(1), }; -export const mockedProposalANoAllocation = { - ...mockedProposalA, +export const mockedProposalATotalValueOfAllocationsUndefined = { + ...mockedProposalATotalValueOfAllocations1, totalValueOfAllocations: undefined, }; -export const mockedProposalAHigherAllocation: ProposalIpfsWithRewards = { - ...mockedProposalA, +export const mockedProposalATotalValueOfAllocations2: ProposalIpfsWithRewards = { + ...mockedProposalATotalValueOfAllocations1, totalValueOfAllocations: BigNumber.from(2), }; -export const mockedProposalB: ProposalIpfsWithRewards = { - ...mockedProposalA, +export const mockedProposalBTotalValueOfAllocations2: ProposalIpfsWithRewards = { + ...mockedProposalATotalValueOfAllocations1, name: 'B', totalValueOfAllocations: BigNumber.from(2), }; -export const mockedProposalBNoAllocation = { - ...mockedProposalB, +export const mockedProposalBTotalValueOfAllocationsUndefined = { + ...mockedProposalBTotalValueOfAllocations2, totalValueOfAllocations: undefined, }; -export const mockedProposalC: ProposalIpfsWithRewards = { - ...mockedProposalA, +export const mockedProposalCTotalValueOfAllocations3: ProposalIpfsWithRewards = { + ...mockedProposalATotalValueOfAllocations1, name: 'C', totalValueOfAllocations: BigNumber.from(3), }; -export const mockedProposalCNoAllocation = { - ...mockedProposalC, +export const mockedProposalCTotalValueOfAllocationsUndefined = { + ...mockedProposalCTotalValueOfAllocations3, totalValueOfAllocations: undefined, }; -export const mockedProposalD: ProposalIpfsWithRewards = { - ...mockedProposalA, +export const mockedProposalDTotalValueOfAllocations4: ProposalIpfsWithRewards = { + ...mockedProposalATotalValueOfAllocations1, name: 'D', totalValueOfAllocations: BigNumber.from(4), }; -export const mockedProposalDNoAllocation = { - ...mockedProposalD, +export const mockedProposalDTotalValueOfAllocationsUndefined = { + ...mockedProposalDTotalValueOfAllocations4, totalValueOfAllocations: undefined, }; diff --git a/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.test.ts b/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.test.ts index 0d3cae4055..b6f8544b80 100644 --- a/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.test.ts +++ b/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.test.ts @@ -1,47 +1,92 @@ import { - mockedProposalA, - mockedProposalB, - mockedProposalC, - mockedProposalBNoAllocation, - mockedProposalD, - mockedProposalCNoAllocation, - mockedProposalDNoAllocation, - mockedProposalANoAllocation, - mockedProposalAHigherAllocation, + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocations2, + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocationsUndefined, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations2, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalCTotalValueOfAllocations3, + mockedProposalDTotalValueOfAllocations4, } from 'mocks/subgraph/proposals'; import getSortedElementsByTotalValueOfAllocationsAndAlphabetical from './getSortedElementsByTotalValueOfAllocationsAndAlphabetical'; +const expectedValueRoot = [ + mockedProposalDTotalValueOfAllocations4, + mockedProposalCTotalValueOfAllocations3, + mockedProposalATotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations2, + mockedProposalBTotalValueOfAllocations2, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations1, + mockedProposalATotalValueOfAllocations1, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocationsUndefined, +]; + +const testCases = [ + { + argument: [ + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations2, + mockedProposalCTotalValueOfAllocations3, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocations4, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations2, + ], + expectedValue: expectedValueRoot, + }, + { + argument: [ + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations2, + mockedProposalCTotalValueOfAllocations3, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations1, + mockedProposalDTotalValueOfAllocations4, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations2, + ], + expectedValue: expectedValueRoot, + }, + { + argument: [ + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations2, + mockedProposalCTotalValueOfAllocations3, + mockedProposalBTotalValueOfAllocations2, + mockedProposalDTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations1, + mockedProposalDTotalValueOfAllocations4, + mockedProposalATotalValueOfAllocations2, + ], + expectedValue: expectedValueRoot, + }, +]; + describe('getSortedElementsByTotalValueOfAllocationsAndAlphabetical', () => { - it('properly sorts elements', () => { - expect( - getSortedElementsByTotalValueOfAllocationsAndAlphabetical([ - mockedProposalA, - mockedProposalB, - mockedProposalAHigherAllocation, - mockedProposalC, - mockedProposalB, - mockedProposalA, - mockedProposalBNoAllocation, - mockedProposalD, - mockedProposalCNoAllocation, - mockedProposalDNoAllocation, - mockedProposalANoAllocation, - mockedProposalAHigherAllocation, - ]), - ).toEqual([ - mockedProposalD, - mockedProposalC, - mockedProposalAHigherAllocation, - mockedProposalAHigherAllocation, - mockedProposalB, - mockedProposalB, - mockedProposalA, - mockedProposalA, - mockedProposalANoAllocation, - mockedProposalBNoAllocation, - mockedProposalCNoAllocation, - mockedProposalDNoAllocation, - ]); - }); + for (const { argument, expectedValue } of testCases) { + it('properly returns expectedValue', () => { + expect(getSortedElementsByTotalValueOfAllocationsAndAlphabetical(argument)).toEqual( + expectedValue, + ); + }); + } }); From 9e93ca8f961b22d1b1a10b217d2dd920199bc93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Fri, 3 Nov 2023 23:45:32 +0100 Subject: [PATCH 09/35] More verbse image tag update --- .gitlab-ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e36dd06cd2..904ef14d03 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -657,8 +657,18 @@ Deploy Release Candidate app: cd $GIT_DIR - cat mainnet/octant-image.values.yaml | yq -r ".[0].value.value = \"$IMAGE_TAG\"" | tee mainnet/octant-image.values.yaml - cat testnet/octant-image.values.yaml | yq -r ".[0].value.value = \"$IMAGE_TAG\"" | tee testnet/octant-image.values.yaml + echo "(debug) before update ==="" + cat mainnet/octant-image.values.yaml + cat testnet/octant-image.values.yaml + echo "(end debug) ==="" + + yq -i -e ".[].value.value = \"$IMAGE_TAG\"" mainnet/octant-image.values.yaml + yq -i -e ".[].value.value = \"$IMAGE_TAG\"" testnet/octant-image.values.yaml + + echo "(debug) after update ==="" + cat mainnet/octant-image.values.yaml + cat testnet/octant-image.values.yaml + echo "(end debug) ==="" git add mainnet/octant-image.values.yaml git add testnet/octant-image.values.yaml From b27604e7e041922eb56c65e806269f661006ac2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Mon, 6 Nov 2023 22:18:24 +0100 Subject: [PATCH 10/35] test, hunt for E2E random error: enable E2E --- .gitlab-ci.yml | 220 ++++++++++++++++++++++++------------------------- 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 904ef14d03..3be83aacd6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -354,21 +354,21 @@ Build images: - export LOCAL_RPC_URL=https://$(bash $CI_PROJECT_DIR/ci/argocd/get_rpc_url.sh) - /app/entrypoint.sh $NETWORK $CI_PROJECT_DIR/build.env -#Run E2E App: -# extends: -# - .deploy_anvil -# rules: -# - !reference [.rules, on_mr ] -# - !reference [.rules, on_push_to_default_branch ] -# - !reference [.rules, on_push_to_master_branch ] -# variables: -# ENV_TYPE: "e2e" -# environment: -# name: e2e/$CI_PIPELINE_IID -# url: https://mr-$CI_MERGE_REQUEST_IID-e2e-$CI_PIPELINE_ID-client.octant.wildland.dev -# deployment_tier: development -# on_stop: Destroy E2E App -# auto_stop_in: 6 hours +Run E2E App: + extends: + - .deploy_anvil + rules: + - !reference [.rules, on_mr ] + - !reference [.rules, on_push_to_default_branch ] + - !reference [.rules, on_push_to_master_branch ] + variables: + ENV_TYPE: "e2e" + environment: + name: e2e/$CI_PIPELINE_IID + url: https://mr-$CI_MERGE_REQUEST_IID-e2e-$CI_PIPELINE_ID-client.octant.wildland.dev + deployment_tier: development + on_stop: Destroy E2E App + auto_stop_in: 6 hours Run MR App: extends: @@ -383,16 +383,16 @@ Run MR App: deployment_tier: development on_stop: Destroy MR App -#E2E contracts: -# extends: -# - .deploy_anvil_contracts -# needs: ["Run E2E App"] -# rules: -# - !reference [.rules, on_mr ] -# - !reference [.rules, on_push_to_default_branch ] -# - !reference [.rules, on_push_to_master_branch ] -# variables: -# ENV_TYPE: "e2e" +E2E contracts: + extends: + - .deploy_anvil_contracts + needs: ["Run E2E App"] + rules: + - !reference [.rules, on_mr ] + - !reference [.rules, on_push_to_default_branch ] + - !reference [.rules, on_push_to_master_branch ] + variables: + ENV_TYPE: "e2e" MR contracts: extends: @@ -403,23 +403,23 @@ MR contracts: variables: ENV_TYPE: "mr" -#E2E app deploy: -# extends: -# - .deploy_app -# needs: ["E2E contracts"] -# dependencies: ["E2E contracts"] -# rules: -# - !reference [.rules, on_mr ] -# - !reference [.rules, on_push_to_default_branch ] -# - !reference [.rules, on_push_to_master_branch ] -# variables: -# ENV_TYPE: "e2e" -# NETWORK_NAME: "local" -# NETWORK_ID: "1337" -# SNAPSHOTTER_ENABLED: "true" -# SCHEDULER_ENABLED: "true" -# GLM_CLAIM_ENABLED: "true" -# VAULT_CONFIRM_WITHDRAWALS_ENABLED: "true" +E2E app deploy: + extends: + - .deploy_app + needs: ["E2E contracts"] + dependencies: ["E2E contracts"] + rules: + - !reference [.rules, on_mr ] + - !reference [.rules, on_push_to_default_branch ] + - !reference [.rules, on_push_to_master_branch ] + variables: + ENV_TYPE: "e2e" + NETWORK_NAME: "local" + NETWORK_ID: "1337" + SNAPSHOTTER_ENABLED: "true" + SCHEDULER_ENABLED: "true" + GLM_CLAIM_ENABLED: "true" + VAULT_CONFIRM_WITHDRAWALS_ENABLED: "true" MR app deploy: extends: @@ -525,19 +525,19 @@ Wait for Master: environment: action: stop -#Destroy E2E App: -# extends: -# - .destroy_app -# needs: ["Run E2E App"] -# variables: -# ENV_TYPE: "e2e" -# rules: -# - !reference [.rules, on_mr_manual ] -# - !reference [.rules, on_push_to_default_branch_manual ] -# - !reference [.rules, on_push_to_master_branch_manual ] -# environment: -# name: e2e/$CI_PIPELINE_IID -# deployment_tier: development +Destroy E2E App: + extends: + - .destroy_app + needs: ["Run E2E App"] + variables: + ENV_TYPE: "e2e" + rules: + - !reference [.rules, on_mr_manual ] + - !reference [.rules, on_push_to_default_branch_manual ] + - !reference [.rules, on_push_to_master_branch_manual ] + environment: + name: e2e/$CI_PIPELINE_IID + deployment_tier: development Destroy MR App: extends: @@ -579,61 +579,61 @@ Destroy Master App: name: persistent/master deployment_tier: testing -#E2E Epoch 1: -# stage: application -# needs: ["E2E app deploy"] -# image: !reference [.images, synpress ] -# <<: *env_resolve_init -# rules: -# - !reference [.rules, on_mr] -# - !reference [.rules, on_push_to_default_branch ] -# - !reference [.rules, on_push_to_master_branch ] -# artifacts: -# when: on_failure -# name: cypress -# paths: -# - client/cypress/videos -# - client/cypress/screenshots -# expire_in: 3 days -# cache: -# - key: $CI_COMMIT_REF_SLUG-yarn-client -# policy: pull -# paths: -# - client/.yarn -# - client/node-modules -# - key: $CI_COMMIT_REF_SLUG-yarn-root -# policy: pull -# paths: -# - node_modules -# - .yarn -# script: -# - set -e -# # Setup NVM to use Node version 16 -# - source /usr/share/nvm/init-nvm.sh -# - nvm use 16 -# - npm i -g yarn -# - cd client -# - yarn install --cache-folder .yarn --frozen-lockfile --prefer-offline --no-audit -# # Wait for the E2E app to become ready -# - bash $CI_PROJECT_DIR/ci/argocd/wait_for_app.sh -# - export OCTANT_BASE_URL=https://$(bash $CI_PROJECT_DIR/ci/argocd/get_web_client_url.sh) -# - set +e -# - yarn synpress:run || CY_EXIT_CODE=$? -# - if [[ "$CY_EXIT_CODE" == "0" ]]; then rm -r $CI_PROJECT_DIR/client/cypress/videos $CI_PROJECT_DIR/client/cypress/screenshots; fi -# - set -e -# # Trigger the stop job -# - | -# JOB_ID=$(curl --fail -s -XGET --header "PRIVATE-TOKEN: $CI_JOB_CONTROLLER" https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs | jq '.[] | select(.name == "Destroy E2E App") | .id') -# -# curl -s --fail -X POST \ -# -H "PRIVATE-TOKEN: $CI_JOB_CONTROLLER" \ -# "$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/$JOB_ID/play" -# - exit $CY_EXIT_CODE -# variables: -# ENV_TYPE: "e2e" -# CYPRESS_DOCKER_RUN: "true" -# CI: "true" -# METAMASK_VERSION: "10.25.0" +E2E Epoch 1: + stage: application + needs: ["E2E app deploy"] + image: !reference [.images, synpress ] + <<: *env_resolve_init + rules: + - !reference [.rules, on_mr] + - !reference [.rules, on_push_to_default_branch ] + - !reference [.rules, on_push_to_master_branch ] + artifacts: + when: on_failure + name: cypress + paths: + - client/cypress/videos + - client/cypress/screenshots + expire_in: 3 days + cache: + - key: $CI_COMMIT_REF_SLUG-yarn-client + policy: pull + paths: + - client/.yarn + - client/node-modules + - key: $CI_COMMIT_REF_SLUG-yarn-root + policy: pull + paths: + - node_modules + - .yarn + script: + - set -e + # Setup NVM to use Node version 16 + - source /usr/share/nvm/init-nvm.sh + - nvm use 16 + - npm i -g yarn + - cd client + - yarn install --cache-folder .yarn --frozen-lockfile --prefer-offline --no-audit + # Wait for the E2E app to become ready + - bash $CI_PROJECT_DIR/ci/argocd/wait_for_app.sh + - export OCTANT_BASE_URL=https://$(bash $CI_PROJECT_DIR/ci/argocd/get_web_client_url.sh) + - set +e + - yarn synpress:run || CY_EXIT_CODE=$? + - if [[ "$CY_EXIT_CODE" == "0" ]]; then rm -r $CI_PROJECT_DIR/client/cypress/videos $CI_PROJECT_DIR/client/cypress/screenshots; fi + - set -e + # Trigger the stop job + - | + JOB_ID=$(curl --fail -s -XGET --header "PRIVATE-TOKEN: $CI_JOB_CONTROLLER" https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs | jq '.[] | select(.name == "Destroy E2E App") | .id') + + curl -s --fail -X POST \ + -H "PRIVATE-TOKEN: $CI_JOB_CONTROLLER" \ + "$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/$JOB_ID/play" + - exit $CY_EXIT_CODE + variables: + ENV_TYPE: "e2e" + CYPRESS_DOCKER_RUN: "true" + CI: "true" + METAMASK_VERSION: "10.25.0" Deploy Release Candidate app: stage: deploy From 0d9cb89518043fc7bf6593ff33a844fb10a4b334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Mon, 6 Nov 2023 22:21:59 +0100 Subject: [PATCH 11/35] test: CY onboarding updated, vite sourcemaps added --- client/cypress/e2e/onboarding.cy.ts | 10 +++++----- client/vite.config.js | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts index 06038aa706..f8c23415e0 100644 --- a/client/cypress/e2e/onboarding.cy.ts +++ b/client/cypress/e2e/onboarding.cy.ts @@ -1,6 +1,6 @@ import { visitWithLoader, navigateWithCheck } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import steps from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { stepsDecisionWindowClosed } from 'src/hooks/helpers/useOnboardingSteps/steps'; import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -231,12 +231,12 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < steps.length - 1; i++) { + for (let i = 1; i < stepsDecisionWindowClosed.length - 1; i++) { checkProgressStepperSlimIsCurrentAndClickNext(i); } cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(steps.length - 1) + .eq(stepsDecisionWindowClosed.length - 1) .click(); cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); }); @@ -318,12 +318,12 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => it('onboarding should have one more step (TOS)', () => { cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]').should( 'have.length', - steps.length + 1, + stepsDecisionWindowClosed.length + 1, ); }); it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < steps.length; i++) { + for (let i = 1; i < stepsDecisionWindowClosed.length; i++) { checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); } }); diff --git a/client/vite.config.js b/client/vite.config.js index ffce81d710..6a770a2873 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -55,6 +55,9 @@ export default defineConfig(({ mode }) => { return { base, + build: { + sourcemap: 'inline', + }, css: { modules: { generateScopedName: localIdentName, From 947ab87d4f571f0c0fa13bf775109de8b2cf4582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 00:15:21 +0100 Subject: [PATCH 12/35] test: change E2E onboarding steps, tscnfig sourcemaps enabled --- client/cypress/e2e/onboarding.cy.ts | 10 +++++----- client/cypress/tsconfig.json | 1 + client/tsconfig.json | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts index f8c23415e0..780979f527 100644 --- a/client/cypress/e2e/onboarding.cy.ts +++ b/client/cypress/e2e/onboarding.cy.ts @@ -1,6 +1,6 @@ import { visitWithLoader, navigateWithCheck } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { stepsDecisionWindowClosed } from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { stepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -231,12 +231,12 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < stepsDecisionWindowClosed.length - 1; i++) { + for (let i = 1; i < stepsDecisionWindowOpen.length - 1; i++) { checkProgressStepperSlimIsCurrentAndClickNext(i); } cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(stepsDecisionWindowClosed.length - 1) + .eq(stepsDecisionWindowOpen.length - 1) .click(); cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); }); @@ -318,12 +318,12 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => it('onboarding should have one more step (TOS)', () => { cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]').should( 'have.length', - stepsDecisionWindowClosed.length + 1, + stepsDecisionWindowOpen.length + 1, ); }); it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < stepsDecisionWindowClosed.length; i++) { + for (let i = 1; i < stepsDecisionWindowOpen.length; i++) { checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); } }); diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json index 4386f04bc7..0dc84b8343 100644 --- a/client/cypress/tsconfig.json +++ b/client/cypress/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { + "sourceMap": true, "allowJs": true, "baseUrl": "../", "types": [ diff --git a/client/tsconfig.json b/client/tsconfig.json index 54c51f2598..e236c79f26 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -20,6 +20,7 @@ "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, + "sourceMap": true, "strict": true, "target": "es2022" }, From 4ac9848ca80cdd6205f3f4708c9d74dc8327e669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 00:53:15 +0100 Subject: [PATCH 13/35] test: inline sourcemaps, onboarding tests removed --- client/cypress/e2e/onboarding.cy.ts | 381 ---------------------------- client/cypress/tsconfig.json | 1 + client/tsconfig.json | 1 + 3 files changed, 2 insertions(+), 381 deletions(-) delete mode 100644 client/cypress/e2e/onboarding.cy.ts diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts deleted file mode 100644 index 780979f527..0000000000 --- a/client/cypress/e2e/onboarding.cy.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { visitWithLoader, navigateWithCheck } from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { stepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; -import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -import Chainable = Cypress.Chainable; - -const connectWallet = ( - isTOSAccepted: boolean, - shouldVisit = true, - shouldReload = false, -): Chainable => { - cy.intercept('GET', '/user/*/tos', { body: { accepted: isTOSAccepted } }); - cy.disconnectMetamaskWalletFromAllDapps(); - if (shouldVisit) { - visitWithLoader(ROOT.absolute, ROOT_ROUTES.proposals.absolute); - } - if (shouldReload) { - cy.reload(); - } - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); - cy.switchToMetamaskNotification(); - return cy.acceptMetamaskAccess(); -}; - -const beforeSetup = () => { - cy.clearLocalStorage(); - cy.setupMetamask(); - cy.activateShowTestnetNetworksInMetamask(); - cy.changeMetamaskNetwork('sepolia'); - window.innerWidth = Cypress.config().viewportWidth; - window.innerHeight = Cypress.config().viewportHeight; -}; - -const checkCurrentElement = (el: number, isCurrent: boolean): Chainable => { - return cy - .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(el) - .invoke('attr', 'data-iscurrent') - .should('eq', `${isCurrent}`); -}; - -const checkProgressStepperSlimIsCurrentAndClickNext = (index, isCurrent = true): Chainable => { - checkCurrentElement(index - 1, isCurrent); - return cy - .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(index) - .click({ force: true }); -}; - -const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { el: 1, key: 'ArrowRight' }, - { el: 2, key: 'ArrowRight' }, - { el: 3, key: 'ArrowRight' }, - { el: 3, key: 'ArrowRight' }, - { el: 2, key: 'ArrowLeft' }, - { el: 1, key: 'ArrowLeft' }, - { el: 0, key: 'ArrowLeft' }, - { el: 0, key: 'ArrowLeft' }, - ].forEach(({ key, el }) => { - cy.get('body').trigger('keydown', { key }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - cy.get('[data-test=ModalOnboarding]').then(element => { - const leftEdgeX = element.offsetParent().offset()?.left as number; - const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; - - [ - { clientX: rightEdgeX - 25, el: 1 }, - { clientX: rightEdgeX - 10, el: 2 }, - { clientX: rightEdgeX - 5, el: 3 }, - // rightEdgeX === browser right frame - { clientX: rightEdgeX - 1, el: 3 }, - { clientX: leftEdgeX + 25, el: 2 }, - { clientX: leftEdgeX + 10, el: 1 }, - { clientX: leftEdgeX + 5, el: 0 }, - { clientX: leftEdgeX, el: 0 }, - ].forEach(({ clientX, el }) => { - cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); - }); -}; - -const checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - cy.get('[data-test=ModalOnboarding]').then(element => { - const leftEdgeX = element.offsetParent().offset()?.left as number; - const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; - - [ - { clientX: rightEdgeX - 25, el: 1 }, - { clientX: rightEdgeX - 26, el: 1 }, - { clientX: leftEdgeX + 26, el: 1 }, - { clientX: leftEdgeX + 25, el: 0 }, - ].forEach(({ clientX, el }) => { - cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); - }); -}; - -const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 3, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 3, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { - cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { - touches: [{ clientX: touchStartClientX }], - }); - cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { - touches: [{ clientX: touchMoveClientX }], - }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 4, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 + 4, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { - cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { - touches: [{ clientX: touchStartClientX }], - }); - cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { - touches: [{ clientX: touchMoveClientX }], - }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - connectWallet(true); - }); - - it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < stepsDecisionWindowOpen.length - 1; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i); - } - - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(stepsDecisionWindowOpen.length - 1) - .click(); - cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); - }); - - it('user is able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); - }); - - it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').check().should('be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders only once when "Always show Allocate onboarding" option is not checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').should('not.be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - - it('user can change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(true); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); - }); - - it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - connectWallet(true, false, true); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - }); -}); - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - cy.intercept( - { - method: 'POST', - url: '/user/*/tos', - }, - { body: { accepted: true }, statusCode: 200 }, - ); - connectWallet(false); - }); - - it('onboarding should have one more step (TOS)', () => { - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]').should( - 'have.length', - stepsDecisionWindowOpen.length + 1, - ); - }); - - it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < stepsDecisionWindowOpen.length; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); - } - }); - - it('user is not able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders every time page is refreshed', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('user cannot change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(false); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); - }); - - it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); - }); - - it('TOS acceptance changes onboarding step to next step', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - }); - - it('TOS acceptance allows the user to close the modal by clicking button in the top-right', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - }); -}); diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json index 0dc84b8343..3a45fd5db7 100644 --- a/client/cypress/tsconfig.json +++ b/client/cypress/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "sourceMap": true, + "inlineSourceMap": true, "allowJs": true, "baseUrl": "../", "types": [ diff --git a/client/tsconfig.json b/client/tsconfig.json index e236c79f26..2f0ebcdfac 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -21,6 +21,7 @@ "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, + "inlineSourceMap": true, "strict": true, "target": "es2022" }, From 8797a75096dc65135e67c07bd861188a4e5ed24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 01:16:27 +0100 Subject: [PATCH 14/35] test: inlineSourcemaps only --- client/cypress/tsconfig.json | 1 - client/tsconfig.json | 1 - 2 files changed, 2 deletions(-) diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json index 3a45fd5db7..0433e51155 100644 --- a/client/cypress/tsconfig.json +++ b/client/cypress/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "sourceMap": true, "inlineSourceMap": true, "allowJs": true, "baseUrl": "../", diff --git a/client/tsconfig.json b/client/tsconfig.json index 2f0ebcdfac..d874acda1c 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -20,7 +20,6 @@ "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, - "sourceMap": true, "inlineSourceMap": true, "strict": true, "target": "es2022" From cd674252d7660f9b6221fbc3fb228e8c96732be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 10:01:28 +0100 Subject: [PATCH 15/35] test: test trigger --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3be83aacd6..175337fc0f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,7 @@ stages: - application - env_test - status - - cleanup +# - cleanup default: tags: From d426d4e55a4a6a795d520e859f473ea883ff0ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 10:01:35 +0100 Subject: [PATCH 16/35] test: test trigger --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 175337fc0f..3be83aacd6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,7 @@ stages: - application - env_test - status -# - cleanup + - cleanup default: tags: From 5a89c220247ff89724d811e6beb36c2fac905459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 12:29:55 +0100 Subject: [PATCH 17/35] test: sourcemaps for the client --- client/cypress/tsconfig.json | 2 +- client/tsconfig.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json index 0433e51155..c72fcd2847 100644 --- a/client/cypress/tsconfig.json +++ b/client/cypress/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "inlineSourceMap": true, + "sourceMap": false, "allowJs": true, "baseUrl": "../", "types": [ diff --git a/client/tsconfig.json b/client/tsconfig.json index d874acda1c..2f0ebcdfac 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -20,6 +20,7 @@ "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, + "sourceMap": true, "inlineSourceMap": true, "strict": true, "target": "es2022" From eca0bff5c9822785afe44b8d27a9d7d34cef72a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 12:43:26 +0100 Subject: [PATCH 18/35] test: sourcemaps for the client --- client/cypress/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json index c72fcd2847..506b2a4570 100644 --- a/client/cypress/tsconfig.json +++ b/client/cypress/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "sourceMap": false, + "inlineSourceMap": false, "allowJs": true, "baseUrl": "../", "types": [ From 7fd46393a7e0f74cdda2696170aa8030fa809f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 12:52:10 +0100 Subject: [PATCH 19/35] test: sourcemaps for the client --- client/cypress/tsconfig.json | 3 +-- client/tsconfig.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json index 506b2a4570..0433e51155 100644 --- a/client/cypress/tsconfig.json +++ b/client/cypress/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "sourceMap": false, - "inlineSourceMap": false, + "inlineSourceMap": true, "allowJs": true, "baseUrl": "../", "types": [ diff --git a/client/tsconfig.json b/client/tsconfig.json index 2f0ebcdfac..d874acda1c 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -20,7 +20,6 @@ "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, - "sourceMap": true, "inlineSourceMap": true, "strict": true, "target": "es2022" From ef02ac19bd536bf8a21f23f6a8266bc5037a121f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 14:13:51 +0100 Subject: [PATCH 20/35] test: sourcemaps removed, DoubleValue props w/ checks to ensure CPS returned values, onboarding CY suite reintroduced --- client/cypress/e2e/onboarding.cy.ts | 381 ++++++++++++++++++ client/cypress/tsconfig.json | 1 - .../src/components/core/DoubleValue/types.ts | 2 +- .../src/utils/getValueFiatToDisplay.test.ts | 8 - client/src/utils/getValueFiatToDisplay.ts | 6 + client/tsconfig.json | 1 - client/vite.config.js | 3 - 7 files changed, 388 insertions(+), 14 deletions(-) create mode 100644 client/cypress/e2e/onboarding.cy.ts diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts new file mode 100644 index 0000000000..780979f527 --- /dev/null +++ b/client/cypress/e2e/onboarding.cy.ts @@ -0,0 +1,381 @@ +import { visitWithLoader, navigateWithCheck } from 'cypress/utils/e2e'; +import viewports from 'cypress/utils/viewports'; +import { stepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; + +import Chainable = Cypress.Chainable; + +const connectWallet = ( + isTOSAccepted: boolean, + shouldVisit = true, + shouldReload = false, +): Chainable => { + cy.intercept('GET', '/user/*/tos', { body: { accepted: isTOSAccepted } }); + cy.disconnectMetamaskWalletFromAllDapps(); + if (shouldVisit) { + visitWithLoader(ROOT.absolute, ROOT_ROUTES.proposals.absolute); + } + if (shouldReload) { + cy.reload(); + } + cy.get('[data-test=MainLayout__Button--connect]').click(); + cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); + cy.switchToMetamaskNotification(); + return cy.acceptMetamaskAccess(); +}; + +const beforeSetup = () => { + cy.clearLocalStorage(); + cy.setupMetamask(); + cy.activateShowTestnetNetworksInMetamask(); + cy.changeMetamaskNetwork('sepolia'); + window.innerWidth = Cypress.config().viewportWidth; + window.innerHeight = Cypress.config().viewportHeight; +}; + +const checkCurrentElement = (el: number, isCurrent: boolean): Chainable => { + return cy + .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(el) + .invoke('attr', 'data-iscurrent') + .should('eq', `${isCurrent}`); +}; + +const checkProgressStepperSlimIsCurrentAndClickNext = (index, isCurrent = true): Chainable => { + checkCurrentElement(index - 1, isCurrent); + return cy + .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(index) + .click({ force: true }); +}; + +const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + [ + { el: 1, key: 'ArrowRight' }, + { el: 2, key: 'ArrowRight' }, + { el: 3, key: 'ArrowRight' }, + { el: 3, key: 'ArrowRight' }, + { el: 2, key: 'ArrowLeft' }, + { el: 1, key: 'ArrowLeft' }, + { el: 0, key: 'ArrowLeft' }, + { el: 0, key: 'ArrowLeft' }, + ].forEach(({ key, el }) => { + cy.get('body').trigger('keydown', { key }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + cy.get('[data-test=ModalOnboarding]').then(element => { + const leftEdgeX = element.offsetParent().offset()?.left as number; + const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; + + [ + { clientX: rightEdgeX - 25, el: 1 }, + { clientX: rightEdgeX - 10, el: 2 }, + { clientX: rightEdgeX - 5, el: 3 }, + // rightEdgeX === browser right frame + { clientX: rightEdgeX - 1, el: 3 }, + { clientX: leftEdgeX + 25, el: 2 }, + { clientX: leftEdgeX + 10, el: 1 }, + { clientX: leftEdgeX + 5, el: 0 }, + { clientX: leftEdgeX, el: 0 }, + ].forEach(({ clientX, el }) => { + cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); + }); +}; + +const checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + cy.get('[data-test=ModalOnboarding]').then(element => { + const leftEdgeX = element.offsetParent().offset()?.left as number; + const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; + + [ + { clientX: rightEdgeX - 25, el: 1 }, + { clientX: rightEdgeX - 26, el: 1 }, + { clientX: leftEdgeX + 26, el: 1 }, + { clientX: leftEdgeX + 25, el: 0 }, + ].forEach(({ clientX, el }) => { + cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); + }); +}; + +const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + [ + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 3, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 3, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { + cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { + touches: [{ clientX: touchStartClientX }], + }); + cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { + touches: [{ clientX: touchMoveClientX }], + }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + [ + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 4, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 + 4, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { + cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { + touches: [{ clientX: touchStartClientX }], + }); + cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { + touches: [{ clientX: touchMoveClientX }], + }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { + describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { + before(() => { + beforeSetup(); + }); + + beforeEach(() => { + connectWallet(true); + }); + + it('user is able to click through entire onboarding flow', () => { + for (let i = 1; i < stepsDecisionWindowOpen.length - 1; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i); + } + + cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(stepsDecisionWindowOpen.length - 1) + .click(); + cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); + }); + + it('user is able to close the modal by clicking button in the top-right', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); + }); + + it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + navigateWithCheck(ROOT_ROUTES.settings.absolute); + cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').check().should('be.checked'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('renders only once when "Always show Allocate onboarding" option is not checked', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + navigateWithCheck(ROOT_ROUTES.settings.absolute); + cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').should('not.be.checked'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + + it('user can change steps with arrow keys (left, right)', () => { + checkChangeStepsWithArrowKeys(true); + }); + + it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); + }); + + it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); + }); + + it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); + }); + + it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + connectWallet(true, false, true); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + }); +}); + +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { + describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { + before(() => { + beforeSetup(); + }); + + beforeEach(() => { + cy.intercept( + { + method: 'POST', + url: '/user/*/tos', + }, + { body: { accepted: true }, statusCode: 200 }, + ); + connectWallet(false); + }); + + it('onboarding should have one more step (TOS)', () => { + cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]').should( + 'have.length', + stepsDecisionWindowOpen.length + 1, + ); + }); + + it('user is not able to click through entire onboarding flow', () => { + for (let i = 1; i < stepsDecisionWindowOpen.length; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); + } + }); + + it('user is not able to close the modal by clicking button in the top-right', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('renders every time page is refreshed', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('user cannot change steps with arrow keys (left, right)', () => { + checkChangeStepsWithArrowKeys(false); + }); + + it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); + }); + + it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); + }); + + it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); + }); + + it('TOS acceptance changes onboarding step to next step', () => { + checkCurrentElement(0, true); + cy.get('[data-test=TOS_InputCheckbox]').check(); + cy.switchToMetamaskNotification(); + cy.confirmMetamaskSignatureRequest(); + checkCurrentElement(1, true); + }); + + it('TOS acceptance allows the user to close the modal by clicking button in the top-right', () => { + checkCurrentElement(0, true); + cy.get('[data-test=TOS_InputCheckbox]').check(); + cy.switchToMetamaskNotification(); + cy.confirmMetamaskSignatureRequest(); + checkCurrentElement(1, true); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + }); +}); diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json index 0433e51155..4386f04bc7 100644 --- a/client/cypress/tsconfig.json +++ b/client/cypress/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "inlineSourceMap": true, "allowJs": true, "baseUrl": "../", "types": [ diff --git a/client/src/components/core/DoubleValue/types.ts b/client/src/components/core/DoubleValue/types.ts index 3818e7190d..d39bb6e2cb 100644 --- a/client/src/components/core/DoubleValue/types.ts +++ b/client/src/components/core/DoubleValue/types.ts @@ -8,7 +8,7 @@ export type DoubleValueVariant = (typeof DOUBLE_VALUE_VARIANTS)[number]; export default interface DoubleValueProps { className?: string; coinPricesServerDowntimeText?: 'Conversion offline' | '...'; - cryptoCurrency?: CryptoCurrency; + cryptoCurrency: CryptoCurrency; dataTest?: string; isDisabled?: boolean; isError?: boolean; diff --git a/client/src/utils/getValueFiatToDisplay.test.ts b/client/src/utils/getValueFiatToDisplay.test.ts index a2512ec30c..c7c235dfd6 100644 --- a/client/src/utils/getValueFiatToDisplay.test.ts +++ b/client/src/utils/getValueFiatToDisplay.test.ts @@ -44,14 +44,6 @@ describe('getValueFiatToDisplay', () => { }); it('should return 0.00 when there is a parameter missing', () => { - expect( - // @ts-expect-error error here is caused by lack of typing for defaultProps. - getValueFiatToDisplay({ - ...defaultProps, - cryptoCurrency: undefined, - }), - ).toEqual('$0.00'); - expect( // @ts-expect-error error here is caused by lack of typing for defaultProps. getValueFiatToDisplay({ diff --git a/client/src/utils/getValueFiatToDisplay.ts b/client/src/utils/getValueFiatToDisplay.ts index 34dc3d6d48..245eaa078e 100644 --- a/client/src/utils/getValueFiatToDisplay.ts +++ b/client/src/utils/getValueFiatToDisplay.ts @@ -34,11 +34,17 @@ export default function getValueFiatToDisplay({ * We need to ensure particular cryptoValues[cryptoCurrency][displayCurrency] is already fetched. * Otherwise, Cypress tests failed when changing the displayCurrency * and requesting to see its fiat value immediately. + * + * We need to ensure cryptoValues[cryptoCurrency] is defined too. + * For the reason unknown coin-prices-server sometimes returns data (cryptoValues defined), + * yet cryptoValues[cryptoCurrency] is unknown, resulting in a crash. + * This happens in E2E runs only. */ // if ( !cryptoCurrency || !cryptoValues || + !cryptoValues[cryptoCurrency] || !cryptoValues[cryptoCurrency][displayCurrency] || !valueCrypto ) { diff --git a/client/tsconfig.json b/client/tsconfig.json index d874acda1c..54c51f2598 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -20,7 +20,6 @@ "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, - "inlineSourceMap": true, "strict": true, "target": "es2022" }, diff --git a/client/vite.config.js b/client/vite.config.js index 6a770a2873..ffce81d710 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -55,9 +55,6 @@ export default defineConfig(({ mode }) => { return { base, - build: { - sourcemap: 'inline', - }, css: { modules: { generateScopedName: localIdentName, From a3a8289c3a5076fe8af1d1536114debcab479588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 7 Nov 2023 15:26:38 +0100 Subject: [PATCH 21/35] test: CY onboarding removed for now, i18n for onboarding fixed --- client/cypress/e2e/onboarding.cy.ts | 381 ------------------ .../helpers/useOnboardingSteps/index.tsx | 8 +- client/src/locales/en/translation.json | 20 +- 3 files changed, 8 insertions(+), 401 deletions(-) delete mode 100644 client/cypress/e2e/onboarding.cy.ts diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts deleted file mode 100644 index 780979f527..0000000000 --- a/client/cypress/e2e/onboarding.cy.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { visitWithLoader, navigateWithCheck } from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { stepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; -import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -import Chainable = Cypress.Chainable; - -const connectWallet = ( - isTOSAccepted: boolean, - shouldVisit = true, - shouldReload = false, -): Chainable => { - cy.intercept('GET', '/user/*/tos', { body: { accepted: isTOSAccepted } }); - cy.disconnectMetamaskWalletFromAllDapps(); - if (shouldVisit) { - visitWithLoader(ROOT.absolute, ROOT_ROUTES.proposals.absolute); - } - if (shouldReload) { - cy.reload(); - } - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); - cy.switchToMetamaskNotification(); - return cy.acceptMetamaskAccess(); -}; - -const beforeSetup = () => { - cy.clearLocalStorage(); - cy.setupMetamask(); - cy.activateShowTestnetNetworksInMetamask(); - cy.changeMetamaskNetwork('sepolia'); - window.innerWidth = Cypress.config().viewportWidth; - window.innerHeight = Cypress.config().viewportHeight; -}; - -const checkCurrentElement = (el: number, isCurrent: boolean): Chainable => { - return cy - .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(el) - .invoke('attr', 'data-iscurrent') - .should('eq', `${isCurrent}`); -}; - -const checkProgressStepperSlimIsCurrentAndClickNext = (index, isCurrent = true): Chainable => { - checkCurrentElement(index - 1, isCurrent); - return cy - .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(index) - .click({ force: true }); -}; - -const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { el: 1, key: 'ArrowRight' }, - { el: 2, key: 'ArrowRight' }, - { el: 3, key: 'ArrowRight' }, - { el: 3, key: 'ArrowRight' }, - { el: 2, key: 'ArrowLeft' }, - { el: 1, key: 'ArrowLeft' }, - { el: 0, key: 'ArrowLeft' }, - { el: 0, key: 'ArrowLeft' }, - ].forEach(({ key, el }) => { - cy.get('body').trigger('keydown', { key }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - cy.get('[data-test=ModalOnboarding]').then(element => { - const leftEdgeX = element.offsetParent().offset()?.left as number; - const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; - - [ - { clientX: rightEdgeX - 25, el: 1 }, - { clientX: rightEdgeX - 10, el: 2 }, - { clientX: rightEdgeX - 5, el: 3 }, - // rightEdgeX === browser right frame - { clientX: rightEdgeX - 1, el: 3 }, - { clientX: leftEdgeX + 25, el: 2 }, - { clientX: leftEdgeX + 10, el: 1 }, - { clientX: leftEdgeX + 5, el: 0 }, - { clientX: leftEdgeX, el: 0 }, - ].forEach(({ clientX, el }) => { - cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); - }); -}; - -const checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - cy.get('[data-test=ModalOnboarding]').then(element => { - const leftEdgeX = element.offsetParent().offset()?.left as number; - const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; - - [ - { clientX: rightEdgeX - 25, el: 1 }, - { clientX: rightEdgeX - 26, el: 1 }, - { clientX: leftEdgeX + 26, el: 1 }, - { clientX: leftEdgeX + 25, el: 0 }, - ].forEach(({ clientX, el }) => { - cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); - }); -}; - -const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 3, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 3, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { - cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { - touches: [{ clientX: touchStartClientX }], - }); - cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { - touches: [{ clientX: touchMoveClientX }], - }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 4, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 + 4, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { - cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { - touches: [{ clientX: touchStartClientX }], - }); - cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { - touches: [{ clientX: touchMoveClientX }], - }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - connectWallet(true); - }); - - it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < stepsDecisionWindowOpen.length - 1; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i); - } - - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(stepsDecisionWindowOpen.length - 1) - .click(); - cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); - }); - - it('user is able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); - }); - - it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').check().should('be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders only once when "Always show Allocate onboarding" option is not checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').should('not.be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - - it('user can change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(true); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); - }); - - it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - connectWallet(true, false, true); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - }); -}); - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - cy.intercept( - { - method: 'POST', - url: '/user/*/tos', - }, - { body: { accepted: true }, statusCode: 200 }, - ); - connectWallet(false); - }); - - it('onboarding should have one more step (TOS)', () => { - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]').should( - 'have.length', - stepsDecisionWindowOpen.length + 1, - ); - }); - - it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < stepsDecisionWindowOpen.length; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); - } - }); - - it('user is not able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders every time page is refreshed', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('user cannot change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(false); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); - }); - - it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); - }); - - it('TOS acceptance changes onboarding step to next step', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - }); - - it('TOS acceptance allows the user to close the modal by clicking button in the top-right', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - }); -}); diff --git a/client/src/hooks/helpers/useOnboardingSteps/index.tsx b/client/src/hooks/helpers/useOnboardingSteps/index.tsx index b71b64e503..98f88b5baa 100644 --- a/client/src/hooks/helpers/useOnboardingSteps/index.tsx +++ b/client/src/hooks/helpers/useOnboardingSteps/index.tsx @@ -37,11 +37,11 @@ const useOnboardingSteps = ( ...(isUserTOSAcceptedInitial === false ? [ { - header: i18n.t('views.onboarding.steps.usingTheApp.header'), + header: i18n.t('views.onboarding.stepsCommon.usingTheApp.header'), image: '/images/onboarding/octant.webp', text: ( -
{i18n.t('views.onboarding.steps.usingTheApp.text')}
+
{i18n.t('views.onboarding.stepsCommon.usingTheApp.text')}
), @@ -51,13 +51,13 @@ const useOnboardingSteps = ( ...(isUserEligibleToClaimGlm && glmClaimCheck?.value ? [ { - header: i18n.t('views.onboarding.steps.claimGlm.header'), + header: i18n.t('views.onboarding.stepsCommon.claimGlm.header'), image: 'images/tip-withdraw.webp', imageClassName: styles.claimGlm, text: ( Earn view, and take a look at the projects you can donate to in the <0>Projects view.

If you already have GLM locked, you’ll have some rewards to use during E1 Allocation window, which runs from 19 Oct to 2 Nov." @@ -371,21 +373,7 @@ "text": "Once you have rewards, use the slider in the <0>Allocate view to divide your rewards into donations to projects or personal allocation.

Change your choices at any time during the Allocation window, just unlock the slider, make some edits and reconfirm in your wallet." } }, - "stepsDecisionWindowClosed": { - "usingTheApp": { - "header": "Using the app", - "text": "Before we can go any further, please read the terms of service and click the checkbox below to agree with them." - }, - "claimGlm": { - "header": "Claim your GLM", - "text": "As you hold an Epoch Zero Token and you participated in the Snapshot vote, you are eligible to claim {{value}} GLM. Click the button below to withdraw to your wallet.", - "buttonLabel": { - "withdraw": "Withdraw GLM", - "withdrawn": "GLM claimed" - }, - "signMessage": "Claim {{value}} GLMs" - }, "welcomeToOctant": { "header": "Welcome to Octant Epoch 2", "text": "To get started, lock some GLM in the <0>Earn view, and see how the Epoch 1 projects performed in the Epoch 1 archive.

If you made personal allocations in the previous epoch, your ETH will be available to withdraw in the <0>Earn view." From be5ce58078c53421782c3ba9b1f44f07d49d4eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Tue, 7 Nov 2023 16:32:52 +0100 Subject: [PATCH 22/35] Gitlab CI typos fix --- .gitlab-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3be83aacd6..ae312dd4b3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -657,18 +657,18 @@ Deploy Release Candidate app: cd $GIT_DIR - echo "(debug) before update ==="" + echo '(debug) before update ===' cat mainnet/octant-image.values.yaml cat testnet/octant-image.values.yaml - echo "(end debug) ==="" + echo '(end debug) ===' yq -i -e ".[].value.value = \"$IMAGE_TAG\"" mainnet/octant-image.values.yaml yq -i -e ".[].value.value = \"$IMAGE_TAG\"" testnet/octant-image.values.yaml - echo "(debug) after update ==="" + echo '(debug) after update ===' cat mainnet/octant-image.values.yaml cat testnet/octant-image.values.yaml - echo "(end debug) ==="" + echo '(end debug) ===' git add mainnet/octant-image.values.yaml git add testnet/octant-image.values.yaml From 7e0b1573759e40e122cc578fc41bfc811478be9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 8 Nov 2023 09:07:19 +0100 Subject: [PATCH 23/35] oct-1130 disabled copy popup on input select - safari mobile --- client/src/components/core/InputText/InputText.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/core/InputText/InputText.module.scss b/client/src/components/core/InputText/InputText.module.scss index a75fb193aa..db034c7381 100644 --- a/client/src/components/core/InputText/InputText.module.scss +++ b/client/src/components/core/InputText/InputText.module.scss @@ -36,6 +36,7 @@ width: 100%; height: 100%; border: 0; + -webkit-user-select: none; &:focus-visible { outline: 0; From 875332038e5c21b5b7a9a8e8dbcbc59941f093b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 8 Nov 2023 09:27:19 +0100 Subject: [PATCH 24/35] oct-1130 user-select none comment --- client/src/components/core/InputText/InputText.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/core/InputText/InputText.module.scss b/client/src/components/core/InputText/InputText.module.scss index db034c7381..f4df4d1bdf 100644 --- a/client/src/components/core/InputText/InputText.module.scss +++ b/client/src/components/core/InputText/InputText.module.scss @@ -36,6 +36,7 @@ width: 100%; height: 100%; border: 0; + // Hides copy/paste popup on safari mobile when an input value is selected -webkit-user-select: none; &:focus-visible { From 715bdc08b6d028788b8c66d1081172bf23de7395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 8 Nov 2023 13:34:31 +0100 Subject: [PATCH 25/35] oct-1105 proposal archived state update (rewards) --- client/cypress/e2e/proposal.cy.ts | 36 +---- .../core/ProgressBar/ProgressBar.module.scss | 4 + .../src/components/core/ProgressBar/types.ts | 2 +- .../ButtonAddToAllocate.module.scss | 19 ++- .../ButtonAddToAllocate.tsx | 16 +- .../dedicated/DonorsHeader/DonorsHeader.tsx | 4 +- .../ProposalRewards.module.scss | 83 ++++------- .../ProposalRewards/ProposalRewards.tsx | 138 +++++++++++------- .../dedicated/ProposalRewards/types.ts | 5 +- .../ProposalsList/ProposalsList.module.scss | 2 +- .../dedicated/ProposalsList/ProposalsList.tsx | 33 +++-- .../ProposalsListItem/ProposalsListItem.tsx | 26 ++-- client/src/gql/gql.ts | 8 +- client/src/gql/graphql.ts | 129 +++++++++++++++- ...chsEndTime.ts => useEpochsStartEndTime.ts} | 19 ++- client/src/locales/en/translation.json | 14 +- .../ProposalView/ProposalView.module.scss | 19 +-- .../src/views/ProposalView/ProposalView.tsx | 23 +-- 18 files changed, 350 insertions(+), 230 deletions(-) rename client/src/hooks/subgraph/{useEpochsEndTime.ts => useEpochsStartEndTime.ts} (59%) diff --git a/client/cypress/e2e/proposal.cy.ts b/client/cypress/e2e/proposal.cy.ts index fcf3809960..9a39997786 100644 --- a/client/cypress/e2e/proposal.cy.ts +++ b/client/cypress/e2e/proposal.cy.ts @@ -7,24 +7,13 @@ import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; -const getButtonAddToAllocate = (currentEpoch: number, isDesktop: boolean): Chainable => { +const getButtonAddToAllocate = (): Chainable => { const proposalView = cy.get('[data-test=ProposalView__proposal').first(); - switch (currentEpoch) { - case 1: - return proposalView.find('[data-test=ProposalView__proposal__ButtonAddToAllocate--primary]'); - default: - return proposalView.find( - `[data-test=${ - isDesktop - ? 'ProposalView__proposal__ButtonAddToAllocate--secondary' - : 'ProposalView__proposal__ButtonAddToAllocate--primary' - }]`, - ); - } + return proposalView.find('[data-test=ProposalView__proposal__ButtonAddToAllocate]'); }; -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { describe(`proposal: ${device}`, { viewportHeight, viewportWidth }, () => { let proposalNames: string[] = []; @@ -55,12 +44,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes const proposalView = cy.get('[data-test=ProposalView__proposal').first(); proposalView.get('[data-test=ProposalView__proposal__Img]').should('be.visible'); proposalView.get('[data-test=ProposalView__proposal__name]').should('be.visible'); - - cy.window().then(window => { - // @ts-expect-error missing typing for client window elements. - const currentEpoch = Number(window.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch)); - getButtonAddToAllocate(currentEpoch, isDesktop).should('be.visible'); - }); + getButtonAddToAllocate().should('be.visible'); proposalView.get('[data-test=ProposalView__proposal__Button]').should('be.visible'); proposalView.get('[data-test=ProposalView__proposal__Description]').should('be.visible'); @@ -94,19 +78,11 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes it('entering proposal view allows to add it to allocation and remove, triggering change of the icon, change of the number in navbar', () => { cy.get('[data-test^=ProposalsView__ProposalsListItem').first().click(); - cy.window().then(window => { - // @ts-expect-error missing typing for client window elements. - const currentEpoch = Number(window.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch)); - getButtonAddToAllocate(currentEpoch, isDesktop).click(); - }); + getButtonAddToAllocate().click(); // cy.get('@buttonAddToAllocate').click(); cy.get('[data-test=Navbar__numberOfAllocations]').contains(1); - cy.window().then(window => { - // @ts-expect-error missing typing for client window elements. - const currentEpoch = Number(window.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch)); - getButtonAddToAllocate(currentEpoch, isDesktop).click(); - }); + getButtonAddToAllocate().click(); cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); }); diff --git a/client/src/components/core/ProgressBar/ProgressBar.module.scss b/client/src/components/core/ProgressBar/ProgressBar.module.scss index f60b293784..f900cdbe19 100644 --- a/client/src/components/core/ProgressBar/ProgressBar.module.scss +++ b/client/src/components/core/ProgressBar/ProgressBar.module.scss @@ -21,6 +21,10 @@ overflow: hidden; &.variant-- { + &ultraThin { + height: 0.2rem; + } + &thin { height: 0.4rem; } diff --git a/client/src/components/core/ProgressBar/types.ts b/client/src/components/core/ProgressBar/types.ts index 4165aee3b8..b2de08c9e6 100644 --- a/client/src/components/core/ProgressBar/types.ts +++ b/client/src/components/core/ProgressBar/types.ts @@ -7,5 +7,5 @@ export default interface ProgressBarProps { labelLeft?: string; labelRight?: string; progressPercentage: number; - variant?: 'thin' | 'normal'; + variant?: 'ultraThin' | 'thin' | 'normal'; } diff --git a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.module.scss b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.module.scss index 0cb3abb2e4..c31184b4b5 100644 --- a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.module.scss +++ b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.module.scss @@ -13,10 +13,21 @@ } } - &.isArchivedProposal.isAllocatedTo { - svg circle { - stroke: $color-octant-grey5; - fill: $color-octant-grey5; + &.isArchivedProposal { + &:hover { + background: transparent; + cursor: default; + } + + svg path { + stroke: $color-octant-grey1; + } + + &.isAllocatedTo { + svg circle { + stroke: $color-octant-grey5; + fill: $color-octant-grey5; + } } } } diff --git a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx index 7a792b74d0..f4f1de6b69 100644 --- a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx +++ b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx @@ -1,8 +1,10 @@ import cx from 'classnames'; import React, { FC, useRef, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import Button from 'components/core/Button/Button'; import Svg from 'components/core/Svg/Svg'; +import Tooltip from 'components/core/Tooltip/Tooltip'; import { IS_INITIAL_LOAD_DONE } from 'constants/dataAttributes'; import { checkMark, heart } from 'svg/misc'; @@ -17,6 +19,9 @@ const ButtonAddToAllocate: FC = ({ isAllocatedTo, isArchivedProposal, }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.dedicated.buttonAddToAllocate', + }); const ref = useRef(null); useEffect(() => { @@ -37,7 +42,16 @@ const ButtonAddToAllocate: FC = ({ [IS_INITIAL_LOAD_DONE]: 'false', }} dataTest={dataTest} - Icon={} + Icon={ + isArchivedProposal && isAllocatedTo ? ( + + + + ) : ( + + ) + } + isDisabled={isArchivedProposal} onClick={onClick} variant="iconOnly" /> diff --git a/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx b/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx index fe63d2b792..53410908ed 100644 --- a/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx +++ b/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx @@ -12,13 +12,13 @@ const DonorsHeader: FC = ({ dataTest = 'DonorsHeader', className, }) => { - const { t } = useTranslation('translation', { keyPrefix: 'components.dedicated.donorsHeader' }); + const { i18n } = useTranslation('translation'); const { data: proposalDonors, isFetching } = useProposalDonors(proposalAddress); return (
- {t('donors')}{' '} + {i18n.t('common.donors')}{' '}
{isFetching ? '--' : proposalDonors?.length}
diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss b/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss index 5dd49606ae..4113bb23e1 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss @@ -5,47 +5,25 @@ align-self: stretch; justify-content: space-between; - .separator { - margin: 0 0 1.2rem 0; - } - - .line { + .divider { height: 0.2rem; width: 100%; content: ''; background: $color-octant-grey3; } - .values { + .sections { + margin-top: 1.2rem; display: flex; justify-content: space-between; - align-items: center; - color: $color-octant-grey5; - - .value { - display: flex; - flex-direction: column; - font-weight: $font-weight-bold; - - &:first-child { - align-items: flex-start; - - .number { - color: $color-black; - &.isArchivedProposal { - color: $color-octant-grey5; - } - } - } - - &:last-child { - align-items: flex-end; - } + &.isArchivedProposal { + margin-top: 1.6rem; + } - &.isHidden { - visibility: hidden; - } + .section { + font-weight: $font-weight-bold; + color: $color-octant-grey5; .label { font-size: $font-size-12; @@ -54,38 +32,31 @@ align-items: center; } - .number { - font-size: $font-size-16; + &.leftSection { + text-align: left; - &.isBelowCutOff { - color: $color-octant-orange; + .label { + justify-content: flex-start; } - &.isArchivedProposal { - color: $color-octant-grey5; + .value { + &.greenValue { + color: $color-octant-green; + } + + &.redValue { + color: $color-octant-orange; + } } } - } - } - .percentage { - position: relative; - color: $color-octant-orange; - margin: 0 0 0 1rem; - padding: 0 0 0 1rem; + &.rightSection { + text-align: right; - &:before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 0.2rem; - height: 100%; - background: $color-octant-grey1; + .label { + justify-content: flex-end; + } + } } } - - .thresholdDataUnavailable { - font-size: $font-size-12; - } } diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx index c393617d08..93da1960ab 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx @@ -1,7 +1,7 @@ import cx from 'classnames'; import { BigNumber } from 'ethers'; import { formatUnits } from 'ethers/lib/utils'; -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import ProgressBar from 'components/core/ProgressBar/ProgressBar'; @@ -17,10 +17,9 @@ import { getProgressPercentage } from './utils'; const ProposalRewards: FC = ({ address, - canFoundedAtHide = true, className, - MiddleElement, epoch, + isProposalView, }) => { const { t, i18n } = useTranslation('translation', { keyPrefix: 'components.dedicated.proposalRewards', @@ -42,19 +41,11 @@ const ProposalRewards: FC = ({ const isDonationAboveThreshold = useIsDonationAboveThreshold(address, epoch); - const isFundedAtHidden = - proposalDonorsRewardsSum?.isZero() || (canFoundedAtHide && isDonationAboveThreshold); - const totalValueOfAllocationsToDisplay = getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', valueCrypto: proposalMatchedProposalRewards?.sum, }); - const cutOffValueToDisplay = getValueCryptoToDisplay({ - cryptoCurrency: 'ethereum', - valueCrypto: proposalRewardsThreshold, - }); - const proposalDonorsRewardsSumToDisplay = getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', valueCrypto: proposalDonorsRewardsSum, @@ -65,58 +56,97 @@ const ProposalRewards: FC = ({ proposalRewardsThreshold !== undefined && proposalDonorsRewardsSum !== undefined; + const leftSectionLabel = useMemo(() => { + if (isDonationAboveThreshold && !isArchivedProposal) { + return t('currentTotal'); + } + if (isDonationAboveThreshold && isArchivedProposal) { + return t('totalRaised'); + } + return t('totalDonated'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isArchivedProposal, isDonationAboveThreshold]); + + const rightSectionLabel = useMemo(() => { + if (isDonationAboveThreshold && isArchivedProposal && isProposalView) { + return t('fundedIn'); + } + if (isDonationAboveThreshold && isArchivedProposal) { + return i18n.t('common.donors'); + } + if (isArchivedProposal && isProposalView) { + return t('didNotReachThreshold'); + } + if (isArchivedProposal) { + return t('didNotReach'); + } + return t('fundedAt'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isArchivedProposal, isDonationAboveThreshold, isProposalView]); + + const rightSectionValueUseMemoDeps = [ + isArchivedProposal, + isDonationAboveThreshold, + isProposalView, + epoch, + proposalDonors?.length, + proposalRewardsThreshold?.toHexString(), + ]; + + const rightSectionValue = useMemo(() => { + if (isDonationAboveThreshold && isArchivedProposal && isProposalView) { + return t('epoch', { epoch }); + } + if (isDonationAboveThreshold && isArchivedProposal) { + return proposalDonors?.length; + } + return getValueCryptoToDisplay({ + cryptoCurrency: 'ethereum', + valueCrypto: proposalRewardsThreshold, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, rightSectionValueUseMemoDeps); + return (
-
- {showProgressBar ? ( - - ) : ( -
- )} -
-
- {proposalMatchedProposalRewards !== undefined || proposalDonors !== undefined ? ( -
- - {t(isDonationAboveThreshold ? 'totalRaised' : 'totalDonated')} - - - {isDonationAboveThreshold - ? totalValueOfAllocationsToDisplay - : proposalDonorsRewardsSumToDisplay} - + {showProgressBar ? ( + + ) : ( +
+ )} +
+
+
+ {leftSectionLabel}
- ) : (
- {i18n.t('common.thresholdDataUnavailable')} + {isDonationAboveThreshold + ? totalValueOfAllocationsToDisplay + : proposalDonorsRewardsSumToDisplay}
- )} - {MiddleElement} -
- {t(isArchivedProposal ? 'didNotReach' : 'fundedAt')} - - {cutOffValueToDisplay} -
+ {!(isDonationAboveThreshold && !isArchivedProposal) && ( +
+
{rightSectionLabel}
+
{rightSectionValue}
+
+ )}
); }; - export default ProposalRewards; diff --git a/client/src/components/dedicated/ProposalRewards/types.ts b/client/src/components/dedicated/ProposalRewards/types.ts index 9c5de9daad..eb556ab064 100644 --- a/client/src/components/dedicated/ProposalRewards/types.ts +++ b/client/src/components/dedicated/ProposalRewards/types.ts @@ -1,9 +1,6 @@ -import { ReactNode } from 'react'; - export default interface ProposalRewardsProps { - MiddleElement?: ReactNode; address: string; - canFoundedAtHide?: boolean; className?: string; epoch?: number; + isProposalView?: boolean; } diff --git a/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss b/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss index 1dd4da08d3..075a0f5094 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss +++ b/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss @@ -29,7 +29,7 @@ $elementMargin: 1.6rem; border-radius: $border-radius-16; margin: 1.6rem 0; - .epochArchiveEnded { + .epochDuration { color: $color-octant-grey5; font-size: $font-size-12; font-weight: $font-weight-medium; diff --git a/client/src/components/dedicated/ProposalsList/ProposalsList.tsx b/client/src/components/dedicated/ProposalsList/ProposalsList.tsx index 34aafd92c6..599e9bbfd7 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsList.tsx +++ b/client/src/components/dedicated/ProposalsList/ProposalsList.tsx @@ -1,12 +1,13 @@ -import { formatDistanceToNow } from 'date-fns'; +import { format, isSameYear } from 'date-fns'; import React, { FC, memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import ProposalsListItem from 'components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem'; import ProposalsListItemSkeleton from 'components/dedicated/ProposalsList/ProposalsListItemSkeleton/ProposalsListItemSkeleton'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; import useProposalsContract from 'hooks/queries/useProposalsContract'; import useProposalsIpfsWithRewards from 'hooks/queries/useProposalsIpfsWithRewards'; -import useEpochsEndTime from 'hooks/subgraph/useEpochsEndTime'; +import useEpochsStartEndTime from 'hooks/subgraph/useEpochsStartEndTime'; import styles from './ProposalsList.module.scss'; import ProposalsListProps from './types'; @@ -20,19 +21,31 @@ const ProposalsList: FC = ({ keyPrefix: 'components.dedicated.proposalsList', }); + const { isDesktop } = useMediaQuery(); + const { data: proposalsAddresses } = useProposalsContract(epoch); const { data: proposalsWithRewards } = useProposalsIpfsWithRewards(epoch); - const { data: epochsEndTime } = useEpochsEndTime(); + const { data: epochsStartEndTime } = useEpochsStartEndTime(); - const epochEndedLabel = useMemo(() => { - if (!epoch || !epochsEndTime) { + const epochDurationLabel = useMemo(() => { + if (!epoch || !epochsStartEndTime) { return ''; } - return formatDistanceToNow(new Date(parseInt(epochsEndTime[epoch - 1].toTs, 10) * 1000), { - addSuffix: true, - }); - }, [epoch, epochsEndTime]); + const epochData = epochsStartEndTime[epoch - 1]; + const epochStartTimestamp = parseInt(epochData.fromTs, 10) * 1000; + const epochEndTimestamp = parseInt(epochData.toTs, 10) * 1000; + + const isEpochEndedAtTheSameYear = isSameYear(epochStartTimestamp, epochEndTimestamp); + + const epochStartLabel = format( + epochStartTimestamp, + `${isDesktop ? 'dd MMMM' : 'MMM'} ${isEpochEndedAtTheSameYear ? '' : 'yyyy'}`, + ); + const epochEndLabel = format(epochEndTimestamp, `${isDesktop ? 'dd MMMM' : 'MMM'} yyyy`); + + return `${epochStartLabel} -> ${epochEndLabel}`; + }, [epoch, epochsStartEndTime, isDesktop]); return (
= ({ )}
{t('epochArchive', { epoch })} - {epochEndedLabel} + {epochDurationLabel}
)} diff --git a/client/src/components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem.tsx b/client/src/components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem.tsx index 647e817f70..dc1fc190c3 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem.tsx +++ b/client/src/components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem.tsx @@ -81,20 +81,18 @@ const ProposalsListItem: FC = ({ } src={`${ipfsGateway}${profileImageSmall}`} /> - {((isArchivedProposal && isAllocatedTo) || !isArchivedProposal) && ( - onAddRemoveFromAllocate(address)} - /> - )} + onAddRemoveFromAllocate(address)} + />
; unlocked?: Maybe; unlockeds: Array; + vaultMerkleRoot?: Maybe; + vaultMerkleRoots: Array; withdrawal?: Maybe; withdrawals: Array; }; @@ -498,6 +500,22 @@ export type QueryUnlockedsArgs = { where?: InputMaybe; }; +export type QueryVaultMerkleRootArgs = { + block?: InputMaybe; + id: Scalars['ID']['input']; + subgraphError?: _SubgraphErrorPolicy_; +}; + +export type QueryVaultMerkleRootsArgs = { + block?: InputMaybe; + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + subgraphError?: _SubgraphErrorPolicy_; + where?: InputMaybe; +}; + export type QueryWithdrawalArgs = { block?: InputMaybe; id: Scalars['ID']['input']; @@ -528,6 +546,8 @@ export type Subscription = { lockeds: Array; unlocked?: Maybe; unlockeds: Array; + vaultMerkleRoot?: Maybe; + vaultMerkleRoots: Array; withdrawal?: Maybe; withdrawals: Array; }; @@ -616,6 +636,22 @@ export type SubscriptionUnlockedsArgs = { where?: InputMaybe; }; +export type SubscriptionVaultMerkleRootArgs = { + block?: InputMaybe; + id: Scalars['ID']['input']; + subgraphError?: _SubgraphErrorPolicy_; +}; + +export type SubscriptionVaultMerkleRootsArgs = { + block?: InputMaybe; + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + subgraphError?: _SubgraphErrorPolicy_; + where?: InputMaybe; +}; + export type SubscriptionWithdrawalArgs = { block?: InputMaybe; id: Scalars['ID']['input']; @@ -722,6 +758,86 @@ export enum Unlocked_OrderBy { User = 'user', } +export type VaultMerkleRoot = { + __typename?: 'VaultMerkleRoot'; + blockNumber: Scalars['Int']['output']; + epoch: Scalars['Int']['output']; + id: Scalars['Bytes']['output']; + root: Scalars['Bytes']['output']; + timestamp: Scalars['Int']['output']; + transactionHash: Scalars['Bytes']['output']; +}; + +export type VaultMerkleRoot_Filter = { + /** Filter for the block changed event. */ + _change_block?: InputMaybe; + and?: InputMaybe>>; + blockNumber?: InputMaybe; + blockNumber_gt?: InputMaybe; + blockNumber_gte?: InputMaybe; + blockNumber_in?: InputMaybe>; + blockNumber_lt?: InputMaybe; + blockNumber_lte?: InputMaybe; + blockNumber_not?: InputMaybe; + blockNumber_not_in?: InputMaybe>; + epoch?: InputMaybe; + epoch_gt?: InputMaybe; + epoch_gte?: InputMaybe; + epoch_in?: InputMaybe>; + epoch_lt?: InputMaybe; + epoch_lte?: InputMaybe; + epoch_not?: InputMaybe; + epoch_not_in?: InputMaybe>; + id?: InputMaybe; + id_contains?: InputMaybe; + id_gt?: InputMaybe; + id_gte?: InputMaybe; + id_in?: InputMaybe>; + id_lt?: InputMaybe; + id_lte?: InputMaybe; + id_not?: InputMaybe; + id_not_contains?: InputMaybe; + id_not_in?: InputMaybe>; + or?: InputMaybe>>; + root?: InputMaybe; + root_contains?: InputMaybe; + root_gt?: InputMaybe; + root_gte?: InputMaybe; + root_in?: InputMaybe>; + root_lt?: InputMaybe; + root_lte?: InputMaybe; + root_not?: InputMaybe; + root_not_contains?: InputMaybe; + root_not_in?: InputMaybe>; + timestamp?: InputMaybe; + timestamp_gt?: InputMaybe; + timestamp_gte?: InputMaybe; + timestamp_in?: InputMaybe>; + timestamp_lt?: InputMaybe; + timestamp_lte?: InputMaybe; + timestamp_not?: InputMaybe; + timestamp_not_in?: InputMaybe>; + transactionHash?: InputMaybe; + transactionHash_contains?: InputMaybe; + transactionHash_gt?: InputMaybe; + transactionHash_gte?: InputMaybe; + transactionHash_in?: InputMaybe>; + transactionHash_lt?: InputMaybe; + transactionHash_lte?: InputMaybe; + transactionHash_not?: InputMaybe; + transactionHash_not_contains?: InputMaybe; + transactionHash_not_in?: InputMaybe>; +}; + +export enum VaultMerkleRoot_OrderBy { + BlockNumber = 'blockNumber', + Epoch = 'epoch', + Id = 'id', + Root = 'root', + Timestamp = 'timestamp', + TransactionHash = 'transactionHash', +} + export type Withdrawal = { __typename?: 'Withdrawal'; amount: Scalars['BigInt']['output']; @@ -862,13 +978,13 @@ export type GetEpochTimestampHappenedInQuery = { epoches: Array<{ __typename?: 'Epoch'; epoch: number }>; }; -export type GetEpochsEndTimeQueryVariables = Exact<{ +export type GetEpochsStartEndTimeQueryVariables = Exact<{ lastEpoch?: InputMaybe; }>; -export type GetEpochsEndTimeQuery = { +export type GetEpochsStartEndTimeQuery = { __typename?: 'Query'; - epoches: Array<{ __typename?: 'Epoch'; epoch: number; toTs: any }>; + epoches: Array<{ __typename?: 'Epoch'; epoch: number; toTs: any; fromTs: any }>; }; export type GetLargestLockedAmountQueryVariables = Exact<{ [key: string]: never }>; @@ -986,13 +1102,13 @@ export const GetEpochTimestampHappenedInDocument = { GetEpochTimestampHappenedInQuery, GetEpochTimestampHappenedInQueryVariables >; -export const GetEpochsEndTimeDocument = { +export const GetEpochsStartEndTimeDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', - name: { kind: 'Name', value: 'GetEpochsEndTime' }, + name: { kind: 'Name', value: 'GetEpochsStartEndTime' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -1018,6 +1134,7 @@ export const GetEpochsEndTimeDocument = { selections: [ { kind: 'Field', name: { kind: 'Name', value: 'epoch' } }, { kind: 'Field', name: { kind: 'Name', value: 'toTs' } }, + { kind: 'Field', name: { kind: 'Name', value: 'fromTs' } }, ], }, }, @@ -1025,7 +1142,7 @@ export const GetEpochsEndTimeDocument = { }, }, ], -} as unknown as DocumentNode; +} as unknown as DocumentNode; export const GetLargestLockedAmountDocument = { kind: 'Document', definitions: [ diff --git a/client/src/hooks/subgraph/useEpochsEndTime.ts b/client/src/hooks/subgraph/useEpochsStartEndTime.ts similarity index 59% rename from client/src/hooks/subgraph/useEpochsEndTime.ts rename to client/src/hooks/subgraph/useEpochsStartEndTime.ts index 330334d913..bad5be2daf 100644 --- a/client/src/hooks/subgraph/useEpochsEndTime.ts +++ b/client/src/hooks/subgraph/useEpochsStartEndTime.ts @@ -4,34 +4,39 @@ import request from 'graphql-request'; import { QUERY_KEYS } from 'api/queryKeys'; import env from 'env'; import { graphql } from 'gql/gql'; -import { GetEpochsEndTimeQuery } from 'gql/graphql'; +import { GetEpochsStartEndTimeQuery } from 'gql/graphql'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; -type EpochsEndTime = { +type EpochsStartEndTime = { epoches: { epoch: number; // timestamp + fromTs: string; + // timestamp toTs: string; }[]; }; -const GET_EPOCHS_END_TIME = graphql(` - query GetEpochsEndTime($lastEpoch: Int) { +const GET_EPOCHS_START_END_TIME = graphql(` + query GetEpochsStartEndTime($lastEpoch: Int) { epoches(first: $lastEpoch) { epoch toTs + fromTs } } `); -export default function useEpochsEndTime(): UseQueryResult { +export default function useEpochsStartEndTime(): UseQueryResult< + EpochsStartEndTime['epoches'] | null +> { const { subgraphAddress } = env; const { data: currentEpoch } = useCurrentEpoch(); - return useQuery( + return useQuery( QUERY_KEYS.epochesEndTime(currentEpoch!), async () => - request(subgraphAddress, GET_EPOCHS_END_TIME, { + request(subgraphAddress, GET_EPOCHS_START_END_TIME, { lastEpoch: currentEpoch, }), { diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 2d4ef75be9..225687a480 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -40,7 +40,8 @@ "thresholdDataUnavailable": "Threshold data unavailable", "noThresholdData": "No threshold data", "valueCantBeEmpty": "Value can't be empty", - "lessThan1m": "less than 1m" + "lessThan1m": "less than 1m", + "donors": "Donors" }, "components": { "settings": { @@ -138,9 +139,6 @@ "noDonationsYet": "No donations yet", "viewAll": "View all" }, - "donorsHeader": { - "donors": "Donors" - }, "glmLock": { "lock": "Lock", "unlock": "Unlock", @@ -222,11 +220,14 @@ "text": "Loading of a proposal
encountered an error." }, "proposalRewards": { + "epoch": "Epoch {{epoch}}", "currentTotal": "Current total", "totalRaised": "Total raised", "totalDonated": "Total donated", + "fundedIn": "Funded in", "fundedAt": "Funded at", - "didNotReach": "Did not reach" + "didNotReach": "Did not reach", + "didNotReachThreshold": "Did not reach threshold" }, "proposalsList": { "epochArchive": "Epoch {{epoch}} Archive" @@ -251,6 +252,9 @@ "estimatedGasPrice": "Est. gas price", "withdrawAll": "Withdraw all", "waitingForConfirmation": "Waiting for confirmation" + }, + "buttonAddToAllocate": { + "donated": "Donated" } } }, diff --git a/client/src/views/ProposalView/ProposalView.module.scss b/client/src/views/ProposalView/ProposalView.module.scss index 1f647dd6dc..a466bdf46e 100644 --- a/client/src/views/ProposalView/ProposalView.module.scss +++ b/client/src/views/ProposalView/ProposalView.module.scss @@ -98,23 +98,10 @@ } .buttonAddToAllocate { - &Primary { - margin-left: 3.2rem; + margin-left: 3.2rem; - @media #{$desktop-up} { - display: none; - margin-left: 2.6rem; - - &.isEpoch1 { - display: initial; - } - } - } - &Secondary { - display: none; - @media #{$desktop-up} { - display: initial; - } + @media #{$desktop-up} { + margin-left: 2.6rem; } } diff --git a/client/src/views/ProposalView/ProposalView.tsx b/client/src/views/ProposalView/ProposalView.tsx index c85f8503a8..0de99086a1 100644 --- a/client/src/views/ProposalView/ProposalView.tsx +++ b/client/src/views/ProposalView/ProposalView.tsx @@ -59,6 +59,9 @@ const ProposalView = (): ReactElement => { const { data: areCurrentEpochsProjectsHiddenOutsideAllocationWindow } = useAreCurrentEpochsProjectsHiddenOutsideAllocationWindow(); + const isArchivedProposal = + epochUrl && currentEpoch ? parseInt(epochUrl!, 10) < currentEpoch : false; + useEffect(() => { if (loadedAddresses.length === 0) { setLoadedAddresses([proposalAddressUrl!]); @@ -241,6 +244,7 @@ const ProposalView = (): ReactElement => { isAllocatedTo: !!userAllocations?.elements.find( ({ address: userAllocationAddress }) => userAllocationAddress === address, ), + isArchivedProposal, onClick: () => onAddRemoveFromAllocate(address), }; return ( @@ -273,11 +277,8 @@ const ProposalView = (): ReactElement => { />
@@ -296,17 +297,9 @@ const ProposalView = (): ReactElement => { {!isEpoch1 ? ( - ) - } + epoch={isArchivedProposal ? parseInt(epochUrl!, 10) : undefined} + isProposalView /> ) : (
From 66ad26d364e3bf0a1167536da1c317f140b88444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 8 Nov 2023 15:34:46 +0100 Subject: [PATCH 26/35] oct-1105 totalDonated fix --- client/src/api/queryKeys/index.ts | 6 +++++- client/src/api/queryKeys/types.ts | 5 ++++- .../src/components/dedicated/Donors/Donors.tsx | 7 ++++++- .../dedicated/DonorsHeader/DonorsHeader.tsx | 8 ++++++-- .../dedicated/DonorsList/DonorsList.tsx | 7 ++++++- .../ProposalRewards/ProposalRewards.tsx | 12 +++++++----- client/src/hooks/queries/useProposalDonors.ts | 16 +++++----------- client/tsconfig.json | 2 -- client/vite.config.js | 3 --- 9 files changed, 39 insertions(+), 27 deletions(-) diff --git a/client/src/api/queryKeys/index.ts b/client/src/api/queryKeys/index.ts index d201017e20..bea7ae1305 100644 --- a/client/src/api/queryKeys/index.ts +++ b/client/src/api/queryKeys/index.ts @@ -38,7 +38,11 @@ export const QUERY_KEYS: QueryKeys = { lockedSummaryLatest: ['lockedSummaryLatest'], matchedProposalRewards: epochNumber => [ROOTS.matchedProposalRewards, epochNumber.toString()], patronMode: userAddress => [ROOTS.patronMode, userAddress], - proposalDonors: proposalAddress => [ROOTS.proposalDonors, proposalAddress], + proposalDonors: (proposalAddress, epochNumber) => [ + ROOTS.proposalDonors, + proposalAddress, + epochNumber.toString(), + ], proposalRewardsThreshold: epochNumber => [ROOTS.proposalRewardsThreshold, epochNumber.toString()], proposalsAllIpfs: ['proposalsAllIpfs'], proposalsCid: ['proposalsCid'], diff --git a/client/src/api/queryKeys/types.ts b/client/src/api/queryKeys/types.ts index b734ccb524..49c5c10d31 100644 --- a/client/src/api/queryKeys/types.ts +++ b/client/src/api/queryKeys/types.ts @@ -40,7 +40,10 @@ export type QueryKeys = { lockedSummaryLatest: ['lockedSummaryLatest']; matchedProposalRewards: (epochNumber: number) => [Root['matchedProposalRewards'], string]; patronMode: (userAddress: string) => [Root['patronMode'], string]; - proposalDonors: (proposalAddress: string) => [Root['proposalDonors'], string]; + proposalDonors: ( + proposalAddress: string, + epochNumber: number, + ) => [Root['proposalDonors'], string, string]; proposalRewardsThreshold: (epochNumber: number) => [Root['proposalRewardsThreshold'], string]; proposalsAllIpfs: ['proposalsAllIpfs']; proposalsCid: ['proposalsCid']; diff --git a/client/src/components/dedicated/Donors/Donors.tsx b/client/src/components/dedicated/Donors/Donors.tsx index 31733457ea..3fc73eee51 100644 --- a/client/src/components/dedicated/Donors/Donors.tsx +++ b/client/src/components/dedicated/Donors/Donors.tsx @@ -1,6 +1,7 @@ import cx from 'classnames'; import React, { FC, useState, Fragment } from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import Button from 'components/core/Button/Button'; import DonorsHeader from 'components/dedicated/DonorsHeader/DonorsHeader'; @@ -14,8 +15,12 @@ import styles from './Donors.module.scss'; import DonorsProps from './types'; const Donors: FC = ({ className, dataTest = 'Donors', proposalAddress }) => { + const { epoch } = useParams(); const { t } = useTranslation('translation', { keyPrefix: 'components.dedicated.donors' }); - const { data: proposalDonors, isFetching } = useProposalDonors(proposalAddress); + const { data: proposalDonors, isFetching } = useProposalDonors( + proposalAddress, + parseInt(epoch!, 10), + ); const { data: currentEpoch } = useCurrentEpoch(); const [isFullDonorsListModalOpen, setIsFullDonorsListModalOpen] = useState(false); diff --git a/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx b/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx index 53410908ed..1279683c53 100644 --- a/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx +++ b/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx @@ -1,6 +1,7 @@ import cx from 'classnames'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import useProposalDonors from 'hooks/queries/useProposalDonors'; @@ -12,10 +13,13 @@ const DonorsHeader: FC = ({ dataTest = 'DonorsHeader', className, }) => { + const { epoch } = useParams(); const { i18n } = useTranslation('translation'); - const { data: proposalDonors, isFetching } = useProposalDonors(proposalAddress); - + const { data: proposalDonors, isFetching } = useProposalDonors( + proposalAddress, + parseInt(epoch!, 10), + ); return (
{i18n.t('common.donors')}{' '} diff --git a/client/src/components/dedicated/DonorsList/DonorsList.tsx b/client/src/components/dedicated/DonorsList/DonorsList.tsx index a18cea9b42..5c608d1049 100644 --- a/client/src/components/dedicated/DonorsList/DonorsList.tsx +++ b/client/src/components/dedicated/DonorsList/DonorsList.tsx @@ -1,5 +1,6 @@ import cx from 'classnames'; import React, { FC } from 'react'; +import { useParams } from 'react-router-dom'; import DonorsItem from 'components/dedicated/DonorsItem/DonorsItem'; import DonorsItemSkeleton from 'components/dedicated/DonorsItem/DonorsItemSkeleton/DonorsItemSkeleton'; @@ -15,7 +16,11 @@ const DonorsList: FC = ({ proposalAddress, showFullList = false, }) => { - const { data: proposalDonors, isFetching } = useProposalDonors(proposalAddress); + const { epoch } = useParams(); + const { data: proposalDonors, isFetching } = useProposalDonors( + proposalAddress, + parseInt(epoch!, 10), + ); return (
diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx index 93da1960ab..6d0c7ac9fa 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx @@ -29,15 +29,17 @@ const ProposalRewards: FC = ({ const { data: proposalRewardsThreshold } = useProposalRewardsThreshold(epoch); const { data: matchedProposalRewards } = useMatchedProposalRewards(epoch); - const { data: proposalDonors } = useProposalDonors(address); + const { data: proposalDonors } = useProposalDonors(address, epoch); const proposalMatchedProposalRewards = matchedProposalRewards?.find( ({ address: proposalAddress }) => address === proposalAddress, ); - const proposalDonorsRewardsSum = proposalDonors?.reduce( - (prev, curr) => prev.add(formatUnits(curr.amount, 'wei')), - BigNumber.from(0), - ); + const proposalDonorsRewardsSum = isArchivedProposal + ? proposalDonors?.reduce( + (prev, curr) => prev.add(formatUnits(curr.amount, 'wei')), + BigNumber.from(0), + ) + : proposalMatchedProposalRewards?.sum; const isDonationAboveThreshold = useIsDonationAboveThreshold(address, epoch); diff --git a/client/src/hooks/queries/useProposalDonors.ts b/client/src/hooks/queries/useProposalDonors.ts index a0f85cd927..168e270e94 100644 --- a/client/src/hooks/queries/useProposalDonors.ts +++ b/client/src/hooks/queries/useProposalDonors.ts @@ -1,11 +1,9 @@ -import { UseQueryOptions, UseQueryResult, useQuery, useQueryClient } from '@tanstack/react-query'; +import { UseQueryOptions, UseQueryResult, useQuery } from '@tanstack/react-query'; import { BigNumber } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { apiGetProposalDonors, Response } from 'api/calls/poroposalDonors'; import { QUERY_KEYS } from 'api/queryKeys'; -import useSubscription from 'hooks/helpers/useSubscription'; -import { WebsocketListenEvent } from 'types/websocketEvents'; import useCurrentEpoch from './useCurrentEpoch'; @@ -22,20 +20,16 @@ const mapDataToProposalDonors = (data: Response): ProposalDonors => export default function useProposalDonors( proposalAddress: string, + epoch?: number, options?: UseQueryOptions, ): UseQueryResult { - const queryClient = useQueryClient(); const { data: currentEpoch } = useCurrentEpoch(); - useSubscription(WebsocketListenEvent.proposalDonors, data => { - queryClient.setQueryData(QUERY_KEYS.proposalDonors(proposalAddress), data); - }); - return useQuery( - QUERY_KEYS.proposalDonors(proposalAddress), - () => apiGetProposalDonors(proposalAddress, currentEpoch! - 1), + QUERY_KEYS.proposalDonors(proposalAddress, epoch || currentEpoch! - 1), + () => apiGetProposalDonors(proposalAddress, epoch || currentEpoch! - 1), { - enabled: !!currentEpoch && !!proposalAddress && currentEpoch > 1, + enabled: !!proposalAddress && (epoch !== undefined || !!(currentEpoch && currentEpoch > 1)), select: response => mapDataToProposalDonors(response), staleTime: Infinity, ...options, diff --git a/client/tsconfig.json b/client/tsconfig.json index 2f0ebcdfac..54c51f2598 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -20,8 +20,6 @@ "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, - "sourceMap": true, - "inlineSourceMap": true, "strict": true, "target": "es2022" }, diff --git a/client/vite.config.js b/client/vite.config.js index 6a770a2873..ffce81d710 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -55,9 +55,6 @@ export default defineConfig(({ mode }) => { return { base, - build: { - sourcemap: 'inline', - }, css: { modules: { generateScopedName: localIdentName, From 2d509c6629ccececa082428eacbc9f42666369a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Thu, 9 Nov 2023 12:32:24 +0000 Subject: [PATCH 27/35] [TEST] Restore CY onboarding suite --- .gitlab-ci.yml | 2 +- client/cypress/e2e/onboarding.cy.ts | 381 ++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 client/cypress/e2e/onboarding.cy.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ae312dd4b3..c1fd2fd5dc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -248,7 +248,7 @@ Documentation: parallel: matrix: - SERVICE: - - contracts-v1 + - contracts-v1 rules: - !reference [.rules, on_mr ] - !reference [.rules, on_push_to_default_branch ] diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts new file mode 100644 index 0000000000..7b3e94bdec --- /dev/null +++ b/client/cypress/e2e/onboarding.cy.ts @@ -0,0 +1,381 @@ +import { visitWithLoader, navigateWithCheck } from 'cypress/utils/e2e'; +import viewports from 'cypress/utils/viewports'; +import { stepsDecisionWindowClosed } from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; + +import Chainable = Cypress.Chainable; + +const connectWallet = ( + isTOSAccepted: boolean, + shouldVisit = true, + shouldReload = false, +): Chainable => { + cy.intercept('GET', '/user/*/tos', { body: { accepted: isTOSAccepted } }); + cy.disconnectMetamaskWalletFromAllDapps(); + if (shouldVisit) { + visitWithLoader(ROOT.absolute, ROOT_ROUTES.proposals.absolute); + } + if (shouldReload) { + cy.reload(); + } + cy.get('[data-test=MainLayout__Button--connect]').click(); + cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); + cy.switchToMetamaskNotification(); + return cy.acceptMetamaskAccess(); +}; + +const beforeSetup = () => { + cy.clearLocalStorage(); + cy.setupMetamask(); + cy.activateShowTestnetNetworksInMetamask(); + cy.changeMetamaskNetwork('sepolia'); + window.innerWidth = Cypress.config().viewportWidth; + window.innerHeight = Cypress.config().viewportHeight; +}; + +const checkCurrentElement = (el: number, isCurrent: boolean): Chainable => { + return cy + .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(el) + .invoke('attr', 'data-iscurrent') + .should('eq', `${isCurrent}`); +}; + +const checkProgressStepperSlimIsCurrentAndClickNext = (index, isCurrent = true): Chainable => { + checkCurrentElement(index - 1, isCurrent); + return cy + .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(index) + .click({ force: true }); +}; + +const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + [ + { el: 1, key: 'ArrowRight' }, + { el: 2, key: 'ArrowRight' }, + { el: 2, key: 'ArrowRight' }, + // { el: 3, key: 'ArrowRight' }, + // { el: 2, key: 'ArrowLeft' }, + { el: 1, key: 'ArrowLeft' }, + { el: 0, key: 'ArrowLeft' }, + { el: 0, key: 'ArrowLeft' }, + ].forEach(({ key, el }) => { + cy.get('body').trigger('keydown', { key }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + cy.get('[data-test=ModalOnboarding]').then(element => { + const leftEdgeX = element.offsetParent().offset()?.left as number; + const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; + + [ + { clientX: rightEdgeX - 25, el: 1 }, + { clientX: rightEdgeX - 10, el: 2 }, + // { clientX: rightEdgeX - 5, el: 3 }, + // rightEdgeX === browser right frame + // { clientX: rightEdgeX - 1, el: 3 }, + // { clientX: leftEdgeX + 25, el: 2 }, + { clientX: leftEdgeX + 10, el: 1 }, + { clientX: leftEdgeX + 5, el: 0 }, + { clientX: leftEdgeX, el: 0 }, + ].forEach(({ clientX, el }) => { + cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); + }); +}; + +const checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + cy.get('[data-test=ModalOnboarding]').then(element => { + const leftEdgeX = element.offsetParent().offset()?.left as number; + const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; + + [ + { clientX: rightEdgeX - 25, el: 1 }, + { clientX: rightEdgeX - 26, el: 1 }, + { clientX: leftEdgeX + 26, el: 1 }, + { clientX: leftEdgeX + 25, el: 0 }, + ].forEach(({ clientX, el }) => { + cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); + }); +}; + +const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + [ + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + // { + // el: 3, + // touchMoveClientX: window.innerWidth / 2 - 5, + // touchStartClientX: window.innerWidth / 2, + // }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { + cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { + touches: [{ clientX: touchStartClientX }], + }); + cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { + touches: [{ clientX: touchMoveClientX }], + }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = (isTOSAccepted: boolean) => { + checkCurrentElement(0, true); + + [ + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 4, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 + 4, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { + cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { + touches: [{ clientX: touchStartClientX }], + }); + cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { + touches: [{ clientX: touchMoveClientX }], + }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { + describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { + before(() => { + beforeSetup(); + }); + + beforeEach(() => { + connectWallet(true); + }); + + it('user is able to click through entire onboarding flow', () => { + for (let i = 1; i < stepsDecisionWindowClosed.length - 1; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i); + } + + cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(stepsDecisionWindowClosed.length - 1) + .click(); + cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); + }); + + it('user is able to close the modal by clicking button in the top-right', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); + }); + + it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + navigateWithCheck(ROOT_ROUTES.settings.absolute); + cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').check().should('be.checked'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('renders only once when "Always show Allocate onboarding" option is not checked', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + navigateWithCheck(ROOT_ROUTES.settings.absolute); + cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').should('not.be.checked'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + + it('user can change steps with arrow keys (left, right)', () => { + checkChangeStepsWithArrowKeys(true); + }); + + it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); + }); + + it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); + }); + + it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); + }); + + it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + connectWallet(true, false, true); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + }); +}); + +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { + describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { + before(() => { + beforeSetup(); + }); + + beforeEach(() => { + cy.intercept( + { + method: 'POST', + url: '/user/*/tos', + }, + { body: { accepted: true }, statusCode: 200 }, + ); + connectWallet(false); + }); + + it('onboarding should have one more step (TOS)', () => { + cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]').should( + 'have.length', + stepsDecisionWindowClosed.length + 1, + ); + }); + + it('user is not able to click through entire onboarding flow', () => { + for (let i = 1; i < stepsDecisionWindowClosed.length; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); + } + }); + + it('user is not able to close the modal by clicking button in the top-right', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('renders every time page is refreshed', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('user cannot change steps with arrow keys (left, right)', () => { + checkChangeStepsWithArrowKeys(false); + }); + + it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); + }); + + it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); + }); + + it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); + }); + + it('TOS acceptance changes onboarding step to next step', () => { + checkCurrentElement(0, true); + cy.get('[data-test=TOS_InputCheckbox]').check(); + cy.switchToMetamaskNotification(); + cy.confirmMetamaskSignatureRequest(); + checkCurrentElement(1, true); + }); + + it('TOS acceptance allows the user to close the modal by clicking button in the top-right', () => { + checkCurrentElement(0, true); + cy.get('[data-test=TOS_InputCheckbox]').check(); + cy.switchToMetamaskNotification(); + cy.confirmMetamaskSignatureRequest(); + checkCurrentElement(1, true); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + }); +}); From 5adbddcd6dc66f5e35e0d33f62a18612183c712d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 9 Nov 2023 14:42:52 +0100 Subject: [PATCH 28/35] oct-1105 epoch end date fix --- .../dedicated/ProposalsList/ProposalsList.tsx | 13 ++++++++++--- client/src/gql/gql.ts | 6 +++--- client/src/gql/graphql.ts | 9 ++++++++- client/src/hooks/subgraph/useEpochsStartEndTime.ts | 3 +++ 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/client/src/components/dedicated/ProposalsList/ProposalsList.tsx b/client/src/components/dedicated/ProposalsList/ProposalsList.tsx index 599e9bbfd7..81b75c46ff 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsList.tsx +++ b/client/src/components/dedicated/ProposalsList/ProposalsList.tsx @@ -34,15 +34,22 @@ const ProposalsList: FC = ({ const epochData = epochsStartEndTime[epoch - 1]; const epochStartTimestamp = parseInt(epochData.fromTs, 10) * 1000; - const epochEndTimestamp = parseInt(epochData.toTs, 10) * 1000; + const epochEndTimestampPlusDecisionWindowDuration = + (parseInt(epochData.toTs, 10) + parseInt(epochData.decisionWindow, 10)) * 1000; - const isEpochEndedAtTheSameYear = isSameYear(epochStartTimestamp, epochEndTimestamp); + const isEpochEndedAtTheSameYear = isSameYear( + epochStartTimestamp, + epochEndTimestampPlusDecisionWindowDuration, + ); const epochStartLabel = format( epochStartTimestamp, `${isDesktop ? 'dd MMMM' : 'MMM'} ${isEpochEndedAtTheSameYear ? '' : 'yyyy'}`, ); - const epochEndLabel = format(epochEndTimestamp, `${isDesktop ? 'dd MMMM' : 'MMM'} yyyy`); + const epochEndLabel = format( + epochEndTimestampPlusDecisionWindowDuration, + `${isDesktop ? 'dd MMMM' : 'MMM'} yyyy`, + ); return `${epochStartLabel} -> ${epochEndLabel}`; }, [epoch, epochsStartEndTime, isDesktop]); diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index 079788575f..d145d0c9ca 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -17,7 +17,7 @@ const documents = { types.GetBlockNumberDocument, '\n query GetEpochTimestampHappenedIn($timestamp: BigInt) {\n epoches(where: { fromTs_lte: $timestamp, toTs_gte: $timestamp }) {\n epoch\n }\n }\n': types.GetEpochTimestampHappenedInDocument, - '\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n }\n }\n': + '\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n decisionWindow\n }\n }\n': types.GetEpochsStartEndTimeDocument, '\n query GetLargestLockedAmount {\n lockeds(orderBy: amount, orderDirection: desc, first: 1) {\n amount\n }\n }\n': types.GetLargestLockedAmountDocument, @@ -57,8 +57,8 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: '\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n }\n }\n', -): (typeof documents)['\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n }\n }\n']; + source: '\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n decisionWindow\n }\n }\n', +): (typeof documents)['\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n decisionWindow\n }\n }\n']; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index 3e5955b077..a2bf2080b9 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -984,7 +984,13 @@ export type GetEpochsStartEndTimeQueryVariables = Exact<{ export type GetEpochsStartEndTimeQuery = { __typename?: 'Query'; - epoches: Array<{ __typename?: 'Epoch'; epoch: number; toTs: any; fromTs: any }>; + epoches: Array<{ + __typename?: 'Epoch'; + epoch: number; + toTs: any; + fromTs: any; + decisionWindow: any; + }>; }; export type GetLargestLockedAmountQueryVariables = Exact<{ [key: string]: never }>; @@ -1135,6 +1141,7 @@ export const GetEpochsStartEndTimeDocument = { { kind: 'Field', name: { kind: 'Name', value: 'epoch' } }, { kind: 'Field', name: { kind: 'Name', value: 'toTs' } }, { kind: 'Field', name: { kind: 'Name', value: 'fromTs' } }, + { kind: 'Field', name: { kind: 'Name', value: 'decisionWindow' } }, ], }, }, diff --git a/client/src/hooks/subgraph/useEpochsStartEndTime.ts b/client/src/hooks/subgraph/useEpochsStartEndTime.ts index bad5be2daf..736146f7a1 100644 --- a/client/src/hooks/subgraph/useEpochsStartEndTime.ts +++ b/client/src/hooks/subgraph/useEpochsStartEndTime.ts @@ -9,6 +9,8 @@ import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; type EpochsStartEndTime = { epoches: { + // decision window duration + decisionWindow: string; epoch: number; // timestamp fromTs: string; @@ -23,6 +25,7 @@ const GET_EPOCHS_START_END_TIME = graphql(` epoch toTs fromTs + decisionWindow } } `); From 0f80766dd1dfd5833740d2487d68466f99ad768f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 9 Nov 2023 14:58:42 +0100 Subject: [PATCH 29/35] oct-1107 removed unnecessary changes --- .../core/ProgressBar/ProgressBar.module.scss | 4 ---- client/src/components/core/ProgressBar/types.ts | 2 +- .../ButtonAddToAllocate/ButtonAddToAllocate.tsx | 15 +-------------- .../ProposalRewards/ProposalRewards.module.scss | 5 +---- .../dedicated/ProposalRewards/ProposalRewards.tsx | 3 +-- client/src/locales/en/translation.json | 3 --- 6 files changed, 4 insertions(+), 28 deletions(-) diff --git a/client/src/components/core/ProgressBar/ProgressBar.module.scss b/client/src/components/core/ProgressBar/ProgressBar.module.scss index f900cdbe19..f60b293784 100644 --- a/client/src/components/core/ProgressBar/ProgressBar.module.scss +++ b/client/src/components/core/ProgressBar/ProgressBar.module.scss @@ -21,10 +21,6 @@ overflow: hidden; &.variant-- { - &ultraThin { - height: 0.2rem; - } - &thin { height: 0.4rem; } diff --git a/client/src/components/core/ProgressBar/types.ts b/client/src/components/core/ProgressBar/types.ts index b2de08c9e6..4165aee3b8 100644 --- a/client/src/components/core/ProgressBar/types.ts +++ b/client/src/components/core/ProgressBar/types.ts @@ -7,5 +7,5 @@ export default interface ProgressBarProps { labelLeft?: string; labelRight?: string; progressPercentage: number; - variant?: 'ultraThin' | 'thin' | 'normal'; + variant?: 'thin' | 'normal'; } diff --git a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx index f4f1de6b69..f9d7047376 100644 --- a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx +++ b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx @@ -1,10 +1,8 @@ import cx from 'classnames'; import React, { FC, useRef, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; import Button from 'components/core/Button/Button'; import Svg from 'components/core/Svg/Svg'; -import Tooltip from 'components/core/Tooltip/Tooltip'; import { IS_INITIAL_LOAD_DONE } from 'constants/dataAttributes'; import { checkMark, heart } from 'svg/misc'; @@ -19,9 +17,6 @@ const ButtonAddToAllocate: FC = ({ isAllocatedTo, isArchivedProposal, }) => { - const { t } = useTranslation('translation', { - keyPrefix: 'components.dedicated.buttonAddToAllocate', - }); const ref = useRef(null); useEffect(() => { @@ -42,15 +37,7 @@ const ButtonAddToAllocate: FC = ({ [IS_INITIAL_LOAD_DONE]: 'false', }} dataTest={dataTest} - Icon={ - isArchivedProposal && isAllocatedTo ? ( - - - - ) : ( - - ) - } + Icon={} isDisabled={isArchivedProposal} onClick={onClick} variant="iconOnly" diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss b/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss index 4113bb23e1..7488f98ba1 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss @@ -6,6 +6,7 @@ justify-content: space-between; .divider { + margin: 0.3rem 0; height: 0.2rem; width: 100%; content: ''; @@ -17,10 +18,6 @@ display: flex; justify-content: space-between; - &.isArchivedProposal { - margin-top: 1.6rem; - } - .section { font-weight: $font-weight-bold; color: $color-octant-grey5; diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx index 6d0c7ac9fa..8759e65c2d 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx @@ -118,12 +118,11 @@ const ProposalRewards: FC = ({ proposalDonorsRewardsSum, proposalRewardsThreshold, )} - variant={isArchivedProposal ? 'ultraThin' : 'normal'} /> ) : (
)} -
+
{leftSectionLabel} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index baaf7264a0..fc261ba078 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -252,9 +252,6 @@ "estimatedGasPrice": "Est. gas price", "withdrawAll": "Withdraw all", "waitingForConfirmation": "Waiting for confirmation" - }, - "buttonAddToAllocate": { - "donated": "Donated" } } }, From 7c1cc0c9ec2a0d42ed62f4d8e1f6e731e0a3e99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 9 Nov 2023 15:19:17 +0100 Subject: [PATCH 30/35] oct-1107 cr fixes --- .../ProposalRewards/ProposalRewards.tsx | 2 +- client/src/hooks/queries/useProposalDonors.ts | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx index 8759e65c2d..01845a160b 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx @@ -36,7 +36,7 @@ const ProposalRewards: FC = ({ const proposalDonorsRewardsSum = isArchivedProposal ? proposalDonors?.reduce( - (prev, curr) => prev.add(formatUnits(curr.amount, 'wei')), + (acc, curr) => acc.add(formatUnits(curr.amount, 'wei')), BigNumber.from(0), ) : proposalMatchedProposalRewards?.sum; diff --git a/client/src/hooks/queries/useProposalDonors.ts b/client/src/hooks/queries/useProposalDonors.ts index 168e270e94..06d7de4bc9 100644 --- a/client/src/hooks/queries/useProposalDonors.ts +++ b/client/src/hooks/queries/useProposalDonors.ts @@ -1,9 +1,11 @@ -import { UseQueryOptions, UseQueryResult, useQuery } from '@tanstack/react-query'; +import { UseQueryOptions, UseQueryResult, useQuery, useQueryClient } from '@tanstack/react-query'; import { BigNumber } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { apiGetProposalDonors, Response } from 'api/calls/poroposalDonors'; import { QUERY_KEYS } from 'api/queryKeys'; +import useSubscription from 'hooks/helpers/useSubscription'; +import { WebsocketListenEvent } from 'types/websocketEvents'; import useCurrentEpoch from './useCurrentEpoch'; @@ -23,8 +25,22 @@ export default function useProposalDonors( epoch?: number, options?: UseQueryOptions, ): UseQueryResult { + const queryClient = useQueryClient(); const { data: currentEpoch } = useCurrentEpoch(); + /** + * Socket returns proposal donors for current epoch only. + * When hook is called for other epoch, subscribe should not be used. + */ + useSubscription(WebsocketListenEvent.proposalDonors, data => { + epoch + ? null + : queryClient.setQueryData( + QUERY_KEYS.proposalDonors(proposalAddress, currentEpoch! - 1), + data, + ); + }); + return useQuery( QUERY_KEYS.proposalDonors(proposalAddress, epoch || currentEpoch! - 1), () => apiGetProposalDonors(proposalAddress, epoch || currentEpoch! - 1), From 26932d3db6261811f49d56a9ba76c74cc4acf0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 9 Nov 2023 16:24:03 +0100 Subject: [PATCH 31/35] oct-1105 lint fix --- client/src/hooks/queries/useProposalDonors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/hooks/queries/useProposalDonors.ts b/client/src/hooks/queries/useProposalDonors.ts index 06d7de4bc9..32a0031cb9 100644 --- a/client/src/hooks/queries/useProposalDonors.ts +++ b/client/src/hooks/queries/useProposalDonors.ts @@ -33,6 +33,7 @@ export default function useProposalDonors( * When hook is called for other epoch, subscribe should not be used. */ useSubscription(WebsocketListenEvent.proposalDonors, data => { + // eslint-disable-next-line chai-friendly/no-unused-expressions epoch ? null : queryClient.setQueryData( From 75300f186fd78d0505ed32a0aff84154f5f0d077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Thu, 9 Nov 2023 22:41:49 +0100 Subject: [PATCH 32/35] feat: skeletons for data loading --- .../ProposalRewards.module.scss | 13 ++++++++- .../ProposalRewards/ProposalRewards.tsx | 29 +++++++++++++++---- .../ProposalsList/ProposalsList.module.scss | 9 +++++- .../dedicated/ProposalsList/ProposalsList.tsx | 15 ++++++++-- .../ProposalsListItem.module.scss | 4 +-- .../ProposalsListItemSkeleton.module.scss | 18 +++++++----- .../ProposalsListItemSkeleton.tsx | 6 ++-- client/src/styles/utils/_variables.scss | 1 + 8 files changed, 70 insertions(+), 25 deletions(-) diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss b/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss index 7488f98ba1..a9b7ad5515 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss @@ -22,9 +22,20 @@ font-weight: $font-weight-bold; color: $color-octant-grey5; + .label, .value { + &.isFetching { + @include skeleton(); + text-indent: 1000%; // moves text outside of view. + white-space: nowrap; + overflow: hidden; + width: 8.8rem; + } + } + .label { font-size: $font-size-12; - height: 2.4rem; + height: 1.6rem; + margin-bottom: 0.4rem; display: flex; align-items: center; } diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx index 01845a160b..e3113c83a4 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx @@ -27,9 +27,14 @@ const ProposalRewards: FC = ({ const isArchivedProposal = epoch !== undefined; - const { data: proposalRewardsThreshold } = useProposalRewardsThreshold(epoch); - const { data: matchedProposalRewards } = useMatchedProposalRewards(epoch); - const { data: proposalDonors } = useProposalDonors(address, epoch); + const { data: proposalRewardsThreshold, isFetching: isFetchingProposalRewardsThreshold } = + useProposalRewardsThreshold(epoch); + const { data: matchedProposalRewards, isFetching: isFetchingMatchedProposalRewards } = + useMatchedProposalRewards(epoch); + const { data: proposalDonors, isFetching: isFetchingProposalDonors } = useProposalDonors( + address, + epoch, + ); const proposalMatchedProposalRewards = matchedProposalRewards?.find( ({ address: proposalAddress }) => address === proposalAddress, ); @@ -109,6 +114,10 @@ const ProposalRewards: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, rightSectionValueUseMemoDeps); + const isFetching = + isFetchingProposalRewardsThreshold || + isFetchingMatchedProposalRewards || + isFetchingProposalDonors; return (
{showProgressBar ? ( @@ -124,7 +133,10 @@ const ProposalRewards: FC = ({ )}
-
+
{leftSectionLabel}
= ({ styles.value, isDonationAboveThreshold && isArchivedProposal && styles.greenValue, !isDonationAboveThreshold && !isArchivedProposal && styles.redValue, + isFetching && styles.isFetching, )} data-test="ProposalRewards__currentTotal__number" > @@ -142,8 +155,12 @@ const ProposalRewards: FC = ({
{!(isDonationAboveThreshold && !isArchivedProposal) && (
-
{rightSectionLabel}
-
{rightSectionValue}
+
+ {rightSectionLabel} +
+
+ {rightSectionValue} +
)}
diff --git a/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss b/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss index 075a0f5094..ddbb25680b 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss +++ b/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss @@ -29,10 +29,17 @@ $elementMargin: 1.6rem; border-radius: $border-radius-16; margin: 1.6rem 0; - .epochDuration { + .epochDurationLabel { color: $color-octant-grey5; font-size: $font-size-12; font-weight: $font-weight-medium; + + &.isFetching { + @include skeleton(); + text-indent: 1000%; // moves text outside of view. + white-space: nowrap; + overflow: hidden; + } } } diff --git a/client/src/components/dedicated/ProposalsList/ProposalsList.tsx b/client/src/components/dedicated/ProposalsList/ProposalsList.tsx index 81b75c46ff..dc8967c49c 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsList.tsx +++ b/client/src/components/dedicated/ProposalsList/ProposalsList.tsx @@ -1,3 +1,4 @@ +import cx from 'classnames'; import { format, isSameYear } from 'date-fns'; import React, { FC, memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -24,7 +25,8 @@ const ProposalsList: FC = ({ const { isDesktop } = useMediaQuery(); const { data: proposalsAddresses } = useProposalsContract(epoch); - const { data: proposalsWithRewards } = useProposalsIpfsWithRewards(epoch); + const { data: proposalsWithRewards, isFetching: isFetchingProposalsWithRewards } = + useProposalsIpfsWithRewards(epoch); const { data: epochsStartEndTime } = useEpochsStartEndTime(); const epochDurationLabel = useMemo(() => { @@ -66,11 +68,18 @@ const ProposalsList: FC = ({ )}
{t('epochArchive', { epoch })} - {epochDurationLabel} + + {epochDurationLabel} +
)} - {proposalsWithRewards.length > 0 + {proposalsWithRewards.length > 0 && !isFetchingProposalsWithRewards ? proposalsWithRewards.map((proposalWithRewards, index) => ( = ({ classNa
-
+
-
+
-
+
diff --git a/client/src/styles/utils/_variables.scss b/client/src/styles/utils/_variables.scss index 08b8270448..8c575be8aa 100644 --- a/client/src/styles/utils/_variables.scss +++ b/client/src/styles/utils/_variables.scss @@ -28,3 +28,4 @@ $layoutMarginHorizontal: 2.4rem; $progressStepperSlimStepPadding: 2.4rem; $modalVariantSmallPaddingMobile: 2.4rem; $modalVariantSmallPaddingDesktop: 5.6rem; +$proposalItemPadding: 2.4rem; From 9b9a6b94c0b79d2a723df52f958212e7439c2c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Fri, 10 Nov 2023 08:58:44 +0000 Subject: [PATCH 33/35] OCT-1076 Don't mix GWEI & ETH together in list views --- .../core/BoxRounded/Sections/types.ts | 1 + .../core/DoubleValue/DoubleValue.tsx | 2 ++ .../src/components/core/DoubleValue/types.ts | 1 + .../src/components/core/DoubleValue/utils.ts | 3 ++ .../AllocationSummary/AllocationSummary.tsx | 2 ++ .../dedicated/DonorsItem/DonorsItem.tsx | 1 + .../HistoryItemDetailsRest.tsx | 2 ++ .../ProjectAllocationDetailRow.tsx | 1 + .../ProposalRewards/ProposalRewards.tsx | 1 + client/src/hooks/queries/useProposalDonors.ts | 18 +++++++++--- client/src/utils/getFormattedEthValue.test.ts | 28 +++++++++++++++++-- client/src/utils/getFormattedEthValue.ts | 5 ++++ client/src/utils/getValueCryptoToDisplay.ts | 4 ++- 13 files changed, 61 insertions(+), 8 deletions(-) diff --git a/client/src/components/core/BoxRounded/Sections/types.ts b/client/src/components/core/BoxRounded/Sections/types.ts index 122738c08c..121f1e0225 100644 --- a/client/src/components/core/BoxRounded/Sections/types.ts +++ b/client/src/components/core/BoxRounded/Sections/types.ts @@ -16,6 +16,7 @@ export interface SectionProps { dataTest?: DoubleValueProps['dataTest']; isDisabled?: DoubleValueProps['isDisabled']; isFetching?: DoubleValueProps['isFetching']; + shouldIgnoreGwei?: DoubleValueProps['shouldIgnoreGwei']; valueCrypto: DoubleValueProps['valueCrypto']; }; hasBottomDivider?: boolean; diff --git a/client/src/components/core/DoubleValue/DoubleValue.tsx b/client/src/components/core/DoubleValue/DoubleValue.tsx index 84ec477cae..5767546773 100644 --- a/client/src/components/core/DoubleValue/DoubleValue.tsx +++ b/client/src/components/core/DoubleValue/DoubleValue.tsx @@ -21,6 +21,7 @@ const DoubleValue: FC = ({ valueString, variant = 'big', isFetching = false, + shouldIgnoreGwei, }) => { const { data: { displayCurrency, isCryptoMainValueDisplay }, @@ -39,6 +40,7 @@ const DoubleValue: FC = ({ displayCurrency: displayCurrency!, error, isCryptoMainValueDisplay, + shouldIgnoreGwei, valueCrypto, valueString, }); diff --git a/client/src/components/core/DoubleValue/types.ts b/client/src/components/core/DoubleValue/types.ts index d39bb6e2cb..64f817a472 100644 --- a/client/src/components/core/DoubleValue/types.ts +++ b/client/src/components/core/DoubleValue/types.ts @@ -13,6 +13,7 @@ export default interface DoubleValueProps { isDisabled?: boolean; isError?: boolean; isFetching?: boolean; + shouldIgnoreGwei?: boolean; textAlignment?: 'left' | 'right'; valueCrypto?: BigNumber; valueString?: string; diff --git a/client/src/components/core/DoubleValue/utils.ts b/client/src/components/core/DoubleValue/utils.ts index a75b6b0fcb..fe3b6079a2 100644 --- a/client/src/components/core/DoubleValue/utils.ts +++ b/client/src/components/core/DoubleValue/utils.ts @@ -17,6 +17,7 @@ export function getValuesToDisplay({ valueString, isCryptoMainValueDisplay, error, + shouldIgnoreGwei, }: { coinPricesServerDowntimeText?: DoubleValueProps['coinPricesServerDowntimeText']; cryptoCurrency: DoubleValueProps['cryptoCurrency']; @@ -24,6 +25,7 @@ export function getValuesToDisplay({ displayCurrency: NonNullable; error: any; isCryptoMainValueDisplay: SettingsData['isCryptoMainValueDisplay']; + shouldIgnoreGwei?: DoubleValueProps['shouldIgnoreGwei']; valueCrypto?: BigNumber; valueString?: DoubleValueProps['valueString']; }): { @@ -41,6 +43,7 @@ export function getValuesToDisplay({ getValueCryptoToDisplay({ cryptoCurrency, isUsingHairSpace: isCryptoMainValueDisplay, + shouldIgnoreGwei, valueCrypto, }); const valueFiatToDisplay = getValueFiatToDisplay({ diff --git a/client/src/components/dedicated/AllocationSummary/AllocationSummary.tsx b/client/src/components/dedicated/AllocationSummary/AllocationSummary.tsx index c6ef4ee4b8..04bd319736 100644 --- a/client/src/components/dedicated/AllocationSummary/AllocationSummary.tsx +++ b/client/src/components/dedicated/AllocationSummary/AllocationSummary.tsx @@ -47,6 +47,7 @@ const AllocationSummary: FC = ({ allocationValues }) => doubleValueProps: { cryptoCurrency: 'ethereum', isFetching: isFetchingIndividualReward, + shouldIgnoreGwei: true, valueCrypto: individualReward?.sub(rewardsForProposals), }, label: i18n.t('common.personal'), @@ -58,6 +59,7 @@ const AllocationSummary: FC = ({ allocationValues }) => { doubleValueProps: { cryptoCurrency: 'ethereum', + shouldIgnoreGwei: true, valueCrypto: rewardsForProposals, }, label: t('allocationProjects', { projectsNumber: allocationValuesPositive.length }), diff --git a/client/src/components/dedicated/DonorsItem/DonorsItem.tsx b/client/src/components/dedicated/DonorsItem/DonorsItem.tsx index d3eac0f6b5..68273d8462 100644 --- a/client/src/components/dedicated/DonorsItem/DonorsItem.tsx +++ b/client/src/components/dedicated/DonorsItem/DonorsItem.tsx @@ -30,6 +30,7 @@ const DonorsItem: FC = ({ donorAddress, amount, className }) => {isCryptoMainValueDisplay ? getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', + shouldIgnoreGwei: true, valueCrypto: amount, }) : getValueFiatToDisplay({ diff --git a/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDetailsRest/HistoryItemDetailsRest.tsx b/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDetailsRest/HistoryItemDetailsRest.tsx index b724aea232..49d0f045db 100644 --- a/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDetailsRest/HistoryItemDetailsRest.tsx +++ b/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDetailsRest/HistoryItemDetailsRest.tsx @@ -33,6 +33,7 @@ const HistoryItemDetailsRest: FC = ({ { doubleValueProps: { cryptoCurrency: type === 'withdrawal' ? 'ethereum' : 'golem', + shouldIgnoreGwei: true, valueCrypto: amount, }, label: t('sections.amount'), @@ -42,6 +43,7 @@ const HistoryItemDetailsRest: FC = ({ cryptoCurrency: 'ethereum', // Gas price is not known for pending transactions. isFetching: isFetchingTransaction || isWaitingForTransactionInitialized, + shouldIgnoreGwei: true, valueCrypto: BigNumber.from(transaction ? transaction.gasPrice : 0), }, label: t('sections.gasPrice'), diff --git a/client/src/components/dedicated/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx b/client/src/components/dedicated/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx index 3c5d1dfb9a..6025729414 100644 --- a/client/src/components/dedicated/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx +++ b/client/src/components/dedicated/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx @@ -41,6 +41,7 @@ const ProjectAllocationDetailRow: FC = ({ addre {isCryptoMainValueDisplay ? getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', + shouldIgnoreGwei: true, valueCrypto: amount, }) : getValueFiatToDisplay({ diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx index c393617d08..abfdead069 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx @@ -47,6 +47,7 @@ const ProposalRewards: FC = ({ const totalValueOfAllocationsToDisplay = getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', + shouldIgnoreGwei: true, valueCrypto: proposalMatchedProposalRewards?.sum, }); diff --git a/client/src/hooks/queries/useProposalDonors.ts b/client/src/hooks/queries/useProposalDonors.ts index a0f85cd927..0ccc5d504a 100644 --- a/client/src/hooks/queries/useProposalDonors.ts +++ b/client/src/hooks/queries/useProposalDonors.ts @@ -15,10 +15,20 @@ type ProposalDonors = { }[]; const mapDataToProposalDonors = (data: Response): ProposalDonors => - data.map(({ address, amount }) => ({ - address, - amount: parseUnits(amount, 'wei'), - })); + data + .map(({ address, amount }) => ({ + address, + amount: parseUnits(amount, 'wei'), + })) + .sort((a, b) => { + if (a.amount.gt(b.amount)) { + return 1; + } + if (a.amount.lt(b.amount)) { + return -1; + } + return 0; + }); export default function useProposalDonors( proposalAddress: string, diff --git a/client/src/utils/getFormattedEthValue.test.ts b/client/src/utils/getFormattedEthValue.test.ts index e70518f860..c6b0ff36dd 100644 --- a/client/src/utils/getFormattedEthValue.test.ts +++ b/client/src/utils/getFormattedEthValue.test.ts @@ -15,7 +15,17 @@ const testCases = [ { argument: BigNumber.from(10).pow(4).sub(1), expectedValue: '9999 WEI' }, { argument: BigNumber.from(10).pow(4), expectedValue: '10\u200a000 WEI' }, { argument: BigNumber.from(10).pow(5).sub(1), expectedValue: '99\u200a999 WEI' }, + { + argument: BigNumber.from(10).pow(5).sub(1), + expectedValue: '99\u200a999 WEI', + shouldIgnoreGwei: true, + }, { argument: BigNumber.from(10).pow(5), expectedValue: '0 GWEI' }, + { + argument: BigNumber.from(10).pow(5), + expectedValue: '< 0.0001 ETH', + shouldIgnoreGwei: true, + }, { argument: BigNumber.from(10).pow(6).sub(1), expectedValue: '0 GWEI' }, { argument: BigNumber.from(10).pow(6), expectedValue: '0 GWEI' }, { argument: BigNumber.from(10).pow(7).sub(1), expectedValue: '0 GWEI' }, @@ -23,6 +33,11 @@ const testCases = [ { argument: BigNumber.from(10).pow(8).sub(1), expectedValue: '0 GWEI' }, { argument: BigNumber.from(10).pow(8), expectedValue: '0 GWEI' }, { argument: BigNumber.from(10).pow(9).sub(1), expectedValue: '1 GWEI' }, + { + argument: BigNumber.from(10).pow(9).sub(1), + expectedValue: '< 0.0001 ETH', + shouldIgnoreGwei: true, + }, { argument: BigNumber.from(10).pow(9), expectedValue: '1 GWEI' }, { argument: BigNumber.from(10).pow(10).sub(1), expectedValue: '10 GWEI' }, { argument: BigNumber.from(10).pow(10), expectedValue: '10 GWEI' }, @@ -33,6 +48,11 @@ const testCases = [ { argument: BigNumber.from(10).pow(13).sub(1), expectedValue: '10\u200a000 GWEI' }, { argument: BigNumber.from(10).pow(13), expectedValue: '10\u200a000 GWEI' }, { argument: BigNumber.from(10).pow(14).sub(1), expectedValue: '100\u200a000 GWEI' }, + { + argument: BigNumber.from(10).pow(14).sub(1), + expectedValue: '< 0.0001 ETH', + shouldIgnoreGwei: true, + }, { argument: BigNumber.from(10).pow(14), expectedValue: '0.0001 ETH' }, { argument: BigNumber.from(10).pow(15).sub(1), expectedValue: '0.001 ETH' }, { argument: BigNumber.from(10).pow(15), expectedValue: '0.001 ETH' }, @@ -53,18 +73,20 @@ const testCases = [ ]; describe('getFormattedEthValue', () => { - for (const { argument, expectedValue } of testCases) { + for (const { argument, expectedValue, shouldIgnoreGwei } of testCases) { it(`returns ${expectedValue} for an argument ${formatUnits( argument, )} when isUsingHairSpace`, () => { - expect(getFormattedEthValue(argument).fullString).toBe(expectedValue); + expect(getFormattedEthValue(argument, true, shouldIgnoreGwei).fullString).toBe(expectedValue); }); const expectedValueNormalSpace = expectedValue.replace(/\u200a/g, ' '); it(`returns ${expectedValueNormalSpace} for an argument ${formatUnits( argument, )} when !isUsingHairSpace`, () => { - expect(getFormattedEthValue(argument, false).fullString).toBe(expectedValueNormalSpace); + expect(getFormattedEthValue(argument, false, shouldIgnoreGwei).fullString).toBe( + expectedValueNormalSpace, + ); }); } }); diff --git a/client/src/utils/getFormattedEthValue.ts b/client/src/utils/getFormattedEthValue.ts index 4ce4d37db4..406da642ce 100644 --- a/client/src/utils/getFormattedEthValue.ts +++ b/client/src/utils/getFormattedEthValue.ts @@ -11,7 +11,9 @@ const WEI_5 = BigNumber.from(10).pow(5); export default function getFormattedEthValue( value: BigNumber, + // eslint-disable-next-line default-param-last isUsingHairSpace = true, + shouldIgnoreGwei?: boolean, ): FormattedCryptoValue { let returnObject: Omit; @@ -22,6 +24,9 @@ export default function getFormattedEthValue( } else if (value.lt(WEI_5)) { returnObject = { suffix: 'WEI', value: formatUnits(value, 'wei') }; } else if (isInGweiRange) { + if (shouldIgnoreGwei) { + return { fullString: '< 0.0001 ETH', suffix: 'ETH', value: '< 0.0001' }; + } returnObject = { suffix: 'GWEI', value: formatUnits(value, 'gwei') }; } else { returnObject = { suffix: 'ETH', value: formatUnits(value) }; diff --git a/client/src/utils/getValueCryptoToDisplay.ts b/client/src/utils/getValueCryptoToDisplay.ts index ea14beb1d9..48e2e5d92a 100644 --- a/client/src/utils/getValueCryptoToDisplay.ts +++ b/client/src/utils/getValueCryptoToDisplay.ts @@ -8,6 +8,7 @@ import getFormattedGlmValue from './getFormattedGlmValue'; export type ValueCryptoToDisplay = { cryptoCurrency?: CryptoCurrency; isUsingHairSpace?: boolean; + shouldIgnoreGwei?: boolean; valueCrypto?: BigNumber; }; @@ -15,8 +16,9 @@ export default function getValueCryptoToDisplay({ cryptoCurrency, isUsingHairSpace = true, valueCrypto = BigNumber.from(0), + shouldIgnoreGwei, }: ValueCryptoToDisplay): string { return cryptoCurrency === 'ethereum' - ? getFormattedEthValue(valueCrypto, isUsingHairSpace).fullString + ? getFormattedEthValue(valueCrypto, isUsingHairSpace, shouldIgnoreGwei).fullString : getFormattedGlmValue(valueCrypto, isUsingHairSpace).fullString; } From 695b8ed14b4625d669125fabbd19a33eba7ce2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Fri, 10 Nov 2023 09:36:17 +0000 Subject: [PATCH 34/35] OCT-1039 useLockedSummarySnapshots query for cumulative glm locked graph --- client/src/api/queryKeys/index.ts | 1 + client/src/api/queryKeys/types.ts | 1 + .../MetricsCumulativeGlmLocked.tsx | 4 +- client/src/gql/gql.ts | 8 + client/src/gql/graphql.ts | 74 ++++ .../subgraph/useLockedSummarySnapshots.ts | 78 +++++ client/src/hooks/subgraph/useLockedsData.ts | 41 +-- .../getMetricsChartDataGroupedByDate.test.ts | 319 ++++++++++++++++++ .../utils/getMetricsChartDataGroupedByDate.ts | 53 +++ 9 files changed, 543 insertions(+), 36 deletions(-) create mode 100644 client/src/hooks/subgraph/useLockedSummarySnapshots.ts create mode 100644 client/src/utils/getMetricsChartDataGroupedByDate.test.ts create mode 100644 client/src/utils/getMetricsChartDataGroupedByDate.ts diff --git a/client/src/api/queryKeys/index.ts b/client/src/api/queryKeys/index.ts index bea7ae1305..422aa16986 100644 --- a/client/src/api/queryKeys/index.ts +++ b/client/src/api/queryKeys/index.ts @@ -36,6 +36,7 @@ export const QUERY_KEYS: QueryKeys = { isDecisionWindowOpen: ['isDecisionWindowOpen'], largestLockedAmount: ['largestLockedAmount'], lockedSummaryLatest: ['lockedSummaryLatest'], + lockedSummarySnapshots: ['lockedSummarySnapshots'], matchedProposalRewards: epochNumber => [ROOTS.matchedProposalRewards, epochNumber.toString()], patronMode: userAddress => [ROOTS.patronMode, userAddress], proposalDonors: (proposalAddress, epochNumber) => [ diff --git a/client/src/api/queryKeys/types.ts b/client/src/api/queryKeys/types.ts index 49c5c10d31..40f50b60a6 100644 --- a/client/src/api/queryKeys/types.ts +++ b/client/src/api/queryKeys/types.ts @@ -38,6 +38,7 @@ export type QueryKeys = { isDecisionWindowOpen: ['isDecisionWindowOpen']; largestLockedAmount: ['largestLockedAmount']; lockedSummaryLatest: ['lockedSummaryLatest']; + lockedSummarySnapshots: ['lockedSummarySnapshots']; matchedProposalRewards: (epochNumber: number) => [Root['matchedProposalRewards'], string]; patronMode: (userAddress: string) => [Root['patronMode'], string]; proposalDonors: ( diff --git a/client/src/components/Metrics/MetricsGrid/MetricsCumulativeGlmLocked/MetricsCumulativeGlmLocked.tsx b/client/src/components/Metrics/MetricsGrid/MetricsCumulativeGlmLocked/MetricsCumulativeGlmLocked.tsx index ab22a1979c..d9b3d65f05 100644 --- a/client/src/components/Metrics/MetricsGrid/MetricsCumulativeGlmLocked/MetricsCumulativeGlmLocked.tsx +++ b/client/src/components/Metrics/MetricsGrid/MetricsCumulativeGlmLocked/MetricsCumulativeGlmLocked.tsx @@ -5,13 +5,13 @@ import { useTranslation } from 'react-i18next'; import AreaChart from 'components/core/AreaChart/AreaChart'; import ChartTimeSlicer from 'components/Metrics/MetricsGrid/common/ChartTimeSlicer/ChartTimeSlicer'; import MetricsGridTile from 'components/Metrics/MetricsGrid/common/MetricsGridTile/MetricsGridTile'; -import useLockedsData from 'hooks/subgraph/useLockedsData'; +import useLockedSummarySnapshots from 'hooks/subgraph/useLockedSummarySnapshots'; import styles from './MetricsCumulativeGlmLocked.module.scss'; const MetricsCumulativeGlmLocked: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'views.metrics' }); - const { data } = useLockedsData(); + const { data } = useLockedSummarySnapshots(); const [dateToFilter, setDateToFilter] = useState(null); diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index d145d0c9ca..c622be9dcd 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -23,6 +23,8 @@ const documents = { types.GetLargestLockedAmountDocument, '\n query GetLockedSummaryLatest {\n lockedSummaryLatest(id: "latest") {\n id\n lockedTotal\n lockedRatio\n }\n }\n': types.GetLockedSummaryLatestDocument, + '\n query GetLockedSummarySnapshots($first: Int = 1000, $skip: Int = 0) {\n lockedSummarySnapshots(first: $first, skip: $skip, orderBy: timestamp) {\n lockedTotal\n timestamp\n }\n }\n': + types.GetLockedSummarySnapshotsDocument, '\n query GetLockedsData($first: Int = 100, $skip: Int = 0) {\n lockeds(first: $first, skip: $skip) {\n user\n timestamp\n amount\n }\n }\n': types.GetLockedsDataDocument, }; @@ -71,6 +73,12 @@ export function graphql( export function graphql( source: '\n query GetLockedSummaryLatest {\n lockedSummaryLatest(id: "latest") {\n id\n lockedTotal\n lockedRatio\n }\n }\n', ): (typeof documents)['\n query GetLockedSummaryLatest {\n lockedSummaryLatest(id: "latest") {\n id\n lockedTotal\n lockedRatio\n }\n }\n']; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: '\n query GetLockedSummarySnapshots($first: Int = 1000, $skip: Int = 0) {\n lockedSummarySnapshots(first: $first, skip: $skip, orderBy: timestamp) {\n lockedTotal\n timestamp\n }\n }\n', +): (typeof documents)['\n query GetLockedSummarySnapshots($first: Int = 1000, $skip: Int = 0) {\n lockedSummarySnapshots(first: $first, skip: $skip, orderBy: timestamp) {\n lockedTotal\n timestamp\n }\n }\n']; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index a2bf2080b9..fa044e2946 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -1012,6 +1012,20 @@ export type GetLockedSummaryLatestQuery = { } | null; }; +export type GetLockedSummarySnapshotsQueryVariables = Exact<{ + first?: InputMaybe; + skip?: InputMaybe; +}>; + +export type GetLockedSummarySnapshotsQuery = { + __typename?: 'Query'; + lockedSummarySnapshots: Array<{ + __typename?: 'LockedSummarySnapshot'; + lockedTotal: any; + timestamp: number; + }>; +}; + export type GetLockedsDataQueryVariables = Exact<{ first?: InputMaybe; skip?: InputMaybe; @@ -1224,6 +1238,66 @@ export const GetLockedSummaryLatestDocument = { }, ], } as unknown as DocumentNode; +export const GetLockedSummarySnapshotsDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'GetLockedSummarySnapshots' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, + type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, + defaultValue: { kind: 'IntValue', value: '1000' }, + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'skip' } }, + type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, + defaultValue: { kind: 'IntValue', value: '0' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'lockedSummarySnapshots' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'first' }, + value: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, + }, + { + kind: 'Argument', + name: { kind: 'Name', value: 'skip' }, + value: { kind: 'Variable', name: { kind: 'Name', value: 'skip' } }, + }, + { + kind: 'Argument', + name: { kind: 'Name', value: 'orderBy' }, + value: { kind: 'EnumValue', value: 'timestamp' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'lockedTotal' } }, + { kind: 'Field', name: { kind: 'Name', value: 'timestamp' } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + GetLockedSummarySnapshotsQuery, + GetLockedSummarySnapshotsQueryVariables +>; export const GetLockedsDataDocument = { kind: 'Document', definitions: [ diff --git a/client/src/hooks/subgraph/useLockedSummarySnapshots.ts b/client/src/hooks/subgraph/useLockedSummarySnapshots.ts new file mode 100644 index 0000000000..d42760bcaf --- /dev/null +++ b/client/src/hooks/subgraph/useLockedSummarySnapshots.ts @@ -0,0 +1,78 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import request from 'graphql-request'; + +import { QUERY_KEYS } from 'api/queryKeys'; +import env from 'env'; +import { graphql } from 'gql/gql'; +import { GetLockedSummarySnapshotsQuery } from 'gql/graphql'; +import getMetricsChartDataGroupedByDate, { + GroupedGlmAmountByDateItem, +} from 'utils/getMetricsChartDataGroupedByDate'; + +const GET_LOCKED_SUMMARY_SNAPSHOTS = graphql(` + query GetLockedSummarySnapshots($first: Int = 1000, $skip: Int = 0) { + lockedSummarySnapshots(first: $first, skip: $skip, orderBy: timestamp) { + lockedTotal + timestamp + } + } +`); + +type GroupedByDateItem = { + cummulativeGlmAmount: number; + dateTime: number; +}; + +type GroupedByDate = GroupedByDateItem[]; + +type UseLockedSummarySnapshotsResponse = + | { + groupedByDate: GroupedByDate; + } + | undefined; + +export default function useLockedSummarySnapshots(): UseQueryResult { + const { subgraphAddress } = env; + + return useQuery( + QUERY_KEYS.lockedSummarySnapshots, + async () => { + const pageSize = 1000; + const lockedSummarySnapshots: GetLockedSummarySnapshotsQuery['lockedSummarySnapshots'] = []; + + const fetchPage = async (first: number) => { + const data = await request(subgraphAddress, GET_LOCKED_SUMMARY_SNAPSHOTS, { + first, + skip: first - pageSize, + }); + + lockedSummarySnapshots.push(...data.lockedSummarySnapshots); + + if (data.lockedSummarySnapshots.length >= pageSize) { + await fetchPage(first + pageSize); + } + }; + + await fetchPage(pageSize); + + return { lockedSummarySnapshots }; + }, + { + refetchOnMount: false, + select: data => { + if (!data?.lockedSummarySnapshots) { + return undefined; + } + + const groupedByDate = getMetricsChartDataGroupedByDate( + data.lockedSummarySnapshots, + 'lockedSummarySnapshots', + ) as GroupedGlmAmountByDateItem[]; + + return { + groupedByDate, + }; + }, + }, + ); +} diff --git a/client/src/hooks/subgraph/useLockedsData.ts b/client/src/hooks/subgraph/useLockedsData.ts index c20f3610eb..0620fcd22f 100644 --- a/client/src/hooks/subgraph/useLockedsData.ts +++ b/client/src/hooks/subgraph/useLockedsData.ts @@ -1,14 +1,13 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { getTime, startOfDay } from 'date-fns'; -import { formatUnits, parseUnits } from 'ethers/lib/utils'; import request from 'graphql-request'; -import sortBy from 'lodash/sortBy'; -import uniq from 'lodash/uniq'; import { QUERY_KEYS } from 'api/queryKeys'; import env from 'env'; import { graphql } from 'gql/gql'; import { GetLockedsDataQuery } from 'gql/graphql'; +import getMetricsChartDataGroupedByDate, { + GroupedUsersByDateItem, +} from 'utils/getMetricsChartDataGroupedByDate'; const GET_LOCKEDS_DATA = graphql(` query GetLockedsData($first: Int = 100, $skip: Int = 0) { @@ -21,7 +20,6 @@ const GET_LOCKEDS_DATA = graphql(` `); type GroupedByDateItem = { - cummulativeGlmAmount: number; dateTime: number; users: `0x${string}`[]; }; @@ -68,35 +66,10 @@ export default function useLockedsData(): UseQueryResult return undefined; } - const groupedByDate = sortBy(data.lockeds, l => l.timestamp).reduce( - (acc, curr) => { - // formatting from WEI to GLM (int) - const glmAmount = parseFloat(formatUnits(parseUnits(curr.amount, 'wei'))); - - // grouping by start of day in user's timezone - const dateTime = getTime(startOfDay(curr.timestamp * 1000)); - - const idx = acc.findIndex(v => v.dateTime === dateTime); - if (idx < 0) { - acc.push({ - cummulativeGlmAmount: - acc.length > 0 ? acc[acc.length - 1].cummulativeGlmAmount + glmAmount : glmAmount, - dateTime, - users: - acc.length > 0 ? uniq([...acc[acc.length - 1].users, curr.user]) : [curr.user], - }); - return acc; - } - - // eslint-disable-next-line operator-assignment - acc[idx].users = uniq([...acc[idx].users, curr.user]); - // eslint-disable-next-line operator-assignment - acc[idx].cummulativeGlmAmount = acc[idx].cummulativeGlmAmount + glmAmount; - - return acc; - }, - [], - ); + const groupedByDate = getMetricsChartDataGroupedByDate( + data.lockeds, + 'lockeds', + ) as GroupedUsersByDateItem[]; const totalAddressesWithoutDuplicates = groupedByDate[groupedByDate.length - 1].users.length; diff --git a/client/src/utils/getMetricsChartDataGroupedByDate.test.ts b/client/src/utils/getMetricsChartDataGroupedByDate.test.ts new file mode 100644 index 0000000000..456b2d8b35 --- /dev/null +++ b/client/src/utils/getMetricsChartDataGroupedByDate.test.ts @@ -0,0 +1,319 @@ +import getMetricsChartDataGroupedByDate from './getMetricsChartDataGroupedByDate'; + +const testCases = [ + { + dataType: 'lockeds', + inputData: [ + { + amount: '100000000000000000000', + timestamp: 1691520096, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '250000000000000000000', + timestamp: 1692078408, + user: '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + }, + { + amount: '2000000000000000000000', + timestamp: 1691167452, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '100000000000000000000', + timestamp: 1691520072, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '12000000000000000000', + timestamp: 1693885908, + user: '0x2b50777bf5657cee8d11b41a906a77387b34be84', + }, + { + amount: '100000000000000000000', + timestamp: 1691518800, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '111000000000000000000', + timestamp: 1691513784, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '30000000000000000000', + timestamp: 1694063148, + user: '0x2b50777bf5657cee8d11b41a906a77387b34be84', + }, + { + amount: '1000000000000000000000', + timestamp: 1691414352, + user: '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + }, + { + amount: '400000000000000000000', + timestamp: 1693885272, + user: '0x2b50777bf5657cee8d11b41a906a77387b34be84', + }, + { + amount: '3000000000000000000000', + timestamp: 1691414196, + user: '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + }, + { + amount: '4000000000000000000000', + timestamp: 1691411496, + user: '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + }, + { + amount: '2000000000000000000000', + timestamp: 1691413836, + user: '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + }, + { + amount: '666000000000000000000', + timestamp: 1691416956, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '5000000000000000000', + timestamp: 1694064276, + user: '0x2b50777bf5657cee8d11b41a906a77387b34be84', + }, + { + amount: '3000000000000000000000', + timestamp: 1691517348, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '111000000000000000000', + timestamp: 1691513256, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '100000000000000000000000000', + timestamp: 1691334144, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '100000000000000000000', + timestamp: 1691520060, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '100000000000000000000000000', + timestamp: 1691421540, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '222000000000000000000', + timestamp: 1691514468, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '555000000000000000000', + timestamp: 1692353928, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '1000000000000000000000', + timestamp: 1695213252, + user: '0x26fa7fc3d3801d039ca313e759a55088e19efd1b', + }, + { + amount: '123000000000000000000', + timestamp: 1692622800, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '2000000000000000000000', + timestamp: 1692158796, + user: '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + }, + { + amount: '3000000000000000000000', + timestamp: 1691517276, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '1000000000000000000000', + timestamp: 1695205176, + user: '0x26fa7fc3d3801d039ca313e759a55088e19efd1b', + }, + { + amount: '100000000000000000000', + timestamp: 1693947888, + user: '0x1078daa844cdf1edb51e5189c8b113b80a6a6957', + }, + ], + outputData: [ + { dateTime: 1691107200000, users: ['0xfc9527820a76b515a2c66c22e0575501dedd8281'] }, + { dateTime: 1691280000000, users: ['0xfc9527820a76b515a2c66c22e0575501dedd8281'] }, + { + dateTime: 1691366400000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + ], + }, + { + dateTime: 1691452800000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + ], + }, + { + dateTime: 1692057600000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + ], + }, + { + dateTime: 1692144000000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + ], + }, + { + dateTime: 1692316800000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + ], + }, + { + dateTime: 1692576000000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + ], + }, + { + dateTime: 1693872000000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + '0x2b50777bf5657cee8d11b41a906a77387b34be84', + '0x1078daa844cdf1edb51e5189c8b113b80a6a6957', + ], + }, + { + dateTime: 1694044800000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + '0x2b50777bf5657cee8d11b41a906a77387b34be84', + '0x1078daa844cdf1edb51e5189c8b113b80a6a6957', + ], + }, + { + dateTime: 1695168000000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + '0x2b50777bf5657cee8d11b41a906a77387b34be84', + '0x1078daa844cdf1edb51e5189c8b113b80a6a6957', + '0x26fa7fc3d3801d039ca313e759a55088e19efd1b', + ], + }, + ], + }, + { + dataType: 'lockedSummarySnapshots', + inputData: [ + { lockedTotal: '2000000000000000000000', timestamp: 1691167452 }, + { lockedTotal: '100002000000000000000000000', timestamp: 1691334144 }, + { lockedTotal: '0', timestamp: 1691334252 }, + { lockedTotal: '4000000000000000000000', timestamp: 1691411496 }, + { lockedTotal: '2000000000000000000000', timestamp: 1691412612 }, + { lockedTotal: '4000000000000000000000', timestamp: 1691413836 }, + { lockedTotal: '1000000000000000000000', timestamp: 1691414040 }, + { lockedTotal: '4000000000000000000000', timestamp: 1691414196 }, + { lockedTotal: '3000000000000000000000', timestamp: 1691414280 }, + { lockedTotal: '4000000000000000000000', timestamp: 1691414352 }, + { lockedTotal: '4666000000000000000000', timestamp: 1691416956 }, + { lockedTotal: '100004666000000000000000000', timestamp: 1691421540 }, + { lockedTotal: '1004666000000000000000000', timestamp: 1691512452 }, + { lockedTotal: '1004777000000000000000000', timestamp: 1691513256 }, + { lockedTotal: '1004888000000000000000000', timestamp: 1691513784 }, + { lockedTotal: '1004666000000000000000000', timestamp: 1691514312 }, + { lockedTotal: '1004888000000000000000000', timestamp: 1691514468 }, + { lockedTotal: '1004777000000000000000000', timestamp: 1691514504 }, + { lockedTotal: '1007777000000000000000000', timestamp: 1691517276 }, + { lockedTotal: '1010777000000000000000000', timestamp: 1691517348 }, + { lockedTotal: '1010877000000000000000000', timestamp: 1691518800 }, + { lockedTotal: '1010977000000000000000000', timestamp: 1691520060 }, + { lockedTotal: '1011077000000000000000000', timestamp: 1691520072 }, + { lockedTotal: '1011177000000000000000000', timestamp: 1691520096 }, + { lockedTotal: '1011427000000000000000000', timestamp: 1692078408 }, + { lockedTotal: '1013427000000000000000000', timestamp: 1692158796 }, + { lockedTotal: '1012761000000000000000000', timestamp: 1692353808 }, + { lockedTotal: '1013316000000000000000000', timestamp: 1692353928 }, + { lockedTotal: '1013439000000000000000000', timestamp: 1692622800 }, + { lockedTotal: '1013839000000000000000000', timestamp: 1693885272 }, + { lockedTotal: '1013851000000000000000000', timestamp: 1693885908 }, + { lockedTotal: '1013951000000000000000000', timestamp: 1693947888 }, + { lockedTotal: '1013981000000000000000000', timestamp: 1694063148 }, + { lockedTotal: '1013986000000000000000000', timestamp: 1694064276 }, + { lockedTotal: '1014986000000000000000000', timestamp: 1695205176 }, + { lockedTotal: '1013986000000000000000000', timestamp: 1695206520 }, + { lockedTotal: '1014986000000000000000000', timestamp: 1695213252 }, + { lockedTotal: '1013986000000000000000000', timestamp: 1695213408 }, + { lockedTotal: '1013539000000000000000000', timestamp: 1696810860 }, + { lockedTotal: '1013289000000000000000000', timestamp: 1696903644 }, + ], + outputData: [ + { cummulativeGlmAmount: 2000, dateTime: 1691107200000 }, + { cummulativeGlmAmount: 0, dateTime: 1691280000000 }, + { cummulativeGlmAmount: 100004666, dateTime: 1691366400000 }, + { cummulativeGlmAmount: 1011177, dateTime: 1691452800000 }, + { cummulativeGlmAmount: 1011427, dateTime: 1692057600000 }, + { cummulativeGlmAmount: 1013427, dateTime: 1692144000000 }, + { cummulativeGlmAmount: 1013316, dateTime: 1692316800000 }, + { cummulativeGlmAmount: 1013439, dateTime: 1692576000000 }, + { cummulativeGlmAmount: 1013951, dateTime: 1693872000000 }, + { cummulativeGlmAmount: 1013986, dateTime: 1694044800000 }, + { cummulativeGlmAmount: 1013986, dateTime: 1695168000000 }, + { cummulativeGlmAmount: 1013539, dateTime: 1696809600000 }, + { cummulativeGlmAmount: 1013289, dateTime: 1696896000000 }, + ], + }, +]; + +describe('getMetricsChartDataGroupedByDate', () => { + for (const { dataType, inputData, outputData } of testCases) { + it(`returns ${JSON.stringify( + outputData, + )} for ${dataType} dataType and inputData: ${JSON.stringify(inputData)}`, () => { + expect( + getMetricsChartDataGroupedByDate( + inputData, + dataType as 'lockedSummarySnapshots' | 'lockeds', + ), + ).toStrictEqual(outputData); + }); + } +}); diff --git a/client/src/utils/getMetricsChartDataGroupedByDate.ts b/client/src/utils/getMetricsChartDataGroupedByDate.ts new file mode 100644 index 0000000000..f03e15b83b --- /dev/null +++ b/client/src/utils/getMetricsChartDataGroupedByDate.ts @@ -0,0 +1,53 @@ +import { getTime, startOfDay } from 'date-fns'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; +import { sortBy, uniq } from 'lodash'; + +export type GroupedGlmAmountByDateItem = { + cummulativeGlmAmount: number; + dateTime: number; +}; + +export type GroupedUsersByDateItem = { + dateTime: number; + users: `0x${string}`[]; +}; + +type GroupedByDate = GroupedGlmAmountByDateItem[] | GroupedUsersByDateItem[]; + +const getMetricsChartDataGroupedByDate = ( + data: any[], + dataType: 'lockeds' | 'lockedSummarySnapshots', +): GroupedByDate => + sortBy(data, l => l.timestamp).reduce((acc, curr) => { + // grouping by start of day in user's timezone + const dateTime = getTime(startOfDay(curr.timestamp * 1000)); + + const idx = acc.findIndex(v => v.dateTime === dateTime); + if (idx < 0) { + acc.push({ + dateTime, + ...(dataType === 'lockeds' + ? { + users: acc.length > 0 ? uniq([...acc[acc.length - 1].users, curr.user]) : [curr.user], + } + : { + // formatting from WEI to GLM (int) + cummulativeGlmAmount: parseFloat(formatUnits(parseUnits(curr.lockedTotal, 'wei'))), + }), + }); + return acc; + } + + if (dataType === 'lockeds') { + // eslint-disable-next-line operator-assignment + acc[idx].users = uniq([...acc[idx].users, curr.user]); + } else { + // formatting from WEI to GLM (int) + // eslint-disable-next-line operator-assignment + acc[idx].cummulativeGlmAmount = parseFloat(formatUnits(parseUnits(curr.lockedTotal, 'wei'))); + } + + return acc; + }, []); + +export default getMetricsChartDataGroupedByDate; From a8050231c42f35973d99df6e1f5a5f361e623deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Mon, 13 Nov 2023 11:26:10 +0000 Subject: [PATCH 35/35] OCT-846 Implement skeletons to handle IPFS fetching error --- client/src/api/errorMessages/index.ts | 6 +++++- client/src/api/errorMessages/types.ts | 6 +++++- .../AllocationItem/AllocationItem.tsx | 8 +------- .../HistoryItemDateAndTime/utils.ts | 3 ++- .../ProposalLoadingStates.tsx | 20 ------------------- .../dedicated/ProposalLoadingStates/types.ts | 7 ------- .../ProposalsListItem/ProposalsListItem.tsx | 5 +---- client/src/hooks/queries/useProposalsIpfs.ts | 16 +++++++++++++++ client/src/locales/en/translation.json | 6 +++--- 9 files changed, 33 insertions(+), 44 deletions(-) delete mode 100644 client/src/components/dedicated/ProposalLoadingStates/ProposalLoadingStates.tsx delete mode 100644 client/src/components/dedicated/ProposalLoadingStates/types.ts diff --git a/client/src/api/errorMessages/index.ts b/client/src/api/errorMessages/index.ts index 3e9473d4e3..e2b6957750 100644 --- a/client/src/api/errorMessages/index.ts +++ b/client/src/api/errorMessages/index.ts @@ -7,7 +7,11 @@ import triggerToast from 'utils/triggerToast'; import { QueryMutationError, QueryMutationErrorConfig, IgnoredQueries } from './types'; -const IGNORED_QUERIES: IgnoredQueries = [ROOTS.cryptoValues, QUERY_KEYS.glmClaimCheck[0]]; +const IGNORED_QUERIES: IgnoredQueries = [ + ROOTS.cryptoValues, + ROOTS.proposalsIpfsResults, + QUERY_KEYS.glmClaimCheck[0], +]; const errors: QueryMutationErrorConfig = { 4001: { diff --git a/client/src/api/errorMessages/types.ts b/client/src/api/errorMessages/types.ts index bedf5c5833..cb3eff7f0b 100644 --- a/client/src/api/errorMessages/types.ts +++ b/client/src/api/errorMessages/types.ts @@ -10,4 +10,8 @@ export type QueryMutationErrorConfig = { [key: string]: QueryMutationError; }; -export type IgnoredQueries = [Root['cryptoValues'], QueryKeys['glmClaimCheck'][0]]; +export type IgnoredQueries = [ + Root['cryptoValues'], + Root['proposalsIpfsResults'], + QueryKeys['glmClaimCheck'][0], +]; diff --git a/client/src/components/dedicated/AllocationItem/AllocationItem.tsx b/client/src/components/dedicated/AllocationItem/AllocationItem.tsx index 58e9447925..3d3eba6639 100644 --- a/client/src/components/dedicated/AllocationItem/AllocationItem.tsx +++ b/client/src/components/dedicated/AllocationItem/AllocationItem.tsx @@ -6,7 +6,6 @@ import BoxRounded from 'components/core/BoxRounded/BoxRounded'; import Img from 'components/core/Img/Img'; import Svg from 'components/core/Svg/Svg'; import AllocationItemSkeleton from 'components/dedicated/AllocationItem/AllocationItemSkeleton/AllocationItemSkeleton'; -import ProposalLoadingStates from 'components/dedicated/ProposalLoadingStates/ProposalLoadingStates'; import env from 'env'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useProposalRewardsThreshold from 'hooks/queries/useProposalRewardsThreshold'; @@ -53,12 +52,7 @@ const AllocationItem: FC = ({ className={cx(styles.root, className)} onClick={isConnected && !isDisabled ? () => onSelectItem(address) : undefined} > - {(isLoading || isLoadingError) && ( - - - - )} - + {(isLoading || isLoadingError) && } {!isLoading && !isLoadingError && ( {(isAllocatedTo || isManuallyEdited) && ( diff --git a/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDateAndTime/utils.ts b/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDateAndTime/utils.ts index ef90b0ad1d..4ac5123945 100644 --- a/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDateAndTime/utils.ts +++ b/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDateAndTime/utils.ts @@ -1,5 +1,6 @@ import format from 'date-fns/format'; export function getHistoryItemDateAndTime(timestamp: string): string { - return format(parseInt(timestamp, 10) / 1000, 'h:mmaaa, dd MMM yyyy'); + // Timestamp from subgraph is in microseconds, needs to be changed to milliseconds. + return format(Math.floor(parseInt(timestamp, 10) / 1000), 'h:mmaaa, dd MMM yyyy'); } diff --git a/client/src/components/dedicated/ProposalLoadingStates/ProposalLoadingStates.tsx b/client/src/components/dedicated/ProposalLoadingStates/ProposalLoadingStates.tsx deleted file mode 100644 index 03266bbc84..0000000000 --- a/client/src/components/dedicated/ProposalLoadingStates/ProposalLoadingStates.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { FC } from 'react'; -import { Trans } from 'react-i18next'; - -import ProposalLoadingStatesProps from './types'; - -const ProposalLoadingStates: FC = ({ - isLoadingError, - isLoading, - children, -}) => { - if (isLoadingError) { - return ; - } - if (isLoading) { - return children; - } - return null; -}; - -export default ProposalLoadingStates; diff --git a/client/src/components/dedicated/ProposalLoadingStates/types.ts b/client/src/components/dedicated/ProposalLoadingStates/types.ts deleted file mode 100644 index 17b7dd86f5..0000000000 --- a/client/src/components/dedicated/ProposalLoadingStates/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ReactNode } from 'react'; - -export default interface ProposalLoadingStatesProps { - children: ReactNode; - isLoading?: boolean; - isLoadingError: boolean; -} diff --git a/client/src/components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem.tsx b/client/src/components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem.tsx index 647e817f70..f130a049d4 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem.tsx +++ b/client/src/components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem.tsx @@ -5,7 +5,6 @@ import { useNavigate } from 'react-router-dom'; import Description from 'components/core/Description/Description'; import Img from 'components/core/Img/Img'; import ButtonAddToAllocate from 'components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate'; -import ProposalLoadingStates from 'components/dedicated/ProposalLoadingStates/ProposalLoadingStates'; import ProposalRewards from 'components/dedicated/ProposalRewards/ProposalRewards'; import ProposalItemSkeleton from 'components/dedicated/ProposalsList/ProposalsListItemSkeleton/ProposalsListItemSkeleton'; import env from 'env'; @@ -66,9 +65,7 @@ const ProposalsListItem: FC = ({ } > {isLoadingError ? ( - - - + ) : (
diff --git a/client/src/hooks/queries/useProposalsIpfs.ts b/client/src/hooks/queries/useProposalsIpfs.ts index a38e7953ae..df9cd52cae 100644 --- a/client/src/hooks/queries/useProposalsIpfs.ts +++ b/client/src/hooks/queries/useProposalsIpfs.ts @@ -1,9 +1,12 @@ import { useQueries, UseQueryResult } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { apiGetProposal } from 'api/calls/proposals'; import { QUERY_KEYS } from 'api/queryKeys'; import { ExtendedProposal } from 'types/extended-proposal'; import { BackendProposal } from 'types/gen/backendproposal'; +import triggerToast from 'utils/triggerToast'; import useProposalsCid from './useProposalsCid'; import useProposalsContract from './useProposalsContract'; @@ -13,6 +16,7 @@ export default function useProposalsIpfs(proposalsAddresses?: string[]): { isFetching: boolean; refetch: () => void; } { + const { t } = useTranslation('translation', { keyPrefix: 'api.errorMessage' }); const { data: proposalsCid, isFetching: isFetchingProposalsCid } = useProposalsCid(); const { refetch } = useProposalsContract(); @@ -21,9 +25,21 @@ export default function useProposalsIpfs(proposalsAddresses?: string[]): { enabled: !!address && !!proposalsCid, queryFn: () => apiGetProposal(`${proposalsCid}/${address}`), queryKey: QUERY_KEYS.proposalsIpfsResults(address), + retry: false, })), }); + const isAnyError = proposalsIpfsResults.some(element => element.isError); + useEffect(() => { + if (!isAnyError) { + return; + } + triggerToast({ + message: t('ipfs.message'), + type: 'error', + }); + }, [isAnyError, t]); + const isProposalsIpfsResultsFetching = isFetchingProposalsCid || proposalsIpfsResults.length === 0 || diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 283e679f25..3fecbeaa27 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -17,6 +17,9 @@ "default": { "title": "Sorry, something went wrong there", "message": "Please reload the app and try again" + }, + "ipfs": { + "message": "We seem to be having trouble loading data from IPFS, please hang on or try reloading" } } }, @@ -218,9 +221,6 @@ "modalWithdrawEth": { "withdrawETH": "Withdraw ETH" }, - "proposalLoadingStates": { - "text": "Loading of a proposal
encountered an error." - }, "proposalRewards": { "currentTotal": "Current total", "totalRaised": "Total raised",