Split Filters logic
This commit is contained in:
@@ -182,7 +182,6 @@ export function useApiDocumentation(
|
|||||||
throw new Error(`$ref not found in response schema for ${pathKey}`);
|
throw new Error(`$ref not found in response schema for ${pathKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(schemaRef.match(/#\/definitions\/(\w+)/))
|
|
||||||
|
|
||||||
// Получить название определения
|
// Получить название определения
|
||||||
const defNameMatch = schemaRef.match(/#\/definitions\/(\w+)/);
|
const defNameMatch = schemaRef.match(/#\/definitions\/(\w+)/);
|
||||||
|
|||||||
@@ -1,190 +1,22 @@
|
|||||||
import {useState, useMemo, useEffect} from 'react'
|
|
||||||
import { FieldInfo } from '../types/common'
|
import { FieldInfo } from '../types/common'
|
||||||
|
import {VisibilityState} from "../types/filters.ts";
|
||||||
|
|
||||||
interface FilteredTableProps {
|
interface FilteredTableProps {
|
||||||
schema: FieldInfo[]
|
schema: FieldInfo[]
|
||||||
data: object[]
|
filteredData: object[]
|
||||||
showFilters?: boolean
|
visibleColumns: VisibilityState
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Компонент динамической фильтрованной таблицы.
|
* Компонент динамической фильтрованной таблицы.
|
||||||
* Позволяет фильтровать данные по различным условиям, в зависимости от типа поля.
|
* Позволяет фильтровать данные по различным условиям, в зависимости от типа поля.
|
||||||
*/
|
*/
|
||||||
export function FilteredTable({ schema, data, showFilters = false }: FilteredTableProps) {
|
export function FilteredTable({ schema, filteredData, visibleColumns}: FilteredTableProps) {
|
||||||
const [filters, setFilters] = useState<Record<string, unknown>>({})
|
|
||||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>({})
|
|
||||||
|
|
||||||
// Изначальный массив данных для фильтрации
|
|
||||||
const initialData = useMemo(() => data, [data])
|
|
||||||
|
|
||||||
// Массив отфильтрованных данных
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
return initialData.filter(item => {
|
|
||||||
return schema.every(field => {
|
|
||||||
const value = item[field.name]
|
|
||||||
const filter = filters[field.name]
|
|
||||||
if (!filter) return true
|
|
||||||
|
|
||||||
// Обработка по типу
|
|
||||||
if (field.type === 'string') {
|
|
||||||
if (filter.searchText) {
|
|
||||||
if (typeof value !== 'string') return false
|
|
||||||
if (!value.toLowerCase().includes(filter.searchText.toLowerCase())) return false
|
|
||||||
}
|
|
||||||
if (filter.exactMatch && filter.searchText) {
|
|
||||||
return value.toLowerCase() === filter.searchText.toLowerCase()
|
|
||||||
|
|
||||||
}
|
|
||||||
if (filter.empty !== undefined) {
|
|
||||||
const isEmpty = value === '' || value === null || value === undefined
|
|
||||||
if (filter.empty && !isEmpty) return false
|
|
||||||
if (filter.notEmpty && isEmpty) return false
|
|
||||||
}
|
|
||||||
} else if (field.type === 'date') {
|
|
||||||
const dateValue = new Date(value).getTime()
|
|
||||||
if (filter.fromDate) {
|
|
||||||
const fromTime = new Date(filter.fromDate).getTime()
|
|
||||||
if (dateValue < fromTime) return false
|
|
||||||
}
|
|
||||||
if (filter.toDate) {
|
|
||||||
const toTime = new Date(filter.toDate).getTime()
|
|
||||||
if (dateValue > toTime) return false
|
|
||||||
}
|
|
||||||
} else if (field.type === 'number') {
|
|
||||||
const numValue = Number(value)
|
|
||||||
if (filter.gte !== undefined && numValue < filter.gte) return false
|
|
||||||
if (filter.lte !== undefined && numValue > filter.lte) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, [initialData, filters, schema])
|
|
||||||
|
|
||||||
// Обработчик изменения фильтров
|
|
||||||
const handleFilterChange = (name: string, filterPart: unknown) => {
|
|
||||||
setFilters(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: {
|
|
||||||
...prev[name],
|
|
||||||
...filterPart
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчик переключения видимости колонок
|
|
||||||
const handleColumnToggle = (name: string) => {
|
|
||||||
setVisibleColumns(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: !prev[name]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
let newVisibleColumns:Record<string, boolean> = {}
|
|
||||||
|
|
||||||
for (const field of schema) {
|
|
||||||
newVisibleColumns = {...newVisibleColumns, [field.name]:true}
|
|
||||||
|
|
||||||
}
|
|
||||||
setVisibleColumns(newVisibleColumns)
|
|
||||||
}, [schema]);
|
|
||||||
|
|
||||||
console.log(visibleColumns)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="filtered-table">
|
<div className="filtered-table">
|
||||||
{showFilters && (
|
|
||||||
<div className="filters-block">
|
|
||||||
{schema.map(field => (
|
|
||||||
<div key={field.name} className="filter-item">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={visibleColumns[field.name]}
|
|
||||||
onChange={() => handleColumnToggle(field.name)}
|
|
||||||
/>
|
|
||||||
{field.title}
|
|
||||||
</label>
|
|
||||||
{field.type === 'string' && (
|
|
||||||
<div className="string-filters">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Поиск"
|
|
||||||
onChange={(e) => handleFilterChange(field.name, {searchText: e.target.value})}
|
|
||||||
/>
|
|
||||||
<label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters[field.name]?.exactMatch || false}
|
|
||||||
onChange={() => handleFilterChange(field.name, {exactMatch: !filters[field.name]?.exactMatch})}
|
|
||||||
/> Строгое совпадение
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters[field.name]?.notEmpty || false}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleFilterChange(field.name, {
|
|
||||||
notEmpty: e.target.checked,
|
|
||||||
empty: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>Не пустые
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters[field.name]?.empty || false}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleFilterChange(field.name, {
|
|
||||||
empty: e.target.checked,
|
|
||||||
notEmpty: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>Пустые
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{field.type === 'date' && (
|
|
||||||
<div className="date-filters">
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
onChange={(e) => handleFilterChange(field.name, { fromDate: e.target.value })}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
onChange={(e) => handleFilterChange(field.name, { toDate: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{field.type === 'number' && (
|
|
||||||
<div className="number-filters">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="От"
|
|
||||||
onChange={(e) =>
|
|
||||||
handleFilterChange(field.name, { gte: e.target.value !== '' ? Number(e.target.value) : undefined })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="До"
|
|
||||||
onChange={(e) =>
|
|
||||||
handleFilterChange(field.name, { lte: e.target.value !== '' ? Number(e.target.value) : undefined })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<table className="filtered-table-main" style={{ overflowX: 'auto', display: 'block' }}>
|
<table className="filtered-table-main" style={{ overflowX: 'auto', display: 'block' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -200,13 +32,13 @@ export function FilteredTable({ schema, data, showFilters = false }: FilteredTab
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item, i) => (
|
filteredData.map((item) => (
|
||||||
<tr key={item['id'] || i}>
|
<tr key={(item as {id: number}).id}>
|
||||||
{schema.map(
|
{schema.map(
|
||||||
(field) =>
|
(field) =>
|
||||||
visibleColumns[field.name] && (
|
visibleColumns[field.name] && (
|
||||||
<td key={field.name} className="data-cell">
|
<td key={field.name} className="data-cell">
|
||||||
{item[field.name] || "-"}
|
{item[field.name] ?? "-"}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
107
src/components/Filters.tsx
Normal file
107
src/components/Filters.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import {FieldInfo} from "../types/common.ts";
|
||||||
|
import {FiltersState, VisibilityState} from "../types/filters.ts";
|
||||||
|
|
||||||
|
interface FiltersProps{
|
||||||
|
schema: FieldInfo[];
|
||||||
|
visibleColumns: VisibilityState;
|
||||||
|
handleColumnToggle(name: string): void;
|
||||||
|
handleFilterChange(name: string, filterPart: unknown):void;
|
||||||
|
filters: FiltersState;
|
||||||
|
isShow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Filters({schema, visibleColumns, handleColumnToggle, handleFilterChange, filters, isShow=false}:FiltersProps ){
|
||||||
|
|
||||||
|
if (!isShow){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="filters-block">
|
||||||
|
{schema.map(field => (
|
||||||
|
<div key={field.name} className="filter-item">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={visibleColumns[field.name]}
|
||||||
|
onChange={() => handleColumnToggle(field.name)}
|
||||||
|
/>
|
||||||
|
{field.title}
|
||||||
|
</label>
|
||||||
|
{field.type === 'string' && (
|
||||||
|
<div className="string-filters">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Поиск"
|
||||||
|
onChange={(e) => handleFilterChange(field.name, {searchText: e.target.value})}
|
||||||
|
/>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filters[field.name]?.exactMatch || false}
|
||||||
|
onChange={() => handleFilterChange(field.name, {exactMatch: !filters[field.name]?.exactMatch})}
|
||||||
|
/> Строгое совпадение
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filters[field.name]?.notEmpty || false}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFilterChange(field.name, {
|
||||||
|
notEmpty: e.target.checked,
|
||||||
|
empty: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/> Не пустые
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filters[field.name]?.empty || false}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFilterChange(field.name, {
|
||||||
|
empty: e.target.checked,
|
||||||
|
notEmpty: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/> Пустые
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{field.type === 'date' && (
|
||||||
|
<div className="date-filters">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
onChange={(e) => handleFilterChange(field.name, {fromDate: e.target.value})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
onChange={(e) => handleFilterChange(field.name, {toDate: e.target.value})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{field.type === 'number' && (
|
||||||
|
<div className="number-filters">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="От"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFilterChange(field.name, {gte: e.target.value !== '' ? Number(e.target.value) : undefined})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="До"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFilterChange(field.name, {lte: e.target.value !== '' ? Number(e.target.value) : undefined})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/hooks/useTableFilters.ts
Normal file
104
src/hooks/useTableFilters.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
import { useState, useMemo, useCallback } from 'react'
|
||||||
|
import { FieldInfo } from '../types/common'
|
||||||
|
import {FiltersState, VisibilityState} from "../types/filters.ts";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хук для управления фильтрами и видимостью колонок таблицы.
|
||||||
|
* @param schema Схема полей таблицы.
|
||||||
|
* @param initialData
|
||||||
|
* @returns Объекты состояния фильтров и видимости, а также функции для их изменения.
|
||||||
|
*/
|
||||||
|
export function useTableFilters(schema: FieldInfo[]| null, initialData: unknown[] | null) {
|
||||||
|
const [filters, setFilters] = useState<FiltersState>({})
|
||||||
|
const [visibleColumns, setVisibleColumns] = useState<VisibilityState>({})
|
||||||
|
|
||||||
|
// Инициализация видимости колонок
|
||||||
|
useMemo(() => {
|
||||||
|
if (!schema){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialVisibility: VisibilityState = {}
|
||||||
|
schema.forEach(field => {
|
||||||
|
initialVisibility[field.name] = true
|
||||||
|
})
|
||||||
|
setVisibleColumns(initialVisibility)
|
||||||
|
}, [schema])
|
||||||
|
|
||||||
|
// Обновление фильтров
|
||||||
|
const handleFilterChange = useCallback((name: string, filterPart: unknown) => {
|
||||||
|
setFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: {
|
||||||
|
...prev[name],
|
||||||
|
...filterPart
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Переключатель видимости колонки
|
||||||
|
const handleColumnToggle = useCallback((name: string) => {
|
||||||
|
setVisibleColumns(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: !prev[name]
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Логика фильтрации данных, вынесенная из компонента
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (!initialData || !schema){
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
return initialData.filter(item => {
|
||||||
|
return schema.every(field => {
|
||||||
|
const value = item[field.name]
|
||||||
|
const filter = filters[field.name]
|
||||||
|
if (!filter) return true
|
||||||
|
|
||||||
|
// Обработка по типу
|
||||||
|
if (field.type === 'string') {
|
||||||
|
if (filter.searchText) {
|
||||||
|
if (typeof value !== 'string') return false
|
||||||
|
if (!value.toLowerCase().includes(filter.searchText.toLowerCase())) return false
|
||||||
|
}
|
||||||
|
if (filter.exactMatch && filter.searchText) {
|
||||||
|
return value.toLowerCase() === filter.searchText.toLowerCase()
|
||||||
|
}
|
||||||
|
if (filter.empty !== undefined) {
|
||||||
|
const isEmpty = value === '' || value === null || value === undefined
|
||||||
|
if (filter.empty && !isEmpty) return false
|
||||||
|
if (filter.notEmpty && isEmpty) return false
|
||||||
|
}
|
||||||
|
} else if (field.format === 'date') {
|
||||||
|
const dateValue = new Date(value as string).getTime()
|
||||||
|
if (filter.fromDate) {
|
||||||
|
const fromTime = new Date(filter.fromDate).getTime()
|
||||||
|
if (dateValue < fromTime) return false
|
||||||
|
}
|
||||||
|
if (filter.toDate) {
|
||||||
|
const toTime = new Date(filter.toDate).getTime()
|
||||||
|
if (dateValue > toTime) return false
|
||||||
|
}
|
||||||
|
} else if (field.type === 'number') {
|
||||||
|
const numValue = Number(value)
|
||||||
|
if (filter.gte !== undefined && numValue < filter.gte) return false
|
||||||
|
if (filter.lte !== undefined && numValue > filter.lte) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [initialData, filters, schema])
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
visibleColumns,
|
||||||
|
setVisibleColumns,
|
||||||
|
handleFilterChange,
|
||||||
|
handleColumnToggle,
|
||||||
|
filteredData
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import { FieldInfo } from "../../types/common.ts";
|
|||||||
import {FilteredTable} from "../../components/FilteredTable.tsx";
|
import {FilteredTable} from "../../components/FilteredTable.tsx";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {LoadingData} from "../../components/LoadingData.tsx";
|
import {LoadingData} from "../../components/LoadingData.tsx";
|
||||||
|
import {useTableFilters} from "../../hooks/useTableFilters.ts";
|
||||||
|
import {Filters} from "../../components/Filters.tsx";
|
||||||
|
|
||||||
export function DataPage() {
|
export function DataPage() {
|
||||||
const { applicationName, endpointName } = useParams<{ applicationName: string; endpointName: string }>();
|
const { applicationName, endpointName } = useParams<{ applicationName: string; endpointName: string }>();
|
||||||
@@ -13,7 +15,13 @@ export function DataPage() {
|
|||||||
|
|
||||||
const [filterShow, setFilterShow] = useState(false);
|
const [filterShow, setFilterShow] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
handleFilterChange,
|
||||||
|
visibleColumns,
|
||||||
|
handleColumnToggle,
|
||||||
|
filteredData
|
||||||
|
} = useTableFilters(schema, dataPage.items)
|
||||||
|
|
||||||
// Функция для экспорта данных в Excel (пока пустая)
|
// Функция для экспорта данных в Excel (пока пустая)
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
@@ -22,7 +30,7 @@ export function DataPage() {
|
|||||||
|
|
||||||
// Функция для добавления новых данных (пока пустая)
|
// Функция для добавления новых данных (пока пустая)
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
// Логика для добавления данных
|
alert("Недостаточно прав!")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для фильтрации данных (пока пустая)
|
// Функция для фильтрации данных (пока пустая)
|
||||||
@@ -43,10 +51,14 @@ export function DataPage() {
|
|||||||
<button className="filters-button" onClick={handleFilters}>Фильтры</button>
|
<button className="filters-button" onClick={handleFilters}>Фильтры</button>
|
||||||
</div>
|
</div>
|
||||||
<LoadingData data={dataPage.items}>
|
<LoadingData data={dataPage.items}>
|
||||||
|
|
||||||
|
<Filters schema={schema as FieldInfo[]} visibleColumns={visibleColumns} handleColumnToggle={handleColumnToggle} handleFilterChange={handleFilterChange} filters={filters} isShow={filterShow}/>
|
||||||
|
|
||||||
|
|
||||||
<FilteredTable
|
<FilteredTable
|
||||||
schema={schema as FieldInfo[]}
|
schema={schema as FieldInfo[]}
|
||||||
data={dataPage.items as object[]}
|
visibleColumns={visibleColumns}
|
||||||
showFilters={filterShow}
|
filteredData={filteredData as object[]}
|
||||||
/>
|
/>
|
||||||
</LoadingData>
|
</LoadingData>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
3
src/types/filters.ts
Normal file
3
src/types/filters.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type FiltersState = Record<string, unknown>
|
||||||
|
export type VisibilityState = Record<string, boolean>
|
||||||
|
|
||||||
Reference in New Issue
Block a user