Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fellowship tasks basket #3121

Merged
merged 6 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ module.exports = {
// TODO error
'@typescript-eslint/consistent-type-assertions': ['off', { assertionStyle: 'never' }],

'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-explicit-any': 'warn',

'no-restricted-syntax': [
Expand Down
4 changes: 3 additions & 1 deletion eslint/rules/enforce-di-naming-convention.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const IDENTIFIERS_SUFFIXES = {
createAnyOf: 'AnyOf',
};

const DEFAULT_IMPORT_SOURCES = [/@\/shared\/di/];

const fixName = (name, suffix) => camelCase(name.replace(new RegExp(suffix, 'gi'), '').replace(/\$/g, '') + suffix);

/**
Expand Down Expand Up @@ -57,7 +59,7 @@ module.exports = {
...IDENTIFIERS_SUFFIXES,
...(settings.identifierCreators || {}),
};
const importSources = [/@\/shared\/di/, ...(settings.importSources || [])];
const importSources = [...DEFAULT_IMPORT_SOURCES, ...(settings.importSources || [])];

return {
VariableDeclarator(node) {
Expand Down
9 changes: 4 additions & 5 deletions eslint/rules/no-relative-import-from-root.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,11 @@ module.exports = {

return {
ImportDeclaration(node) {
const { source } = node;
if (!isLiteral(source)) {
if (!isLiteral(node.source)) {
return;
}

const requestPath = source.value.toString();
const requestPath = node.source.value.toString();
// Not relative import to parent
if (!requestPath.startsWith('../')) {
return;
Expand All @@ -52,7 +51,7 @@ module.exports = {
if (possibleRoot === absoluteRoot) {
return context.report({
node,
message: `Relative import through root is forbidden.`,
message: `Relative imports through root are forbidden.`,
});
}

Expand All @@ -68,7 +67,7 @@ module.exports = {
if (sourcePackage !== requestedPackage) {
return context.report({
node,
message: `Relative to another package is forbidden.\n${sourcePackage}\n${requestedPackage}`,
message: `Relative imports to another package are forbidden.\n${sourcePackage}\n${requestedPackage}`,
});
}
},
Expand Down
9 changes: 4 additions & 5 deletions eslint/rules/no-self-import.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,11 @@ module.exports = {
return;
}

const { source } = node;
if (!isLiteral(source)) {
if (!isLiteral(node.source)) {
return;
}

const requestPath = source.value.toString();
const requestPath = node.source.value.toString();

// Child import
if (requestPath.startsWith('./')) {
Expand Down Expand Up @@ -132,9 +131,9 @@ module.exports = {
{
desc: `Replace with relative path ${replacedPath}`,
fix(fixer) {
const stringQ = source.raw.charAt(0);
const stringQ = node.source.raw.charAt(0);

return fixer.replaceText(source, `${stringQ}${replacedPath}${stringQ}`);
return fixer.replaceText(node.source, `${stringQ}${replacedPath}${stringQ}`);
},
},
],
Expand Down
50 changes: 45 additions & 5 deletions src/renderer/aggregates/basket-operations/model.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { createEffect, createStore, sample } from 'effector';
import { combine, createEffect, createEvent, createStore, sample } from 'effector';
import { uniq } from 'lodash';
import { readonly } from 'patronum';

import { storageService } from '@/shared/api/storage';
import { type BasketTransaction } from '@/shared/core';
import { type BasketTransaction, type ID } from '@/shared/core';

const $basketTransactions = createStore<BasketTransaction[]>([]);
// list

const $list = createStore<BasketTransaction[]>([]);

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

Expand All @@ -24,7 +27,7 @@ const removeTransactionsFx = createEffect((transactions: BasketTransaction[]): P

sample({
clock: populateFx.doneData,
target: $basketTransactions,
target: $list,
});

sample({
Expand All @@ -42,11 +45,48 @@ sample({
target: populateFx,
});

// select

const $selectedIds = createStore<ID[]>([]);
const $selected = combine($list, $selectedIds, (list, ids) => {
return list.filter(record => ids.includes(record.id));
});

const select = createEvent<ID[]>();
const toggle = createEvent<ID>();
const deselect = createEvent<ID[]>();

sample({
clock: select,
source: $selectedIds,
fn: (selected, toAdd) => uniq(selected.concat(toAdd)),
target: $selectedIds,
});

sample({
clock: toggle,
source: $selectedIds,
fn: (selected, id) => (selected.includes(id) ? selected.filter(x => x !== id) : uniq(selected.concat(id))),
target: $selectedIds,
});

sample({
clock: deselect,
source: $selectedIds,
fn: (selected, toRemove) => selected.filter(s => !toRemove.includes(s)),
target: $selectedIds,
});

export const basketOperations = {
$list: readonly($basketTransactions),
$list: readonly($list),
$selected: readonly($selected),

populate: populateFx,
addTransactions: addTransactionsFx,
updateTransactions: updateTransactionsFx,
removeTransactions: removeTransactionsFx,

select,
toggle,
deselect,
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Props = {
};

export const OperationTitle = ({ title, chainId, className }: Props) => (
<div className={cnTw('flex h-7 flex-1 items-center truncate', className)}>
<div className={cnTw('flex flex-1 items-center truncate', className)}>
<HeaderTitleText>{title}</HeaderTitleText>
<ChainTitle
chainId={chainId}
Expand Down
12 changes: 4 additions & 8 deletions src/renderer/features/app-shell/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { memo } from 'react';

import { createPipeline, usePipeline } from '@/shared/di';
import { createPipeline, createSlot, usePipeline, useSlot } from '@/shared/di';

import { NavItem, type Props as NavItemProps } from './NavItem';

Expand All @@ -9,11 +9,11 @@ export const navigationTopLinksPipeline = createPipeline<NavItemProps[]>({
return items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
},
});
export const navigationBottomLinksPipeline = createPipeline<NavItemProps[]>();
export const navigationBottomLinksSlot = createSlot();

export const Navigation = memo(() => {
const upperItems = usePipeline(navigationTopLinksPipeline, []);
const lowerItems = usePipeline(navigationBottomLinksPipeline, []);
const lowerItems = useSlot(navigationBottomLinksSlot);

return (
<nav className="h-full overflow-y-auto">
Expand All @@ -22,11 +22,7 @@ export const Navigation = memo(() => {
<NavItem key={link} icon={icon} title={title} link={link} badge={badge} />
))}

<div className="mt-auto flex flex-col gap-2">
{lowerItems.map(({ icon, title, link, badge }) => (
<NavItem key={link} icon={icon} title={title} link={link} badge={badge} />
))}
</div>
<div className="mt-auto flex flex-col gap-2">{lowerItems}</div>
</div>
</nav>
);
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/features/app-shell/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { NavItem } from './components/NavItem';
export { AppShell } from './components/AppShell';
export { navigationTopLinksPipeline, navigationBottomLinksPipeline } from './components/Navigation';
export { navigationTopLinksPipeline, navigationBottomLinksSlot } from './components/Navigation';
export { navigationHeaderSlot } from './components/AppShell';
46 changes: 26 additions & 20 deletions src/renderer/features/basket-navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,41 @@ import { useUnit } from 'effector-react';

import { $features } from '@/shared/config/features';
import { createFeature } from '@/shared/feature';
import { useI18n } from '@/shared/i18n';
import { Paths } from '@/shared/routes';
import { BodyText } from '@/shared/ui';
import { walletModel } from '@/entities/wallet';
import { basketUtils } from '@/entities/basket';
import { basketOperations } from '@/aggregates/basket-operations';
import { navigationBottomLinksPipeline } from '@/features/app-shell';
import { basketUtils } from '@/features/operations/OperationsConfirm/lib/basket-utils';
import { walletSelect } from '@/aggregates/wallet-select';
import { NavItem, navigationBottomLinksSlot } from '@/features/app-shell';

export const basketNavigationFeature = createFeature({
name: 'basket/navigation',
enable: $features.map(({ basket }) => basket),
});

basketNavigationFeature.inject(navigationBottomLinksPipeline, (items) => {
const wallet = useUnit(walletModel.$activeWallet);
const basket = useUnit(basketOperations.$list);
basketNavigationFeature.inject(navigationBottomLinksSlot, {
order: 0,
render() {
const { t } = useI18n();
const accounts = useUnit(walletSelect.$selectedAccounts);
const basket = useUnit(basketOperations.$list);

if (!wallet || !basketUtils.isBasketAvailable(wallet)) {
return items;
}
const isAvailable = accounts.some(basketUtils.isBasketAvailableForAccount);

return items.concat({
order: 0,
icon: 'operations',
title: 'navigation.basketLabel',
link: Paths.BASKET,
badge: (
<BodyText className="ml-auto text-text-tertiary">
{basket.filter((tx) => tx.initiatorWallet === wallet?.id).length || ''}
</BodyText>
),
});
if (!isAvailable) {
return null;
}

const availableOperations = basket.filter((tx) => accounts.some((a) => a.walletId === tx.initiatorWallet));

return (
<NavItem
icon="operations"
title={t('navigation.basketLabel')}
link={Paths.BASKET}
badge={<BodyText className="ml-auto text-text-tertiary">{availableOperations.length || ''}</BodyText>}
></NavItem>
);
},
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useGate, useUnit } from 'effector-react';
import { 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 { basketOperations } from '@/aggregates/basket-operations';
import { signOperations } from '../model/sign';

import { EmptyBasket } from './EmptyBasket';
Expand All @@ -21,24 +21,27 @@ export const operationTitleSlot = createSlot<{ operation: BasketTransaction }>()

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

const selectedTxs = useUnit(selectOperations.$selectedTxs);
const selected = useUnit(basketOperations.$selected);

const isSignAvailable = selectedTxs.length > 0;
const isSignAvailable = selected.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)}
checked={operations.length > 0 && operations.length === selected.length}
semiChecked={selected.length > 0 && operations.length !== selected.length}
onChange={(value) => {
value
? basketOperations.select(operations.map((x) => x.id))
: basketOperations.deselect(operations.map((x) => x.id));
}}
>
<FootnoteText className="text-text-secondary">
{t('basket.selectedStatus', { count: operations.length, selected: selectedTxs.length })}
{t('basket.selectedStatus', { count: operations.length, selected: selected.length })}
</FootnoteText>
</Checkbox>
</div>
Expand All @@ -47,9 +50,9 @@ export const BasketOperations = ({ operations }: Props) => {
size="sm"
className="w-[125px]"
disabled={!isSignAvailable}
onClick={() => signOperations.events.flowStarted({ transactions: selectedTxs, feeMap: {} })}
onClick={() => signOperations.events.flowStarted({ transactions: selected, feeMap: {} })}
>
{t(selectedTxs.length === 0 ? 'basket.emptySignButton' : 'basket.signButton')}
{t(selected.length === 0 ? 'basket.emptySignButton' : 'basket.signButton')}
</Button>
</div>
</div>
Expand All @@ -62,12 +65,12 @@ export const BasketOperations = ({ operations }: Props) => {
<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)}
checked={selected.includes(operation)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

selectOperations.selectTx(operation);
basketOperations.toggle(operation.id);
}}
/>
</div>
Expand All @@ -88,7 +91,7 @@ export const BasketOperations = ({ operations }: Props) => {

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

{selectedTxs.length > 1 ? <SignOperations /> : <SignOperation />}
{selected.length > 1 ? <SignOperations /> : <SignOperation />}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export const SignOperation = () => {
const isModalOpen = useUnit(signOperations.$isModalOpen);
const wallet = useUnit(walletSelect.$selectedWallet);

const operation = transactions[0];
const operation = transactions.at(0);

if (!operation) return null;

if (signOperationsUtils.isSubmitStep(step)) {
return <OperationSubmit isOpen={isModalOpen} onClose={signOperations.output.flowFinished} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type BasketTransaction, WalletType } from '@/shared/core';
import { Slot, createSlot } from '@/shared/di';
import { useI18n } from '@/shared/i18n';
import { HeaderTitleText } from '@/shared/ui';
import { Modal } from '@/shared/ui-kit';
import { Box, Modal } from '@/shared/ui-kit';
import { SignButton } from '@/entities/operations';
import { OperationSign, OperationSubmit } from '@/features/operations';
import { ConfirmSlider } from '@/features/operations/OperationsConfirm';
Expand Down Expand Up @@ -41,7 +41,9 @@ export const SignOperations = () => {
>
{transactions.map((t) => (
<ConfirmSlider.Item key={t.id}>
<Slot id={confirmTitleSlot} props={{ operation: t }} />
<Box padding={5}>
<Slot id={confirmTitleSlot} props={{ operation: t }} />
</Box>
<Slot id={confirmDetailsSlot} props={{ operation: t }} />
</ConfirmSlider.Item>
))}
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/features/basket-operations/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { SignOperations } from './components/SignOperations';
export { confirmDetailsSlot, confirmTitleSlot } from './components/SignOperations';
export { BasketOperations, operationTitleSlot } from './components/BasketOperations';
export { basketOperationsFeature } from './model/feature';
export { validate } from './model/validate';
export { signOperations } from './model/sign';
Loading
Loading