Skip to content

Commit

Permalink
feat: basket feature refactoring (#3117)
Browse files Browse the repository at this point in the history
  • Loading branch information
Asmadek authored Feb 12, 2025
1 parent 220b8fb commit 7957b62
Show file tree
Hide file tree
Showing 134 changed files with 4,449 additions and 3,316 deletions.
8 changes: 5 additions & 3 deletions src/renderer/aggregates/basket-operations/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ const $basketTransactions = createStore<BasketTransaction[]>([]);

const populateFx = createEffect(() => storageService.basketTransactions.readAll());

const addTransactionsFx = createEffect(async (transactions: BasketTransaction[]): Promise<BasketTransaction[]> => {
return storageService.basketTransactions.createAll(transactions).then(result => result ?? []);
});
const addTransactionsFx = createEffect(
async (transactions: Omit<BasketTransaction, 'id'>[]): Promise<BasketTransaction[]> => {
return storageService.basketTransactions.createAll(transactions).then(result => result ?? []);
},
);

const updateTransactionsFx = createEffect((transactions: BasketTransaction[]): Promise<number[]> => {
return storageService.basketTransactions.updateAll(transactions).then(result => result ?? []);
Expand Down
26 changes: 24 additions & 2 deletions src/renderer/aggregates/basket-operations/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { type BasketTransaction, type Transaction, TransactionType } from '@/shared/core';
import { findCoreBatchAll, isEditDelegationTransaction } from '@/entities/transaction';
import { type ApiPromise } from '@polkadot/api';

import { type BasketTransaction, type Chain, type ChainId, type Transaction, TransactionType } from '@/shared/core';
import { toAccountId } from '@/shared/lib/utils';
import { type AnyAccount } from '@/domains/network';
import { findCoreBatchAll, isEditDelegationTransaction, transactionService } from '@/entities/transaction';

const getCoreTx = (tx: BasketTransaction): Transaction => {
if (isEditDelegationTransaction(tx.coreTx)) {
Expand All @@ -9,6 +13,24 @@ const getCoreTx = (tx: BasketTransaction): Transaction => {
return tx.coreTx.type === TransactionType.BATCH_ALL ? findCoreBatchAll(tx.coreTx) : tx.coreTx;
};

async function getTransactionData(
transaction: BasketTransaction,
apis: Record<ChainId, ApiPromise>,
chains: Record<ChainId, Chain>,
accounts: AnyAccount[],
) {
const chainId = transaction.coreTx.chainId as ChainId;
const fee = await transactionService.getTransactionFee(transaction.coreTx, apis[chainId]);

const chain = chains[chainId]!;
const account = accounts.find(
a => a.walletId === transaction.initiatorWallet && a.accountId === toAccountId(transaction.coreTx.address),
);

return { chainId, chain, account, fee };
}

export const basketOperationsService = {
getCoreTx,
getTransactionData,
};
13 changes: 13 additions & 0 deletions src/renderer/app/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,31 @@ import { basketOperations } from '@/aggregates/basket-operations';
import { assetsSettingsModel } from '@/features/assets';
import { assetsNavigationFeature } from '@/features/assets-navigation';
import { basketNavigationFeature } from '@/features/basket-navigation';
import { basketOperationsFeature } from '@/features/basket-operations';
import { contactsNavigationFeature } from '@/features/contacts-navigation';
import { extensionWalletFeature } from '@/features/extension-wallet';
import { fellowshipBasketOperationFeature } from '@/features/fellowship-basket-operation';
import { fellowshipEvidenceFeature } from '@/features/fellowship-evidence';
import { fellowshipMembersFeature } from '@/features/fellowship-members';
import { fellowshipNavigationFeature } from '@/features/fellowship-navigation';
import { fellowshipProfileFeature } from '@/features/fellowship-profile';
import { fellowshipSalaryFeature } from '@/features/fellowship-salary';
import { flexibleMultisigNavigationFeature } from '@/features/flexible-multisig-navigation';
import { governanceBasketOperationFeature } from '@/features/governance-basket-operation';
import { governanceNavigationFeature } from '@/features/governance-navigation';
import { governanceOperationDetailFeature } from '@/features/governance-operation-details';
import { importDBFeature } from '@/features/import-db';
import { multisigOperationDetailsFeature } from '@/features/multisig-operation-details';
import { notificationsNavigationFeature } from '@/features/notifications-navigation';
import { operationsNavigationFeature } from '@/features/operations-navigation';
import { proxiesModel } from '@/features/proxies';
import { proxyBasketOperationFeature } from '@/features/proxy-basket-operation';
import { proxyOperationDetailFeature } from '@/features/proxy-operation-details';
import { settingsNavigationFeature } from '@/features/settings-navigation';
import { stakingBasketOperationFeature } from '@/features/staking-basket-operation';
import { stakingNavigationFeature } from '@/features/staking-navigation';
import { stakingOperationDetailFeature } from '@/features/staking-operation-details';
import { transferBasketOperationFeature } from '@/features/transfer-basket-operation';
import { transferOperationDetailFeature } from '@/features/transfer-operation-details';
import { walletDetailsFeature } from '@/features/wallet-details';
import { walletMultisigFeature } from '@/features/wallet-multisig';
Expand Down Expand Up @@ -128,6 +134,13 @@ export const bootstrap = () => {
stakingOperationDetailFeature,
transferOperationDetailFeature,

basketOperationsFeature,
transferBasketOperationFeature,
stakingBasketOperationFeature,
governanceBasketOperationFeature,
proxyBasketOperationFeature,
fellowshipBasketOperationFeature,

importDBFeature,
]);

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
import { render, screen } from '@testing-library/react';
import { vi } from 'vitest';

import { type Transaction, TransactionType } from '@/shared/core';
import { TEST_ADDRESS, TEST_CHAIN_ID } from '@/shared/lib/utils';

import { TransactionTitle } from './TransactionTitle';

vi.mock('@/shared/i18n', () => ({
useI18n: jest.fn().mockReturnValue({
t: (key: string) => key,
}),
}));

const transaction = {
type: TransactionType.TRANSFER,
address: TEST_ADDRESS,
chainId: TEST_CHAIN_ID,
args: {
dest: TEST_ADDRESS,
value: '100000000000',
},
} as Transaction;

describe('pages/Operations/components/TransactionTitle', () => {
test('should render component', () => {
render(<TransactionTitle tx={transaction} />);
render(<TransactionTitle title="operations.titles.transfer" />);

const title = screen.getByText('operations.titles.transfer');
expect(title).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import { type PropsWithChildren } from 'react';

import { type DecodedTransaction, type Transaction } from '@/shared/core';
import { useI18n } from '@/shared/i18n';
import { cnTw } from '@/shared/lib/utils';
import { BodyText, Icon } from '@/shared/ui';
import { getIconName, getTransactionTitle } from '../../lib';
import { BodyText, Icon, type IconNames } from '@/shared/ui';

type Props = {
tx?: Transaction | DecodedTransaction;
title: string;
icon?: IconNames;
className?: string;
};

// TODO decompose
export const TransactionTitle = ({ tx, className, children }: PropsWithChildren<Props>) => {
const { t } = useI18n();

const title = getTransactionTitle(t, tx);

export const TransactionTitle = ({ title, icon, className, children }: PropsWithChildren<Props>) => {
return (
<div className={cnTw('inline-flex items-center gap-x-3', className)}>
<div className="box-content flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-token-container-border">
<Icon name={getIconName(tx)} size={20} />
</div>
{icon && (
<div className="box-content flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-token-container-border">
<Icon name={icon} size={20} />
</div>
)}
<div className="flex flex-col justify-center gap-y-0.5 overflow-hidden">
<div className="flex items-center gap-x-1">
<BodyText className={cnTw('whitespace-nowrap', !children && 'truncate')}>{t(title)}</BodyText>
<BodyText className={cnTw('whitespace-nowrap', !children && 'truncate')}>{title}</BodyText>
{children}
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/features/basket-filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { BasketFilter } from './ui/BasketFilter';
export { filter } from './model/filter';
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type TFunction } from 'i18next';
import { type BasketTransaction, type Chain, TransactionType } from '@/shared/core';
import { type DropdownOption, type DropdownResult } from '@/shared/ui/types';
import { XcmTypes, findCoreBatchAll } from '@/entities/transaction';
import { type SelectedFilters } from '../common/types';
import { type SelectedFilters } from '../model/types';

import { TransferTypes, TxStatus, UNKNOWN_TYPE } from './constants';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ import { combine, createEvent, restore } from 'effector';

import { walletModel } from '@/entities/wallet';
import { basketOperations } from '@/aggregates/basket-operations';
import { type SelectedFilters } from '../common/types';
import { filterTx } from '../lib/utils';

import { type SelectedFilters } from './types';

const EmptySelectedFilters: SelectedFilters = {
status: [],
network: [],
type: [],
};

const selectedOptionsChanged = createEvent<SelectedFilters>();
const invalidTxsSet = createEvent<number[]>();

const $selectedOptions = restore(selectedOptionsChanged, EmptySelectedFilters);
const $invalidTxs = restore(invalidTxsSet, []);

const $basketTxs = combine(
{
Expand All @@ -29,21 +28,17 @@ const $filteredTxs = combine(
{
basketTxs: $basketTxs,
selectedOptions: $selectedOptions,
invalidTxs: $invalidTxs,
},
({ basketTxs, selectedOptions, invalidTxs }) => {
return basketTxs.filter((tx) => filterTx(tx, invalidTxs, selectedOptions));
({ basketTxs, selectedOptions }) => {
return basketTxs.filter((tx) => filterTx(tx, [], selectedOptions));
},
);

export const basketFilterModel = {
export const filter = {
$selectedOptions,
$basketTxs,
$filteredTxs,

events: {
selectedOptionsChanged,
selectedOptionsReset: $selectedOptions.reinit,
invalidTxsSet,
},
selectedOptionsChanged,
selectedOptionsReset: $selectedOptions.reinit,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { useI18n } from '@/shared/i18n';
import { Button, MultiSelect } from '@/shared/ui';
import { type DropdownOption, type DropdownResult } from '@/shared/ui/types';
import { networkModel } from '@/entities/network';
import { type FilterName, type FiltersOptions } from '../common/types';
import { getAvailableFiltersOptions } from '../lib/utils';
import { basketFilterModel } from '../model/baket-filter-model';
import { filter } from '../model/filter';
import { type FilterName, type FiltersOptions } from '../model/types';

const EmptyOptions: FiltersOptions = {
status: new Set<DropdownOption>(),
Expand All @@ -20,8 +20,8 @@ export const BasketFilter = () => {

const [filtersOptions, setFiltersOptions] = useState<FiltersOptions>(EmptyOptions);

const selectedOptions = useUnit(basketFilterModel.$selectedOptions);
const basketTxs = useUnit(basketFilterModel.$basketTxs);
const selectedOptions = useUnit(filter.$selectedOptions);
const basketTxs = useUnit(filter.$basketTxs);
const chains = useStoreMap(networkModel.$chains, (chains) => Object.values(chains));

useEffect(() => {
Expand All @@ -31,11 +31,11 @@ export const BasketFilter = () => {
const handleFilterChange = (values: DropdownResult[], filterName: FilterName) => {
const newSelectedOptions = { ...selectedOptions, [filterName]: values };

basketFilterModel.events.selectedOptionsChanged(newSelectedOptions);
filter.selectedOptionsChanged(newSelectedOptions);
};

const clearFilters = () => {
basketFilterModel.events.selectedOptionsReset();
filter.selectedOptionsReset();
};

const filtersSelected =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useGate, useUnit } from 'effector-react';

import { type BasketTransaction } from '@/shared/core';
import { Slot, createSlot } from '@/shared/di';
import { useI18n } from '@/shared/i18n';
import { Button, FootnoteText } from '@/shared/ui';
import { Checkbox } from '@/shared/ui-kit';
import { selectOperations } from '../model/select';
import { signOperations } from '../model/sign';

import { EmptyBasket } from './EmptyBasket';
import { RemoveOperation } from './RemoveOperation';
import { SignOperation } from './SignOperation';
import { SignOperations } from './SignOperations';

type Props = {
operations: BasketTransaction[];
};

export const operationTitleSlot = createSlot<{ operation: BasketTransaction }>();

export const BasketOperations = ({ operations }: Props) => {
const { t } = useI18n();
useGate(selectOperations.flow);

const selectedTxs = useUnit(selectOperations.$selectedTxs);

const isSignAvailable = selectedTxs.length > 0;

return (
<>
<div className="mt-4 flex w-full flex-col items-center gap-4">
<div className="flex w-[736px] items-center justify-between">
<div className="ml-3">
<Checkbox
checked={operations.length > 0 && operations.length === selectedTxs.length}
semiChecked={selectedTxs.length > 0 && operations.length !== selectedTxs.length}
onChange={() => selectOperations.selectTxs(operations)}
>
<FootnoteText className="text-text-secondary">
{t('basket.selectedStatus', { count: operations.length, selected: selectedTxs.length })}
</FootnoteText>
</Checkbox>
</div>
<div className="flex items-center gap-4">
<Button
size="sm"
className="w-[125px]"
disabled={!isSignAvailable}
onClick={() => signOperations.events.flowStarted({ transactions: selectedTxs, feeMap: {} })}
>
{t(selectedTxs.length === 0 ? 'basket.emptySignButton' : 'basket.signButton')}
</Button>
</div>
</div>
</div>

{operations.length > 0 && (
<div className="scrollbar-stable mt-4 flex w-full flex-col items-center gap-4 overflow-y-auto">
<ul className="flex w-[736px] flex-col gap-y-1.5 divide-y rounded-md">
{operations.map((operation) => (
<li key={operation.id} className="flex gap-x-4 bg-block-background-default px-3">
<div className="flex items-center justify-center">
<Checkbox
checked={selectedTxs.includes(operation)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

selectOperations.selectTx(operation);
}}
/>
</div>

<div
className="flex h-[52px] w-full items-center gap-x-4 overflow-hidden"
onClick={() => signOperations.events.flowStarted({ transactions: [operation], feeMap: {} })}
>
<Slot id={operationTitleSlot} props={{ operation }} />
</div>

<RemoveOperation operation={operation} />
</li>
))}
</ul>
</div>
)}

{operations.length === 0 && <EmptyBasket />}

{selectedTxs.length > 1 ? <SignOperations /> : <SignOperation />}
</>
);
};
Loading

0 comments on commit 7957b62

Please sign in to comment.