refactor(advancedSearch): reorder exports and improve type definitions
refactor(types): reorganize SearchCondition and AdvancedSearchState interfaces refactor(filterPresets): streamline useFilterPresets hook and localStorage handling refactor(filtering): clean up ColumnFilterButton and ColumnFilterPopover components refactor(loading): separate GriddyLoadingOverlay from GriddyLoadingSkeleton refactor(searchHistory): enhance useSearchHistory hook with persistence refactor(index): update exports for adapters and core components refactor(rendering): improve EditableCell and TableCell components for clarity refactor(rendering): enhance TableHeader and VirtualBody components for better readability
This commit is contained in:
@@ -15,25 +15,33 @@ import {
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from '@tanstack/react-table'
|
||||
import React, { forwardRef, type Ref, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
} from '@tanstack/react-table';
|
||||
import React, {
|
||||
forwardRef,
|
||||
type Ref,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { GriddyProps, GriddyRef } from './types'
|
||||
import type { GriddyProps, GriddyRef } from './types';
|
||||
|
||||
import { AdvancedSearchPanel, advancedSearchGlobalFilterFn } from '../features/advancedSearch'
|
||||
import { GriddyErrorBoundary } from '../features/errorBoundary'
|
||||
import { useKeyboardNavigation } from '../features/keyboard/useKeyboardNavigation'
|
||||
import { GriddyLoadingOverlay, GriddyLoadingSkeleton } from '../features/loading'
|
||||
import { PaginationControl } from '../features/pagination'
|
||||
import { SearchOverlay } from '../features/search/SearchOverlay'
|
||||
import { GridToolbar } from '../features/toolbar'
|
||||
import { useGridVirtualizer } from '../rendering/hooks/useGridVirtualizer'
|
||||
import { TableHeader } from '../rendering/TableHeader'
|
||||
import { VirtualBody } from '../rendering/VirtualBody'
|
||||
import styles from '../styles/griddy.module.css'
|
||||
import { mapColumns } from './columnMapper'
|
||||
import { CSS, DEFAULTS } from './constants'
|
||||
import { GriddyProvider, useGriddyStore } from './GriddyStore'
|
||||
import { advancedSearchGlobalFilterFn, AdvancedSearchPanel } from '../features/advancedSearch';
|
||||
import { GriddyErrorBoundary } from '../features/errorBoundary';
|
||||
import { useKeyboardNavigation } from '../features/keyboard/useKeyboardNavigation';
|
||||
import { GriddyLoadingOverlay, GriddyLoadingSkeleton } from '../features/loading';
|
||||
import { PaginationControl } from '../features/pagination';
|
||||
import { SearchOverlay } from '../features/search/SearchOverlay';
|
||||
import { GridToolbar } from '../features/toolbar';
|
||||
import { useGridVirtualizer } from '../rendering/hooks/useGridVirtualizer';
|
||||
import { TableHeader } from '../rendering/TableHeader';
|
||||
import { VirtualBody } from '../rendering/VirtualBody';
|
||||
import styles from '../styles/griddy.module.css';
|
||||
import { mapColumns } from './columnMapper';
|
||||
import { CSS, DEFAULTS } from './constants';
|
||||
import { GriddyProvider, useGriddyStore } from './GriddyStore';
|
||||
|
||||
// ─── Inner Component (lives inside Provider, has store access) ───────────────
|
||||
|
||||
@@ -45,119 +53,119 @@ function _Griddy<T>(props: GriddyProps<T>, ref: Ref<GriddyRef<T>>) {
|
||||
</GriddyErrorBoundary>
|
||||
{props.children}
|
||||
</GriddyProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Component with forwardRef ──────────────────────────────────────────
|
||||
|
||||
function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
|
||||
// Read props from synced store
|
||||
const data = useGriddyStore((s) => s.data)
|
||||
const userColumns = useGriddyStore((s) => s.columns)
|
||||
const getRowId = useGriddyStore((s) => s.getRowId)
|
||||
const selection = useGriddyStore((s) => s.selection)
|
||||
const search = useGriddyStore((s) => s.search)
|
||||
const groupingConfig = useGriddyStore((s) => s.grouping)
|
||||
const paginationConfig = useGriddyStore((s) => s.pagination)
|
||||
const controlledSorting = useGriddyStore((s) => s.sorting)
|
||||
const onSortingChange = useGriddyStore((s) => s.onSortingChange)
|
||||
const controlledFilters = useGriddyStore((s) => s.columnFilters)
|
||||
const onColumnFiltersChange = useGriddyStore((s) => s.onColumnFiltersChange)
|
||||
const controlledPinning = useGriddyStore((s) => s.columnPinning)
|
||||
const onColumnPinningChange = useGriddyStore((s) => s.onColumnPinningChange)
|
||||
const controlledRowSelection = useGriddyStore((s) => s.rowSelection)
|
||||
const onRowSelectionChange = useGriddyStore((s) => s.onRowSelectionChange)
|
||||
const onEditCommit = useGriddyStore((s) => s.onEditCommit)
|
||||
const rowHeight = useGriddyStore((s) => s.rowHeight)
|
||||
const overscanProp = useGriddyStore((s) => s.overscan)
|
||||
const height = useGriddyStore((s) => s.height)
|
||||
const keyboardNavigation = useGriddyStore((s) => s.keyboardNavigation)
|
||||
const className = useGriddyStore((s) => s.className)
|
||||
const showToolbar = useGriddyStore((s) => s.showToolbar)
|
||||
const exportFilename = useGriddyStore((s) => s.exportFilename)
|
||||
const isLoading = useGriddyStore((s) => s.isLoading)
|
||||
const filterPresets = useGriddyStore((s) => s.filterPresets)
|
||||
const advancedSearch = useGriddyStore((s) => s.advancedSearch)
|
||||
const persistenceKey = useGriddyStore((s) => s.persistenceKey)
|
||||
const manualSorting = useGriddyStore((s) => s.manualSorting)
|
||||
const manualFiltering = useGriddyStore((s) => s.manualFiltering)
|
||||
const dataCount = useGriddyStore((s) => s.dataCount)
|
||||
const setTable = useGriddyStore((s) => s.setTable)
|
||||
const setVirtualizer = useGriddyStore((s) => s.setVirtualizer)
|
||||
const setScrollRef = useGriddyStore((s) => s.setScrollRef)
|
||||
const setFocusedRow = useGriddyStore((s) => s.setFocusedRow)
|
||||
const setFocusedColumn = useGriddyStore((s) => s.setFocusedColumn)
|
||||
const setEditing = useGriddyStore((s) => s.setEditing)
|
||||
const setTotalRows = useGriddyStore((s) => s.setTotalRows)
|
||||
const focusedRowIndex = useGriddyStore((s) => s.focusedRowIndex)
|
||||
const data = useGriddyStore((s) => s.data);
|
||||
const userColumns = useGriddyStore((s) => s.columns);
|
||||
const getRowId = useGriddyStore((s) => s.getRowId);
|
||||
const selection = useGriddyStore((s) => s.selection);
|
||||
const search = useGriddyStore((s) => s.search);
|
||||
const groupingConfig = useGriddyStore((s) => s.grouping);
|
||||
const paginationConfig = useGriddyStore((s) => s.pagination);
|
||||
const controlledSorting = useGriddyStore((s) => s.sorting);
|
||||
const onSortingChange = useGriddyStore((s) => s.onSortingChange);
|
||||
const controlledFilters = useGriddyStore((s) => s.columnFilters);
|
||||
const onColumnFiltersChange = useGriddyStore((s) => s.onColumnFiltersChange);
|
||||
const controlledPinning = useGriddyStore((s) => s.columnPinning);
|
||||
const onColumnPinningChange = useGriddyStore((s) => s.onColumnPinningChange);
|
||||
const controlledRowSelection = useGriddyStore((s) => s.rowSelection);
|
||||
const onRowSelectionChange = useGriddyStore((s) => s.onRowSelectionChange);
|
||||
const onEditCommit = useGriddyStore((s) => s.onEditCommit);
|
||||
const rowHeight = useGriddyStore((s) => s.rowHeight);
|
||||
const overscanProp = useGriddyStore((s) => s.overscan);
|
||||
const height = useGriddyStore((s) => s.height);
|
||||
const keyboardNavigation = useGriddyStore((s) => s.keyboardNavigation);
|
||||
const className = useGriddyStore((s) => s.className);
|
||||
const showToolbar = useGriddyStore((s) => s.showToolbar);
|
||||
const exportFilename = useGriddyStore((s) => s.exportFilename);
|
||||
const isLoading = useGriddyStore((s) => s.isLoading);
|
||||
const filterPresets = useGriddyStore((s) => s.filterPresets);
|
||||
const advancedSearch = useGriddyStore((s) => s.advancedSearch);
|
||||
const persistenceKey = useGriddyStore((s) => s.persistenceKey);
|
||||
const manualSorting = useGriddyStore((s) => s.manualSorting);
|
||||
const manualFiltering = useGriddyStore((s) => s.manualFiltering);
|
||||
const dataCount = useGriddyStore((s) => s.dataCount);
|
||||
const setTable = useGriddyStore((s) => s.setTable);
|
||||
const setVirtualizer = useGriddyStore((s) => s.setVirtualizer);
|
||||
const setScrollRef = useGriddyStore((s) => s.setScrollRef);
|
||||
const setFocusedRow = useGriddyStore((s) => s.setFocusedRow);
|
||||
const setFocusedColumn = useGriddyStore((s) => s.setFocusedColumn);
|
||||
const setEditing = useGriddyStore((s) => s.setEditing);
|
||||
const setTotalRows = useGriddyStore((s) => s.setTotalRows);
|
||||
const focusedRowIndex = useGriddyStore((s) => s.focusedRowIndex);
|
||||
|
||||
const effectiveRowHeight = rowHeight ?? DEFAULTS.rowHeight
|
||||
const effectiveOverscan = overscanProp ?? DEFAULTS.overscan
|
||||
const enableKeyboard = keyboardNavigation !== false
|
||||
const effectiveRowHeight = rowHeight ?? DEFAULTS.rowHeight;
|
||||
const effectiveOverscan = overscanProp ?? DEFAULTS.overscan;
|
||||
const enableKeyboard = keyboardNavigation !== false;
|
||||
|
||||
// ─── Column Mapping ───
|
||||
const columns = useMemo(
|
||||
() => mapColumns(userColumns ?? [], selection) as ColumnDef<T, any>[],
|
||||
[userColumns, selection],
|
||||
)
|
||||
[userColumns, selection]
|
||||
);
|
||||
|
||||
// ─── Table State (internal/uncontrolled) ───
|
||||
const [internalSorting, setInternalSorting] = useState<SortingState>([])
|
||||
const [internalFilters, setInternalFilters] = useState<ColumnFiltersState>([])
|
||||
const [internalRowSelection, setInternalRowSelection] = useState<RowSelectionState>({})
|
||||
const [globalFilter, setGlobalFilter] = useState<string | undefined>(undefined)
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([])
|
||||
const [internalSorting, setInternalSorting] = useState<SortingState>([]);
|
||||
const [internalFilters, setInternalFilters] = useState<ColumnFiltersState>([]);
|
||||
const [internalRowSelection, setInternalRowSelection] = useState<RowSelectionState>({});
|
||||
const [globalFilter, setGlobalFilter] = useState<string | undefined>(undefined);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([]);
|
||||
|
||||
// Build initial column pinning from column definitions
|
||||
const initialPinning = useMemo(() => {
|
||||
const left: string[] = []
|
||||
const right: string[] = []
|
||||
userColumns?.forEach(col => {
|
||||
if (col.pinned === 'left') left.push(col.id)
|
||||
else if (col.pinned === 'right') right.push(col.id)
|
||||
})
|
||||
return { left, right }
|
||||
}, [userColumns])
|
||||
const left: string[] = [];
|
||||
const right: string[] = [];
|
||||
userColumns?.forEach((col) => {
|
||||
if (col.pinned === 'left') left.push(col.id);
|
||||
else if (col.pinned === 'right') right.push(col.id);
|
||||
});
|
||||
return { left, right };
|
||||
}, [userColumns]);
|
||||
|
||||
const [internalPinning, setInternalPinning] = useState<ColumnPinningState>(initialPinning)
|
||||
const [grouping, setGrouping] = useState<GroupingState>(groupingConfig?.columns ?? [])
|
||||
const [expanded, setExpanded] = useState({})
|
||||
const [internalPinning, setInternalPinning] = useState<ColumnPinningState>(initialPinning);
|
||||
const [grouping, setGrouping] = useState<GroupingState>(groupingConfig?.columns ?? []);
|
||||
const [expanded, setExpanded] = useState({});
|
||||
const [internalPagination, setInternalPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: paginationConfig?.pageSize ?? DEFAULTS.pageSize,
|
||||
})
|
||||
});
|
||||
|
||||
// Wrap pagination setters to call callbacks
|
||||
const handlePaginationChange = (updater: any) => {
|
||||
setInternalPagination(prev => {
|
||||
const next = typeof updater === 'function' ? updater(prev) : updater
|
||||
setInternalPagination((prev) => {
|
||||
const next = typeof updater === 'function' ? updater(prev) : updater;
|
||||
// Call callbacks if pagination config exists
|
||||
if (paginationConfig) {
|
||||
if (next.pageIndex !== prev.pageIndex && paginationConfig.onPageChange) {
|
||||
paginationConfig.onPageChange(next.pageIndex)
|
||||
paginationConfig.onPageChange(next.pageIndex);
|
||||
}
|
||||
if (next.pageSize !== prev.pageSize && paginationConfig.onPageSizeChange) {
|
||||
paginationConfig.onPageSizeChange(next.pageSize)
|
||||
paginationConfig.onPageSizeChange(next.pageSize);
|
||||
}
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Resolve controlled vs uncontrolled
|
||||
const sorting = controlledSorting ?? internalSorting
|
||||
const setSorting = onSortingChange ?? setInternalSorting
|
||||
const columnFilters = controlledFilters ?? internalFilters
|
||||
const setColumnFilters = onColumnFiltersChange ?? setInternalFilters
|
||||
const columnPinning = controlledPinning ?? internalPinning
|
||||
const setColumnPinning = onColumnPinningChange ?? setInternalPinning
|
||||
const rowSelectionState = controlledRowSelection ?? internalRowSelection
|
||||
const setRowSelection = onRowSelectionChange ?? setInternalRowSelection
|
||||
const sorting = controlledSorting ?? internalSorting;
|
||||
const setSorting = onSortingChange ?? setInternalSorting;
|
||||
const columnFilters = controlledFilters ?? internalFilters;
|
||||
const setColumnFilters = onColumnFiltersChange ?? setInternalFilters;
|
||||
const columnPinning = controlledPinning ?? internalPinning;
|
||||
const setColumnPinning = onColumnPinningChange ?? setInternalPinning;
|
||||
const rowSelectionState = controlledRowSelection ?? internalRowSelection;
|
||||
const setRowSelection = onRowSelectionChange ?? setInternalRowSelection;
|
||||
|
||||
// ─── Selection config ───
|
||||
const enableRowSelection = selection ? selection.mode !== 'none' : false
|
||||
const enableMultiRowSelection = selection?.mode === 'multi'
|
||||
const enableRowSelection = selection ? selection.mode !== 'none' : false;
|
||||
const enableMultiRowSelection = selection?.mode === 'multi';
|
||||
|
||||
// ─── TanStack Table Instance ───
|
||||
const table = useReactTable<T>({
|
||||
@@ -177,7 +185,7 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
|
||||
getExpandedRowModel: getExpandedRowModel(),
|
||||
getFilteredRowModel: manualFiltering ? undefined : getFilteredRowModel(),
|
||||
getGroupedRowModel: groupingConfig?.enabled ? getGroupedRowModel() : undefined,
|
||||
getRowId: getRowId as any ?? ((_, index) => String(index)),
|
||||
getRowId: (getRowId as any) ?? ((_, index) => String(index)),
|
||||
getSortedRowModel: manualSorting ? undefined : getSortedRowModel(),
|
||||
manualFiltering: manualFiltering ?? false,
|
||||
manualSorting: manualSorting ?? false,
|
||||
@@ -206,10 +214,10 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
|
||||
},
|
||||
...(paginationConfig?.enabled ? { getPaginationRowModel: getPaginationRowModel() } : {}),
|
||||
columnResizeMode: 'onChange',
|
||||
})
|
||||
});
|
||||
|
||||
// ─── Scroll Container Ref ───
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// ─── TanStack Virtual ───
|
||||
const virtualizer = useGridVirtualizer({
|
||||
@@ -217,16 +225,22 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
|
||||
rowHeight: effectiveRowHeight,
|
||||
scrollRef,
|
||||
table,
|
||||
})
|
||||
});
|
||||
|
||||
// ─── Sync table + virtualizer + scrollRef into store ───
|
||||
useEffect(() => { setTable(table) }, [table, setTable])
|
||||
useEffect(() => { setVirtualizer(virtualizer) }, [virtualizer, setVirtualizer])
|
||||
useEffect(() => { setScrollRef(scrollRef.current) }, [setScrollRef])
|
||||
useEffect(() => {
|
||||
setTable(table);
|
||||
}, [table, setTable]);
|
||||
useEffect(() => {
|
||||
setVirtualizer(virtualizer);
|
||||
}, [virtualizer, setVirtualizer]);
|
||||
useEffect(() => {
|
||||
setScrollRef(scrollRef.current);
|
||||
}, [setScrollRef]);
|
||||
|
||||
// ─── Keyboard Navigation ───
|
||||
// Get the full store state for imperative access in keyboard handler
|
||||
const storeState = useGriddyStore()
|
||||
const storeState = useGriddyStore();
|
||||
|
||||
useKeyboardNavigation({
|
||||
editingEnabled: !!onEditCommit,
|
||||
@@ -236,59 +250,64 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
|
||||
storeState,
|
||||
table,
|
||||
virtualizer,
|
||||
})
|
||||
});
|
||||
|
||||
// ─── Set initial focus when data loads ───
|
||||
const rowCount = table.getRowModel().rows.length
|
||||
const rowCount = table.getRowModel().rows.length;
|
||||
|
||||
useEffect(() => {
|
||||
setTotalRows(rowCount)
|
||||
setTotalRows(rowCount);
|
||||
if (rowCount > 0 && focusedRowIndex === null) {
|
||||
setFocusedRow(0)
|
||||
setFocusedRow(0);
|
||||
}
|
||||
}, [rowCount, focusedRowIndex, setTotalRows, setFocusedRow])
|
||||
}, [rowCount, focusedRowIndex, setTotalRows, setFocusedRow]);
|
||||
|
||||
// ─── Imperative Ref ───
|
||||
useImperativeHandle(tableRef, () => ({
|
||||
deselectAll: () => table.resetRowSelection(),
|
||||
focusRow: (index: number) => {
|
||||
setFocusedRow(index)
|
||||
virtualizer.scrollToIndex(index, { align: 'auto' })
|
||||
},
|
||||
getTable: () => table,
|
||||
getUIState: () => ({
|
||||
focusedColumnId: null,
|
||||
focusedRowIndex,
|
||||
isEditing: false,
|
||||
isSearchOpen: false,
|
||||
isSelecting: false,
|
||||
totalRows: rowCount,
|
||||
} as any),
|
||||
getVirtualizer: () => virtualizer,
|
||||
scrollToRow: (index: number) => virtualizer.scrollToIndex(index, { align: 'auto' }),
|
||||
selectRow: (id: string) => {
|
||||
const row = table.getRowModel().rows.find((r) => r.id === id)
|
||||
row?.toggleSelected(true)
|
||||
},
|
||||
startEditing: (rowId: string, columnId?: string) => {
|
||||
const rowIndex = table.getRowModel().rows.findIndex((r) => r.id === rowId)
|
||||
if (rowIndex >= 0) {
|
||||
setFocusedRow(rowIndex)
|
||||
if (columnId) setFocusedColumn(columnId)
|
||||
setEditing(true)
|
||||
}
|
||||
},
|
||||
}), [table, virtualizer, setFocusedRow, setFocusedColumn, setEditing, focusedRowIndex, rowCount])
|
||||
useImperativeHandle(
|
||||
tableRef,
|
||||
() => ({
|
||||
deselectAll: () => table.resetRowSelection(),
|
||||
focusRow: (index: number) => {
|
||||
setFocusedRow(index);
|
||||
virtualizer.scrollToIndex(index, { align: 'auto' });
|
||||
},
|
||||
getTable: () => table,
|
||||
getUIState: () =>
|
||||
({
|
||||
focusedColumnId: null,
|
||||
focusedRowIndex,
|
||||
isEditing: false,
|
||||
isSearchOpen: false,
|
||||
isSelecting: false,
|
||||
totalRows: rowCount,
|
||||
}) as any,
|
||||
getVirtualizer: () => virtualizer,
|
||||
scrollToRow: (index: number) => virtualizer.scrollToIndex(index, { align: 'auto' }),
|
||||
selectRow: (id: string) => {
|
||||
const row = table.getRowModel().rows.find((r) => r.id === id);
|
||||
row?.toggleSelected(true);
|
||||
},
|
||||
startEditing: (rowId: string, columnId?: string) => {
|
||||
const rowIndex = table.getRowModel().rows.findIndex((r) => r.id === rowId);
|
||||
if (rowIndex >= 0) {
|
||||
setFocusedRow(rowIndex);
|
||||
if (columnId) setFocusedColumn(columnId);
|
||||
setEditing(true);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[table, virtualizer, setFocusedRow, setFocusedColumn, setEditing, focusedRowIndex, rowCount]
|
||||
);
|
||||
|
||||
// ─── Render ───
|
||||
const containerStyle: React.CSSProperties = {
|
||||
height: height ?? '100%',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
}
|
||||
};
|
||||
|
||||
const focusedRow = focusedRowIndex !== null ? table.getRowModel().rows[focusedRowIndex] : null
|
||||
const focusedRowId = focusedRow ? `griddy-row-${focusedRow.id}` : undefined
|
||||
const focusedRow = focusedRowIndex !== null ? table.getRowModel().rows[focusedRowIndex] : null;
|
||||
const focusedRowId = focusedRow ? `griddy-row-${focusedRow.id}` : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -325,15 +344,12 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
|
||||
)}
|
||||
</div>
|
||||
{paginationConfig?.enabled && (
|
||||
<PaginationControl
|
||||
pageSizeOptions={paginationConfig.pageSizeOptions}
|
||||
table={table}
|
||||
/>
|
||||
<PaginationControl pageSizeOptions={paginationConfig.pageSizeOptions} table={table} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const Griddy = forwardRef(_Griddy) as <T>(
|
||||
props: GriddyProps<T> & React.RefAttributes<GriddyRef<T>>
|
||||
) => React.ReactElement
|
||||
) => React.ReactElement;
|
||||
|
||||
Reference in New Issue
Block a user