chore: add @conarti/eslint-plugin-feature-sliced for FSD layer enforcement
This commit is contained in:
parent
ad196164ee
commit
2744608d06
@ -7,7 +7,7 @@ module.exports = {
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'react', 'react-hooks', 'import'],
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'react', 'react-hooks', 'import', '@conarti/feature-sliced'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
@ -33,7 +33,18 @@ module.exports = {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
|
||||
// FSD layer boundaries
|
||||
// FSD layer boundaries (from @conarti/eslint-plugin-feature-sliced)
|
||||
// layers-slices: catches cross-layer violations (e.g., shared→entities)
|
||||
'@conarti/feature-sliced/layers-slices': ['error', {
|
||||
// allow test files and test utilities to import from any layer for mocking
|
||||
ignoreInFilesPatterns: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx', '**/test/**'],
|
||||
}],
|
||||
// absolute-relative: false positives with @/ alias convention — disabled
|
||||
'@conarti/feature-sliced/absolute-relative': 'off',
|
||||
// public-api: too strict for app/ and test internals — disabled
|
||||
'@conarti/feature-sliced/public-api': 'off',
|
||||
|
||||
// FSD layer boundaries (from import/no-restricted-paths)
|
||||
// NOTE: `from` = what's being imported, `target` = the file doing the import
|
||||
'import/no-restricted-paths': [
|
||||
'error',
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
"react-router-dom": "^6.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@conarti/eslint-plugin-feature-sliced": "^1.0.5",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
BROKER_OPERATION_TYPE_OPTIONS,
|
||||
@ -7,43 +7,30 @@ import {
|
||||
} from '@/entities/broker-operation';
|
||||
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
||||
import { BrokerOperationsTable } from '@/widgets/broker-operations-table';
|
||||
import { useCursorPagination } from '@/shared/lib/useCursorPagination';
|
||||
|
||||
export function BrokerOperationsPage() {
|
||||
const { accountId } = useBrokerAccountContext();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const urlType = searchParams.get('type');
|
||||
const selectedType = isBrokerOperationType(urlType) ? urlType : '';
|
||||
const [cursor, setCursor] = useState<string | undefined>(undefined);
|
||||
const [cursorStack, setCursorStack] = useState<Array<string | undefined>>([]);
|
||||
const pagination = useCursorPagination();
|
||||
|
||||
const operations = useBrokerOperations(accountId, {
|
||||
limit: 10,
|
||||
cursor,
|
||||
cursor: pagination.cursor,
|
||||
operationTypes: selectedType || undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setCursor(undefined);
|
||||
setCursorStack([]);
|
||||
}, [selectedType]);
|
||||
pagination.reset();
|
||||
}, [selectedType]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
function handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const nextType = event.target.value;
|
||||
setSearchParams(nextType ? { type: nextType } : {}, { replace: true });
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
const nextCursor = operations.data?.nextCursor;
|
||||
if (!nextCursor || !operations.data?.hasNext) return;
|
||||
setCursorStack((previous) => [...previous, cursor]);
|
||||
setCursor(nextCursor);
|
||||
}
|
||||
|
||||
function handlePrevious() {
|
||||
if (cursorStack.length === 0) return;
|
||||
setCursor(cursorStack[cursorStack.length - 1]);
|
||||
setCursorStack((previous) => previous.slice(0, -1));
|
||||
}
|
||||
|
||||
const history = operations.error ? (
|
||||
<p role="alert">Не удалось загрузить историю операций</p>
|
||||
) : (
|
||||
@ -56,11 +43,11 @@ export function BrokerOperationsPage() {
|
||||
isFetching={operations.isFetching}
|
||||
page={operations.data}
|
||||
pagination={{
|
||||
pageNumber: cursorStack.length + 1,
|
||||
canGoBack: cursorStack.length > 0,
|
||||
pageNumber: pagination.pageNumber,
|
||||
canGoBack: pagination.pageNumber > 1,
|
||||
canGoForward: Boolean(operations.data?.hasNext && operations.data.nextCursor),
|
||||
onPrevious: handlePrevious,
|
||||
onNext: handleNext,
|
||||
onPrevious: pagination.handlePrevious,
|
||||
onNext: () => pagination.handleNext(operations.data?.nextCursor),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useBrokerPositions } from '@/entities/broker-position';
|
||||
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
||||
import { BrokerPositionTable } from '@/widgets/broker-positions-table';
|
||||
import { useCursorPagination } from '@/shared/lib/useCursorPagination';
|
||||
|
||||
type BrokerPositionsPageProps = {
|
||||
type: 'share' | 'bond';
|
||||
@ -10,22 +10,8 @@ type BrokerPositionsPageProps = {
|
||||
|
||||
export function BrokerPositionsPage({ type, title }: BrokerPositionsPageProps) {
|
||||
const { accountId } = useBrokerAccountContext();
|
||||
const [cursor, setCursor] = useState<string | undefined>(undefined);
|
||||
const [cursorStack, setCursorStack] = useState<Array<string | undefined>>([]);
|
||||
const positions = useBrokerPositions(accountId, { type, limit: 10, cursor });
|
||||
|
||||
function handleNext() {
|
||||
const nextCursor = positions.data?.nextCursor;
|
||||
if (!nextCursor || !positions.data?.hasNext) return;
|
||||
setCursorStack((previous) => [...previous, cursor]);
|
||||
setCursor(nextCursor);
|
||||
}
|
||||
|
||||
function handlePrevious() {
|
||||
if (cursorStack.length === 0) return;
|
||||
setCursor(cursorStack[cursorStack.length - 1]);
|
||||
setCursorStack((previous) => previous.slice(0, -1));
|
||||
}
|
||||
const pagination = useCursorPagination();
|
||||
const positions = useBrokerPositions(accountId, { type, limit: 10, cursor: pagination.cursor });
|
||||
|
||||
if (positions.error) {
|
||||
return (
|
||||
@ -47,9 +33,9 @@ export function BrokerPositionsPage({ type, title }: BrokerPositionsPageProps) {
|
||||
isLoading={positions.isLoading}
|
||||
isFetching={positions.isFetching}
|
||||
emptyMessage={type === 'share' ? 'На счёте нет акций' : 'На счёте нет облигаций'}
|
||||
pageNumber={cursorStack.length + 1}
|
||||
onNext={handleNext}
|
||||
onPrevious={handlePrevious}
|
||||
pageNumber={pagination.pageNumber}
|
||||
onNext={() => pagination.handleNext(positions.data?.nextCursor)}
|
||||
onPrevious={pagination.handlePrevious}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
40
apps/frontend/src/shared/lib/useCursorPagination.ts
Normal file
40
apps/frontend/src/shared/lib/useCursorPagination.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useCursorPagination() {
|
||||
const [cursor, setCursor] = useState<string | undefined>(undefined);
|
||||
const [cursorStack, setCursorStack] = useState<Array<string | undefined>>([]);
|
||||
|
||||
const handleNext = useCallback(
|
||||
(nextCursor: string | null | undefined) => {
|
||||
if (!nextCursor) return;
|
||||
setCursorStack((prev) => [...prev, cursor]);
|
||||
setCursor(nextCursor);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[cursor],
|
||||
);
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
setCursorStack((prev) => {
|
||||
if (prev.length === 0) return prev;
|
||||
const lastCursor = prev[prev.length - 1];
|
||||
const remaining = prev.slice(0, -1);
|
||||
setCursor(lastCursor);
|
||||
return remaining;
|
||||
});
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setCursor(undefined);
|
||||
setCursorStack([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
cursor,
|
||||
pageNumber: cursorStack.length + 1,
|
||||
handleNext,
|
||||
handlePrevious,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
31
package-lock.json
generated
31
package-lock.json
generated
@ -93,6 +93,7 @@
|
||||
"react-router-dom": "^6.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@conarti/eslint-plugin-feature-sliced": "^1.0.5",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@ -3416,6 +3417,36 @@
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@conarti/eslint-plugin-feature-sliced": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@conarti/eslint-plugin-feature-sliced/-/eslint-plugin-feature-sliced-1.0.5.tgz",
|
||||
"integrity": "sha512-Gx90Zupi8nv6cCP3O6cYWE9GT4RIw0D9JBHWeUwXEv7qmjCd3dXSqDEmQchN7k41hhCPupZvMevH4JrSORcVjQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7",
|
||||
"eslint-plugin-import": ">=2.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@conarti/eslint-plugin-feature-sliced/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/cascade-layer-name-parser": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user