proje-react-panel 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/__tests__/build/svgoViewBox.test.d.ts +1 -0
- package/dist/__tests__/components/list/Pagination.test.d.ts +1 -0
- package/dist/__tests__/components/list/listMetrics.test.d.ts +1 -0
- package/dist/components/list/Datagrid.d.ts +10 -1
- package/dist/components/list/listMetrics.d.ts +31 -0
- package/dist/decorators/list/List.d.ts +15 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/how_to.md +23 -0
- package/package.json +3 -2
- package/src/__tests__/build/svgoViewBox.test.ts +48 -0
- package/src/__tests__/components/list/Pagination.test.tsx +53 -0
- package/src/__tests__/components/list/listMetrics.test.ts +36 -0
- package/src/assets/icons/svg/down-arrow-backup-2.svg +2 -2
- package/src/components/list/Datagrid.tsx +18 -3
- package/src/components/list/ListPage.tsx +94 -8
- package/src/components/list/Pagination.tsx +5 -1
- package/src/components/list/cells/ImageCell.tsx +6 -1
- package/src/components/list/listMetrics.ts +42 -0
- package/src/decorators/list/List.ts +15 -0
- package/svgo.icons.json +6 -0
- package/.idea/codeStyles/Project.xml +0 -54
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/jsLinters/eslint.xml +0 -6
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -7
- package/.idea/proje-react-panel.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/.idea/watcherTasks.xml +0 -4
package/how_to.md
CHANGED
|
@@ -137,6 +137,29 @@ export class ProductDetails {
|
|
|
137
137
|
}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
### 4.4.1 Page size
|
|
141
|
+
|
|
142
|
+
`ListPage` asks for as many rows as fit the datagrid, instead of a fixed page size. It measures the
|
|
143
|
+
grid element (so your own shell CSS is accounted for) and divides by the row height — which is
|
|
144
|
+
declared, not measured, and then applied to the row, so the number cannot drift from what is on
|
|
145
|
+
screen:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Default: auto page size, library row height, nothing to declare.
|
|
149
|
+
@List({ getData: dataFetchers.products.getAll, primaryId: 'id' })
|
|
150
|
+
|
|
151
|
+
// A list with taller rows (an image cell, or your own row CSS) declares its height.
|
|
152
|
+
@List({ getData: dataFetchers.products.getAll, primaryId: 'id', rowHeight: 124 })
|
|
153
|
+
|
|
154
|
+
// Opt out: `getData` (or the server) decides the page size, as before.
|
|
155
|
+
@List({ getData: dataFetchers.products.getAll, primaryId: 'id', autoCalculate: false })
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The computed size is sent as `limit`, so the backend contract in §2 is unchanged. It is clamped to
|
|
159
|
+
5–100 rows, and recomputed on window resize.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
140
163
|
### 4.5 Route the pages
|
|
141
164
|
|
|
142
165
|
```tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proje-react-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "SEFA DEMİR",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"module": "dist/index.esm.js",
|
|
10
10
|
"source": "src/index.ts",
|
|
11
11
|
"types": "dist/index.d.ts",
|
|
12
|
+
"packageManager": "yarn@4.17.0",
|
|
12
13
|
"scripts": {
|
|
13
14
|
"test": "jest",
|
|
14
15
|
"build": "rollup -c",
|
|
@@ -23,12 +24,12 @@
|
|
|
23
24
|
"url": "https://github.com/demirsefa/proje-react-panel/issues"
|
|
24
25
|
},
|
|
25
26
|
"homepage": "https://github.com/demirsefa/proje-react-panel#readme",
|
|
26
|
-
"dependencies": {},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@eslint/compat": "^1.2.9",
|
|
29
29
|
"@hookform/resolvers": "^4.1.3",
|
|
30
30
|
"@rollup/plugin-commonjs": "^28.0.3",
|
|
31
31
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
32
|
+
"@svgr/core": "^8.1.0",
|
|
32
33
|
"@svgr/rollup": "^8.1.0",
|
|
33
34
|
"@testing-library/dom": "^10.4.1",
|
|
34
35
|
"@testing-library/react": "^16.3.2",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { describe, expect, it } from '@jest/globals';
|
|
4
|
+
import { transform } from '@svgr/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ikonlar bundle'a SVGR + SVGO'dan gecerek gomuluyor ve SVGO'nun varsayilan
|
|
8
|
+
* `removeViewBox` plugin'i, viewBox width/height ile birebir ayniysa onu
|
|
9
|
+
* "gereksiz" sayip siliyor. viewBox'siz bir SVG olceklenemez: tuketici CSS'te
|
|
10
|
+
* `.icon { width: 16px }` verdiginde ikon kucultulmez, KIRPILIR — check/cross
|
|
11
|
+
* bir kez tam bu yuzden gorunmez oldu, dist'e yama atilarak kapatildi, yama
|
|
12
|
+
* baska bir fix'le dusunce geri geldi.
|
|
13
|
+
*
|
|
14
|
+
* Bu yuzden test tek bir ikonu degil ayari kilitliyor: rollup'in okudugu
|
|
15
|
+
* `svgo.icons.json` ile HER ikon gercekten donusturuluyor. Ayar bozulursa —
|
|
16
|
+
* ya da viewBox'i width/height'iyla ayni yeni bir ikon eklenirse — kirmizi yanar.
|
|
17
|
+
*/
|
|
18
|
+
const repoRoot = path.resolve(__dirname, '../../..');
|
|
19
|
+
const iconDir = path.join(repoRoot, 'src/assets/icons/svg');
|
|
20
|
+
const svgoConfig = JSON.parse(
|
|
21
|
+
readFileSync(path.join(repoRoot, 'svgo.icons.json'), 'utf8')
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const icons = readdirSync(iconDir).filter((f) => f.endsWith('.svg'));
|
|
25
|
+
|
|
26
|
+
const compile = (file: string): Promise<string> =>
|
|
27
|
+
transform(
|
|
28
|
+
readFileSync(path.join(iconDir, file), 'utf8'),
|
|
29
|
+
{ plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], svgoConfig },
|
|
30
|
+
{ componentName: 'Icon', filePath: path.join(iconDir, file) }
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
describe('icon build config', () => {
|
|
34
|
+
it('has icons to check', () => {
|
|
35
|
+
expect(icons.length).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it.each(icons)('%s keeps its viewBox', async (file) => {
|
|
39
|
+
expect(await compile(file)).toContain('viewBox=');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Kirpilma yalnizca viewBox'in varligina degil dogruluguna bagli: yanlis bir
|
|
43
|
+
// viewBox da ayni sonucu verir, o yuzden regresyonun kaynagi olan ikon acikca
|
|
44
|
+
// kendi kutusuyla kilitli.
|
|
45
|
+
it('check.svg keeps the exact box its paths are drawn in', async () => {
|
|
46
|
+
expect(await compile('check.svg')).toContain('viewBox="0 0 24 24"');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { TextDecoder, TextEncoder } from 'util';
|
|
5
|
+
// jsdom'da TextEncoder yok; testing-library'nin cektigi bagimliliklar import
|
|
6
|
+
// aninda okuyor.
|
|
7
|
+
Object.assign(globalThis, { TextEncoder, TextDecoder });
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
11
|
+
import { render, screen } from '@testing-library/react';
|
|
12
|
+
import { Pagination } from '../../../components/list/Pagination';
|
|
13
|
+
|
|
14
|
+
const lastPageButton = () => {
|
|
15
|
+
const numbered = screen
|
|
16
|
+
.getAllByRole('button')
|
|
17
|
+
.map(button => Number(button.textContent))
|
|
18
|
+
.filter(value => Number.isFinite(value) && value > 0);
|
|
19
|
+
return Math.max(...numbered);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('Pagination', () => {
|
|
23
|
+
it('eksik dolu son sayfayi da cizer', () => {
|
|
24
|
+
// 2151 kayit / 10 limit = 216 sayfa. `floor` ile 215 ciziliyordu ve son
|
|
25
|
+
// kayda hicbir sayfadan ulasilamiyordu.
|
|
26
|
+
render(
|
|
27
|
+
<Pagination pagination={{ total: 2151, page: 1, limit: 10 }} onPageChange={jest.fn()} />
|
|
28
|
+
);
|
|
29
|
+
expect(lastPageButton()).toBe(216);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('ikinci sayfasi olan listede gizlenmez', () => {
|
|
33
|
+
// total < 2*limit: `floor` 1 sayfa sanip pagination'i tamamen gizliyordu.
|
|
34
|
+
render(<Pagination pagination={{ total: 15, page: 1, limit: 10 }} onPageChange={jest.fn()} />);
|
|
35
|
+
expect(lastPageButton()).toBe(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('tek sayfaya sigan listede cizilmez', () => {
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<Pagination pagination={{ total: 8, page: 1, limit: 10 }} onPageChange={jest.fn()} />
|
|
41
|
+
);
|
|
42
|
+
expect(container.firstChild).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('limit gelmeden cizilmez', () => {
|
|
46
|
+
// Liste ilk fetch'ini beklerken pagination state'i {0,0,0}; bolme NaN
|
|
47
|
+
// uretip "NaN <= 1" false donuyordu.
|
|
48
|
+
const { container } = render(
|
|
49
|
+
<Pagination pagination={{ total: 0, page: 0, limit: 0 }} onPageChange={jest.fn()} />
|
|
50
|
+
);
|
|
51
|
+
expect(container.firstChild).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
LIST_FALLBACK_LIMIT,
|
|
4
|
+
LIST_MAX_LIMIT,
|
|
5
|
+
LIST_MIN_LIMIT,
|
|
6
|
+
LIST_ROW_HEIGHT,
|
|
7
|
+
calculateListLimit,
|
|
8
|
+
} from '../../../components/list/listMetrics';
|
|
9
|
+
|
|
10
|
+
describe('calculateListLimit', () => {
|
|
11
|
+
it('istenen satirlar konteynere sigar, bir fazlasi sigmaz', () => {
|
|
12
|
+
const containerHeight = 780;
|
|
13
|
+
const limit = calculateListLimit(containerHeight);
|
|
14
|
+
|
|
15
|
+
// Tablo basligi da konteynerin icinde; onun icin bir satirlik yer ayriliyor.
|
|
16
|
+
// Toplam konteyneri asiyorsa otomatik boyut kendi amacinin tersine scroll
|
|
17
|
+
// uretiyor demektir.
|
|
18
|
+
expect((limit + 1) * LIST_ROW_HEIGHT).toBeLessThanOrEqual(containerHeight);
|
|
19
|
+
expect((limit + 2) * LIST_ROW_HEIGHT).toBeGreaterThan(containerHeight);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('yuksek satirda daha az kayit ister', () => {
|
|
23
|
+
expect(calculateListLimit(780, 124)).toBeLessThan(calculateListLimit(780));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('cok kisa konteynerde tabana, cok uzununda tavana yapisir', () => {
|
|
27
|
+
expect(calculateListLimit(120)).toBe(LIST_MIN_LIMIT);
|
|
28
|
+
expect(calculateListLimit(100000)).toBe(LIST_MAX_LIMIT);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('olculemeyen degerlerde fallback limit doner', () => {
|
|
32
|
+
expect(calculateListLimit(Number.NaN)).toBe(LIST_FALLBACK_LIMIT);
|
|
33
|
+
expect(calculateListLimit(780, 0)).toBe(LIST_FALLBACK_LIMIT);
|
|
34
|
+
expect(calculateListLimit(0)).toBe(LIST_FALLBACK_LIMIT);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
3
3
|
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M903.232 256l56.768 50.432L512 768 64 306.432 120.768 256 512 659.072z" fill="#000000" /></svg>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Link } from 'react-router';
|
|
3
3
|
import { EmptyList } from './EmptyList';
|
|
4
|
+
import { LoadingScreen } from '../LoadingScreen';
|
|
4
5
|
import SearchIcon from '../../assets/icons/svg/search.svg';
|
|
5
6
|
import PencilIcon from '../../assets/icons/svg/pencil.svg';
|
|
6
7
|
import DownArrowIcon from '../../assets/icons/svg/down-arrow-backup-2.svg';
|
|
@@ -21,12 +22,24 @@ const ACTIONS_COLUMN_WIDTH = '120px';
|
|
|
21
22
|
interface DatagridProps<T extends AnyClass> {
|
|
22
23
|
data: T[];
|
|
23
24
|
listPageMeta: ListPageMeta<T>;
|
|
25
|
+
loading?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Satir yuksekligi satira UYGULANIR, sadece bildirilmez: otomatik sayfa
|
|
28
|
+
* boyutu bu sayiya gore hesaplandigi icin gercekle ayrismasina izin
|
|
29
|
+
* verilemez. Inline veriliyor ki tuketicinin kendi tablo CSS'i olsa da
|
|
30
|
+
* gecerli olsun.
|
|
31
|
+
*/
|
|
32
|
+
rowHeight?: number;
|
|
33
|
+
containerRef?: React.RefObject<HTMLDivElement | null>;
|
|
24
34
|
onRemoveItem?: (item: T) => Promise<void>;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export function Datagrid<T extends AnyClass>({
|
|
28
38
|
data,
|
|
29
39
|
listPageMeta,
|
|
40
|
+
loading,
|
|
41
|
+
rowHeight,
|
|
42
|
+
containerRef,
|
|
30
43
|
onRemoveItem,
|
|
31
44
|
}: DatagridProps<T>) {
|
|
32
45
|
const cells = listPageMeta.cells;
|
|
@@ -38,8 +51,10 @@ export function Datagrid<T extends AnyClass>({
|
|
|
38
51
|
: null;
|
|
39
52
|
|
|
40
53
|
return (
|
|
41
|
-
<div className="datagrid">
|
|
42
|
-
{
|
|
54
|
+
<div className="datagrid" ref={containerRef}>
|
|
55
|
+
{loading ? (
|
|
56
|
+
<LoadingScreen id={listPageMeta.class.key} />
|
|
57
|
+
) : !data || data.length === 0 ? (
|
|
43
58
|
<EmptyList />
|
|
44
59
|
) : (
|
|
45
60
|
<table className="datagrid-table">
|
|
@@ -75,7 +90,7 @@ export function Datagrid<T extends AnyClass>({
|
|
|
75
90
|
| undefined)
|
|
76
91
|
: null;
|
|
77
92
|
return (
|
|
78
|
-
<tr key={index}>
|
|
93
|
+
<tr key={index} style={{ height: rowHeight }}>
|
|
79
94
|
{cells.map((configuration: CellConfiguration) => {
|
|
80
95
|
return (
|
|
81
96
|
<CellField
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useMemo,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
useId,
|
|
9
|
+
} from 'react';
|
|
2
10
|
import { useParams, useNavigate } from 'react-router';
|
|
3
11
|
import { Datagrid } from './Datagrid';
|
|
4
12
|
import { ErrorComponent } from '../ErrorComponent';
|
|
5
|
-
import { LoadingScreen } from '../LoadingScreen';
|
|
6
13
|
import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
|
|
7
14
|
import { Pagination } from './Pagination';
|
|
8
15
|
import { ListHeader } from './ListHeader';
|
|
9
16
|
import { FilterPopup } from './FilterPopup';
|
|
10
17
|
import { getListPageMeta } from '../../decorators/list/getListPageMeta';
|
|
18
|
+
import { LIST_ROW_HEIGHT, calculateListLimit } from './listMetrics';
|
|
19
|
+
|
|
20
|
+
const RESIZE_DEBOUNCE_MS = 200;
|
|
11
21
|
|
|
12
22
|
export function ListPage<T extends AnyClass>({
|
|
13
23
|
model,
|
|
@@ -24,17 +34,51 @@ export function ListPage<T extends AnyClass>({
|
|
|
24
34
|
const [error, setError] = useState<unknown>(null);
|
|
25
35
|
|
|
26
36
|
const [pagination, setPagination] = useState({ total: 0, page: 0, limit: 0 });
|
|
37
|
+
|
|
38
|
+
const rowHeight = listPageMeta.class.rowHeight ?? LIST_ROW_HEIGHT;
|
|
39
|
+
const autoCalculate = listPageMeta.class.autoCalculate ?? true;
|
|
40
|
+
|
|
41
|
+
const datagridRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const [containerHeight, setContainerHeight] = useState<number | null>(null);
|
|
43
|
+
|
|
44
|
+
// undefined: limit gonderme (autoCalculate kapali). null: henuz olculmedi,
|
|
45
|
+
// yanlis boyutla bir istek atmaktansa bekle.
|
|
46
|
+
const limit = !autoCalculate
|
|
47
|
+
? undefined
|
|
48
|
+
: containerHeight === null
|
|
49
|
+
? null
|
|
50
|
+
: calculateListLimit(containerHeight, rowHeight);
|
|
51
|
+
|
|
27
52
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
|
28
53
|
const [activeFilters, setActiveFilters] = useState<Record<string, string>>();
|
|
29
54
|
const params = useParams();
|
|
30
55
|
const navigate = useNavigate();
|
|
31
56
|
|
|
57
|
+
// Gorsel hucresi satiri kutuphanenin olcusunun cok uzerine cikariyor; tablo
|
|
58
|
+
// artik satiri kirptigi icin bunu soylemeden birakmak gorseli sessizce
|
|
59
|
+
// kucultmek olurdu. Uretim derlemesinde sussun, tuketicinin konsolunu
|
|
60
|
+
// kirletmeyelim.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (process.env.NODE_ENV === 'production') return;
|
|
63
|
+
if (listPageMeta.class.rowHeight !== undefined) return;
|
|
64
|
+
if (!listPageMeta.cells.some(cell => cell.type === 'image')) return;
|
|
65
|
+
|
|
66
|
+
console.warn(
|
|
67
|
+
`[proje-react-panel] "${listPageMeta.class.key}" listesinde gorsel hucresi var ` +
|
|
68
|
+
`ama rowHeight verilmemis; satir ${LIST_ROW_HEIGHT}px'e kirpilacak. ` +
|
|
69
|
+
`@List({ rowHeight: ... }) ile gorselin sigacagi yuksekligi bildir.`
|
|
70
|
+
);
|
|
71
|
+
}, [listPageMeta]);
|
|
72
|
+
|
|
32
73
|
const fetchData = useCallback(
|
|
33
74
|
async (page: number, filters?: Record<string, string>) => {
|
|
34
75
|
setLoading(true);
|
|
35
76
|
try {
|
|
36
77
|
const result = await listPageMeta.class.getData({
|
|
37
78
|
page,
|
|
79
|
+
// autoCalculate kapaliysa limit hic gonderilmez: sayfa boyutunu
|
|
80
|
+
// eskisi gibi `getData`/sunucu belirlesin.
|
|
81
|
+
...(limit ? { limit } : {}),
|
|
38
82
|
filters: filters ?? activeFilters ?? {},
|
|
39
83
|
});
|
|
40
84
|
//TODO: any is not a good solution, we need to find a better way to do this
|
|
@@ -52,7 +96,7 @@ export function ListPage<T extends AnyClass>({
|
|
|
52
96
|
setLoading(false);
|
|
53
97
|
}
|
|
54
98
|
},
|
|
55
|
-
[activeFilters, listPageMeta.class]
|
|
99
|
+
[activeFilters, limit, listPageMeta.class]
|
|
56
100
|
);
|
|
57
101
|
|
|
58
102
|
useEffect(() => {
|
|
@@ -60,15 +104,47 @@ export function ListPage<T extends AnyClass>({
|
|
|
60
104
|
const filtersFromUrl: Record<string, string> = {};
|
|
61
105
|
searchParams.forEach((value, key) => {
|
|
62
106
|
filtersFromUrl[key] = value;
|
|
63
|
-
});
|
|
107
|
+
});
|
|
64
108
|
setActiveFilters(filtersFromUrl);
|
|
65
109
|
}, []);
|
|
66
110
|
|
|
111
|
+
// Datagrid ilk boyamadan once olculuyor: veri beklerken de render edildigi
|
|
112
|
+
// icin yuksekligi hazir, yani dogru limit ILK istekte kullanilabiliyor.
|
|
113
|
+
// Olcum penceye degil elemana bakiyor; tuketicinin kabugu (sidebar, baslik,
|
|
114
|
+
// dolgu) ne kadar yer kaplarsa kaplasin hesap dogru kaliyor.
|
|
115
|
+
//
|
|
116
|
+
// ResizeObserver yerine window resize: icerik buyudukce buyuyen bir
|
|
117
|
+
// konteynerde observer "daha cok satir iste -> daha uzun konteyner" dongusune
|
|
118
|
+
// girebilirdi. Yuksekligi degistiren gercek olay zaten pencere boyutu.
|
|
119
|
+
useLayoutEffect(() => {
|
|
120
|
+
if (!autoCalculate) return;
|
|
121
|
+
|
|
122
|
+
const measure = () => {
|
|
123
|
+
const element = datagridRef.current;
|
|
124
|
+
if (!element) return;
|
|
125
|
+
const next = element.clientHeight;
|
|
126
|
+
setContainerHeight(previous => (previous === next ? previous : next));
|
|
127
|
+
};
|
|
128
|
+
measure();
|
|
129
|
+
|
|
130
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
131
|
+
const onResize = () => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
timer = setTimeout(measure, RESIZE_DEBOUNCE_MS);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
window.addEventListener('resize', onResize);
|
|
137
|
+
return () => {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
window.removeEventListener('resize', onResize);
|
|
140
|
+
};
|
|
141
|
+
}, [autoCalculate]);
|
|
142
|
+
|
|
67
143
|
useEffect(() => {
|
|
68
|
-
if (activeFilters) {
|
|
144
|
+
if (activeFilters && limit !== null) {
|
|
69
145
|
fetchData(parseInt(params.page as string) || 1, activeFilters);
|
|
70
146
|
}
|
|
71
|
-
}, [fetchData, params.page, activeFilters, listPageMeta.class.getData]);
|
|
147
|
+
}, [fetchData, limit, params.page, activeFilters, listPageMeta.class.getData]);
|
|
72
148
|
|
|
73
149
|
const handleFilterApply = (filters: Record<string, string>) => {
|
|
74
150
|
setActiveFilters(filters);
|
|
@@ -86,11 +162,18 @@ export function ListPage<T extends AnyClass>({
|
|
|
86
162
|
fetchData(1, filters); // Reset to first page when filters change
|
|
87
163
|
};
|
|
88
164
|
|
|
89
|
-
|
|
165
|
+
// Yukleme ekrani artik sayfanin tamamini degil datagrid'in icini kapliyor:
|
|
166
|
+
// konteyner ilk istekten once render olmazsa olculemez, olculemezse ilk
|
|
167
|
+
// istek yanlis sayfa boyutuyla gider ve duzeltmek ikinci bir istek olurdu.
|
|
90
168
|
if (error) return <ErrorComponent id={id} error={error} />;
|
|
91
169
|
|
|
92
170
|
return (
|
|
93
|
-
<div
|
|
171
|
+
<div
|
|
172
|
+
className="list"
|
|
173
|
+
// Satir yuksekligi hucrelere de lazim (ornegin gorsel bu yukseklige
|
|
174
|
+
// sigdiriliyor); tek kaynak yine `@List({ rowHeight })`.
|
|
175
|
+
style={{ '--prp-list-row-height': `${rowHeight}px` } as React.CSSProperties}
|
|
176
|
+
>
|
|
94
177
|
<ListHeader
|
|
95
178
|
listPageMeta={listPageMeta}
|
|
96
179
|
filtered={!!(activeFilters && !!Object.keys(activeFilters).length)}
|
|
@@ -100,6 +183,9 @@ export function ListPage<T extends AnyClass>({
|
|
|
100
183
|
<Datagrid
|
|
101
184
|
listPageMeta={listPageMeta}
|
|
102
185
|
data={data}
|
|
186
|
+
loading={loading}
|
|
187
|
+
rowHeight={rowHeight}
|
|
188
|
+
containerRef={datagridRef}
|
|
103
189
|
onRemoveItem={async () => {
|
|
104
190
|
await fetchData(pagination.page);
|
|
105
191
|
}}
|
|
@@ -11,7 +11,11 @@ interface PaginationProps {
|
|
|
11
11
|
|
|
12
12
|
export function Pagination({ pagination, onPageChange }: PaginationProps) {
|
|
13
13
|
const { total, page, limit } = pagination;
|
|
14
|
-
|
|
14
|
+
// `floor` son, eksik dolu sayfayi hic saymiyordu: 2151 kayit / 10 limit ->
|
|
15
|
+
// 215 sayfa cizilir, son kayda hicbir sayfadan ulasilamazdi. Daha kotusu
|
|
16
|
+
// total < 2*limit olan her listede sonuc 1 cikip pagination tamamen
|
|
17
|
+
// gizleniyor, ikinci sayfadaki kayitlar erisilemez oluyordu.
|
|
18
|
+
const totalPages = limit > 0 ? Math.ceil(total / limit) : 0;
|
|
15
19
|
|
|
16
20
|
if (totalPages <= 1) return null;
|
|
17
21
|
|
|
@@ -17,11 +17,16 @@ export function ImageCell({ item, configuration }: ImageCellProps) {
|
|
|
17
17
|
// maxWidth sart: sabit tablo duzeninde kolon 100px'ten dar kalabiliyor ve
|
|
18
18
|
// hucre tasmayi kirptigi icin gorsel sessizce yarim gorunurdu. Kucultmek
|
|
19
19
|
// kirpmaktan iyi; objectFit: contain oranı koruyor.
|
|
20
|
+
//
|
|
21
|
+
// maxHeight ayni isi dikeyde yapiyor: gorsel satir yuksekliginden uzun
|
|
22
|
+
// olursa satiri tek basina buyutur ve otomatik sayfa boyutunun dayandigi
|
|
23
|
+
// rowHeight yalan olurdu. Gorseli buyuk gormek isteyen liste
|
|
24
|
+
// `@List({ rowHeight })` ile satiri buyutur.
|
|
20
25
|
<img
|
|
21
26
|
width={100}
|
|
22
27
|
height={100}
|
|
23
28
|
src={imageConfiguration.baseUrl + value}
|
|
24
|
-
style={{ objectFit: 'contain', maxWidth: '100%' }}
|
|
29
|
+
style={{ objectFit: 'contain', maxWidth: '100%', maxHeight: 'var(--prp-list-row-height)' }}
|
|
25
30
|
alt=""
|
|
26
31
|
/>
|
|
27
32
|
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Otomatik sayfa boyutunun iki girdisi var ve ikisi ayri yollardan geliyor:
|
|
3
|
+
*
|
|
4
|
+
* - Datagrid'in yuksekligi OLCULUR. Tuketici kutuphanenin liste CSS'ini
|
|
5
|
+
* kullanmak zorunda degil (kendi kabugunu yazan bir panelde datagrid ne
|
|
6
|
+
* `100vh - 120px`'tir ne de baska bir sabit), dolayisiyla buraya px yazmak
|
|
7
|
+
* sessizce yanlislanacak bir varsayim olurdu.
|
|
8
|
+
* - Satir yuksekligi BEYAN EDILIR ve tabloya dayatilir (`@List({ rowHeight })`).
|
|
9
|
+
* Olculmuyor: olcmek icin once satirin render olmasi, yani bir istek atilmis
|
|
10
|
+
* olmasi gerekirdi; dogru limit ikinci bir istek demek olurdu.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Olcum yapilamadiginda (SSR, jsdom, yukseklik 0) kullanilan sayfa boyutu. */
|
|
14
|
+
export const LIST_FALLBACK_LIMIT = 10;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Satirin varsayilan yuksekligi (px): hucre dolgusu + satir kutusu + alt cizgi.
|
|
18
|
+
* Satirini kendi CSS'iyle degistiren ya da `image` gibi yuksek hucre tasiyan
|
|
19
|
+
* tuketici `@List({ rowHeight })` ile kendi olcusunu verir.
|
|
20
|
+
*/
|
|
21
|
+
export const LIST_ROW_HEIGHT = 44;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Otomatik hesabin tabani ve tavani: cok kisa ekranda kullanilamaz bir liste,
|
|
25
|
+
* cok uzun ekranda sunucuyu doven bir sorgu cikmasin.
|
|
26
|
+
*/
|
|
27
|
+
export const LIST_MIN_LIMIT = 5;
|
|
28
|
+
export const LIST_MAX_LIMIT = 100;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Datagrid'e kac satir sigiyorsa o kadar kayit iste. Tablo basligi da o alanin
|
|
32
|
+
* icinde duruyor, o yuzden bir satir dusuluyor; dusulmezse hesap tam bir satir
|
|
33
|
+
* tasar ve otomatik boyut kendi amacinin tersine scroll uretir.
|
|
34
|
+
*/
|
|
35
|
+
export function calculateListLimit(containerHeight: number, rowHeight = LIST_ROW_HEIGHT): number {
|
|
36
|
+
if (!Number.isFinite(containerHeight) || containerHeight <= 0 || rowHeight <= 0) {
|
|
37
|
+
return LIST_FALLBACK_LIMIT;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rows = Math.floor(containerHeight / rowHeight) - 1;
|
|
41
|
+
return Math.min(LIST_MAX_LIMIT, Math.max(LIST_MIN_LIMIT, rows));
|
|
42
|
+
}
|
|
@@ -38,6 +38,21 @@ export interface ListOptions<T> {
|
|
|
38
38
|
actions?: ((item: T) => ListActionOptions<T>) | ListActionOptions<T>;
|
|
39
39
|
primaryId?: string;
|
|
40
40
|
key?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Sayfa boyutunu ekrana sigan satir sayisindan hesapla. Varsayilan acik:
|
|
43
|
+
* sabit bir sayfa boyutu (ornegin 10) buyuk ekranda tablonun altinda kocaman
|
|
44
|
+
* bir bosluk birakiyordu — datagrid yuksekligi viewport'a civili.
|
|
45
|
+
* Kapatirsan `getData` limit'i eskisi gibi kendi belirler.
|
|
46
|
+
*/
|
|
47
|
+
autoCalculate?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Satir yuksekligi (px). Sadece bir bilgi degil, tabloya UYGULANIR: satira
|
|
50
|
+
* bu yukseklik verilir ve hucre icerigi (ornegin gorsel) buna kirpilir.
|
|
51
|
+
* Boylece beyan ile gercek asla ayrisamaz. Yalnizca satirini kendi CSS'iyle
|
|
52
|
+
* degistiren ya da `image` gibi yuksek hucre tasiyan listelerde gerekir;
|
|
53
|
+
* verilmezse kutuphanenin kendi olcusu (`LIST_ROW_HEIGHT`) kullanilir.
|
|
54
|
+
*/
|
|
55
|
+
rowHeight?: number;
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
export type ListConfiguration<T> = ListOptions<T> & {
|
package/svgo.icons.json
ADDED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
<component name="ProjectCodeStyleConfiguration">
|
|
2
|
-
<code_scheme name="Project" version="173">
|
|
3
|
-
<HTMLCodeStyleSettings>
|
|
4
|
-
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
|
5
|
-
</HTMLCodeStyleSettings>
|
|
6
|
-
<JSCodeStyleSettings version="0">
|
|
7
|
-
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
|
8
|
-
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
|
9
|
-
<option name="FORCE_QUOTE_STYlE" value="true" />
|
|
10
|
-
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
|
11
|
-
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
|
12
|
-
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
|
13
|
-
</JSCodeStyleSettings>
|
|
14
|
-
<TypeScriptCodeStyleSettings version="0">
|
|
15
|
-
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
|
16
|
-
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
|
17
|
-
<option name="FORCE_QUOTE_STYlE" value="true" />
|
|
18
|
-
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
|
19
|
-
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
|
20
|
-
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
|
21
|
-
</TypeScriptCodeStyleSettings>
|
|
22
|
-
<VueCodeStyleSettings>
|
|
23
|
-
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
|
24
|
-
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
|
25
|
-
</VueCodeStyleSettings>
|
|
26
|
-
<codeStyleSettings language="HTML">
|
|
27
|
-
<option name="SOFT_MARGINS" value="120" />
|
|
28
|
-
<indentOptions>
|
|
29
|
-
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
|
30
|
-
<option name="USE_TAB_CHARACTER" value="true" />
|
|
31
|
-
</indentOptions>
|
|
32
|
-
</codeStyleSettings>
|
|
33
|
-
<codeStyleSettings language="JavaScript">
|
|
34
|
-
<option name="SOFT_MARGINS" value="120" />
|
|
35
|
-
<indentOptions>
|
|
36
|
-
<option name="USE_TAB_CHARACTER" value="true" />
|
|
37
|
-
</indentOptions>
|
|
38
|
-
</codeStyleSettings>
|
|
39
|
-
<codeStyleSettings language="TypeScript">
|
|
40
|
-
<option name="SOFT_MARGINS" value="120" />
|
|
41
|
-
<indentOptions>
|
|
42
|
-
<option name="USE_TAB_CHARACTER" value="true" />
|
|
43
|
-
</indentOptions>
|
|
44
|
-
</codeStyleSettings>
|
|
45
|
-
<codeStyleSettings language="Vue">
|
|
46
|
-
<option name="SOFT_MARGINS" value="120" />
|
|
47
|
-
<indentOptions>
|
|
48
|
-
<option name="INDENT_SIZE" value="4" />
|
|
49
|
-
<option name="TAB_SIZE" value="4" />
|
|
50
|
-
<option name="USE_TAB_CHARACTER" value="true" />
|
|
51
|
-
</indentOptions>
|
|
52
|
-
</codeStyleSettings>
|
|
53
|
-
</code_scheme>
|
|
54
|
-
</component>
|
package/.idea/misc.xml
DELETED
package/.idea/modules.xml
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="ProjectModuleManager">
|
|
4
|
-
<modules>
|
|
5
|
-
<module fileurl="file://$PROJECT_DIR$/.idea/proje-react-panel.iml" filepath="$PROJECT_DIR$/.idea/proje-react-panel.iml" />
|
|
6
|
-
</modules>
|
|
7
|
-
</component>
|
|
8
|
-
</project>
|
package/.idea/prettier.xml
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<module type="WEB_MODULE" version="4">
|
|
3
|
-
<component name="NewModuleRootManager">
|
|
4
|
-
<content url="file://$MODULE_DIR$">
|
|
5
|
-
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
6
|
-
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
7
|
-
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
8
|
-
</content>
|
|
9
|
-
<orderEntry type="inheritedJdk" />
|
|
10
|
-
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
-
</component>
|
|
12
|
-
</module>
|