u
This commit is contained in:
@@ -32,9 +32,6 @@ import { artikelDetailPath } from '../components/article/articleRoute';
|
||||
import withSearchParams from '../components/article/withSearchParams';
|
||||
import { setLastArticleSku } from '../automats';
|
||||
import { BrandLabel } from '../components/brand';
|
||||
import {
|
||||
BLUE_BG,
|
||||
} from '../colors';
|
||||
|
||||
const resultRowSx = {
|
||||
alignItems: 'center',
|
||||
@@ -50,18 +47,31 @@ const resultRowSx = {
|
||||
|
||||
function ResultRowMeasureSample() {
|
||||
return (
|
||||
<ListItemButton tabIndex={-1} divider sx={resultRowSx}>
|
||||
<ListItemButton
|
||||
tabIndex={-1}
|
||||
divider
|
||||
disabled
|
||||
sx={{
|
||||
...resultRowSx,
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
opacity: 0.2,
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary="Measure"
|
||||
primary={'\u00A0'}
|
||||
secondary={
|
||||
<Typography component="span" variant="body2" color="text.secondary" noWrap>
|
||||
SKU · Bestand 0 · Verfügbar 0 · Kategorie
|
||||
<Typography component="span" variant="body2" sx={{ visibility: 'hidden' }} noWrap>
|
||||
|
||||
</Typography>
|
||||
}
|
||||
slotProps={{ primary: { fontWeight: 500, noWrap: true } }}
|
||||
/>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, flexShrink: 0, ml: 2 }}>
|
||||
0,00 €
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 600, flexShrink: 0, ml: 2, visibility: 'hidden' }}
|
||||
>
|
||||
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
);
|
||||
@@ -75,87 +85,35 @@ function initialSearchState(searchParams) {
|
||||
? query.target
|
||||
: (stored?.target?.trim().length >= 3 ? stored.target : query.target);
|
||||
|
||||
const automatResult = articleSearchAutomat.state.result;
|
||||
|
||||
return {
|
||||
input,
|
||||
error: null,
|
||||
categoryFacets: null,
|
||||
totalCount: null,
|
||||
recordset: null,
|
||||
pageSize: null,
|
||||
error: automatResult.error,
|
||||
categoryFacets: automatResult.categoryFacets,
|
||||
totalCount: automatResult.totalCount,
|
||||
recordset: automatResult.recordset,
|
||||
pageSize: automatResult.pageSize,
|
||||
pageCount: automatResult.pageCount,
|
||||
currentPage: automatResult.currentPage,
|
||||
offset: automatResult.offset,
|
||||
};
|
||||
}
|
||||
|
||||
class ArtikelSearchPage extends Component {
|
||||
state = initialSearchState(this.props.searchParams);
|
||||
listSlotRef = { current: null };
|
||||
rowMeasureRef = { current: null };
|
||||
paginationRef = createRef();
|
||||
|
||||
setListSlotRef = (node) => {
|
||||
if (this.listSlotRef.current === node) return;
|
||||
this.listSlotRef.current = node;
|
||||
if (!node) return;
|
||||
this.updatePageSize();
|
||||
};
|
||||
|
||||
setRowMeasureRef = (node) => {
|
||||
if (this.rowMeasureRef.current === node) return;
|
||||
this.rowMeasureRef.current = node;
|
||||
if (!node) return;
|
||||
this.updatePageSize();
|
||||
};
|
||||
|
||||
measureRowHeight = () => {
|
||||
// Always use the sample — real rows used to report a different height after paint
|
||||
// (wrapped text), which made resize "fix" a wrong first measure.
|
||||
const sample = this.rowMeasureRef.current;
|
||||
if (!sample) return { height: 0, source: 'none' };
|
||||
return {
|
||||
height: sample.getBoundingClientRect().height,
|
||||
source: 'sample',
|
||||
};
|
||||
};
|
||||
|
||||
updatePageSize = ({ fromResize = false } = {}) => {
|
||||
const slot = this.listSlotRef.current;
|
||||
if (!slot) return;
|
||||
|
||||
// After the first measure, ignore layout churn from painting results.
|
||||
// Only window resize may change pageSize and refetch.
|
||||
if (this.state.pageSize != null && !fromResize) return;
|
||||
|
||||
const slotTop = slot.getBoundingClientRect().top;
|
||||
const available = window.innerHeight - slotTop;
|
||||
const { height: rowHeight, source: rowSource } = this.measureRowHeight();
|
||||
if (available < 1 || rowHeight < 1) return;
|
||||
|
||||
// Outlined Paper around the list is 1px top + 1px bottom — not in row height.
|
||||
const listChrome = 2;
|
||||
const next = Math.max(1, Math.floor((available - listChrome) / rowHeight));
|
||||
|
||||
console.log('[pageSize] measure', {
|
||||
fromResize,
|
||||
prevPageSize: this.state.pageSize,
|
||||
next,
|
||||
available,
|
||||
rowHeight,
|
||||
rowSource,
|
||||
listChrome,
|
||||
slotTop,
|
||||
realRowHeight:
|
||||
slot.querySelector('[data-result-row]')?.getBoundingClientRect().height ?? null,
|
||||
});
|
||||
|
||||
if (next === this.state.pageSize) return;
|
||||
|
||||
this.setState({ pageSize: next }, () => {
|
||||
const target = this.props.searchParams.get('target') ?? '';
|
||||
if (target) this.fetchResults();
|
||||
});
|
||||
};
|
||||
|
||||
handleWindowResize = () => {
|
||||
this.updatePageSize({ fromResize: true });
|
||||
handleMeasurement = (count) => {
|
||||
if (!count || count === this.state.pageSize) return;
|
||||
this.setState({ pageSize: count });
|
||||
const { target, filter, offset } = queryFromParams(this.props.searchParams);
|
||||
void articleSearchAutomat.actions.search({
|
||||
target: target || this.state.input,
|
||||
filter,
|
||||
offset,
|
||||
pageSize: count,
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
persistQuery = () => {
|
||||
@@ -170,19 +128,14 @@ class ArtikelSearchPage extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mouseup', preserveSearchFocus, true);
|
||||
window.addEventListener('resize', this.handleWindowResize);
|
||||
setLastArticleSku(null);
|
||||
|
||||
// Push search results into local component state.
|
||||
this.unsubSearch = articleSearchAutomat.subscribe(this, (state) => state.result);
|
||||
|
||||
// Kick off initial search (empty input = show all products).
|
||||
void articleSearchAutomat.actions.search(this.state.input).catch(() => {});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mouseup', preserveSearchFocus, true);
|
||||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
this.unsubSearch?.();
|
||||
this.unsubSearch = null;
|
||||
this.persistQuery();
|
||||
@@ -221,7 +174,16 @@ class ArtikelSearchPage extends Component {
|
||||
handleInputChange = (event) => {
|
||||
const value = event.target.value;
|
||||
this.setState({ input: value });
|
||||
void articleSearchAutomat.actions.search(value).catch(() => {});
|
||||
const { filter } = queryFromParams(this.props.searchParams);
|
||||
const { pageSize } = this.state;
|
||||
if (pageSize != null) {
|
||||
void articleSearchAutomat.actions.search({
|
||||
target: value,
|
||||
filter,
|
||||
offset: 0,
|
||||
pageSize,
|
||||
}).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
fetchResults = () => {
|
||||
@@ -232,13 +194,36 @@ class ArtikelSearchPage extends Component {
|
||||
|
||||
writeArtikelQuery({ target, filter, offset });
|
||||
|
||||
void articleSearchAutomat.actions.search(target.trim()).catch(() => {});
|
||||
void articleSearchAutomat.actions.search({
|
||||
target: target.trim(),
|
||||
filter,
|
||||
offset,
|
||||
pageSize,
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
handlePageChange = (_e, page) => {
|
||||
const { pageSize } = this.state;
|
||||
const safePageSize = pageSize ?? 1;
|
||||
const nextOffset = (page - 1) * safePageSize;
|
||||
const filterParam = this.props.searchParams.get('filter') ?? '';
|
||||
this.updateParams(this.state.input, filterParam, nextOffset);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { input, error, categoryFacets, totalCount, recordset, pageSize } = this.state;
|
||||
const {
|
||||
input,
|
||||
error,
|
||||
categoryFacets,
|
||||
totalCount,
|
||||
recordset,
|
||||
pageSize,
|
||||
pageCount,
|
||||
currentPage,
|
||||
offset: stateOffset,
|
||||
} = this.state;
|
||||
const filterParam = this.props.searchParams.get('filter') ?? '';
|
||||
const offset = Math.max(0, Number(this.props.searchParams.get('offset')) || 0);
|
||||
const offset = Math.max(0, Number(this.props.searchParams.get('offset')) || stateOffset || 0);
|
||||
const categories = parseFilter(filterParam);
|
||||
// Drive UI from input so hint vs results switch in one paint (URL lags one tick).
|
||||
const searchActive = input.trim().length > 0 || (recordset ?? []).length > 0;
|
||||
@@ -256,16 +241,9 @@ class ArtikelSearchPage extends Component {
|
||||
.sort((a, b) => Number(a.id) - Number(b.id))
|
||||
: [];
|
||||
|
||||
const safePageSize = pageSize ?? 1;
|
||||
const pageCount = Math.max(1, Math.ceil((totalCount ?? 0) / safePageSize));
|
||||
const currentPage = Math.floor(offset / safePageSize) + 1;
|
||||
const showPagination =
|
||||
rows.length > 0 && pageSize != null && totalCount != null && totalCount > pageSize;
|
||||
|
||||
const handlePageChange = (_e, page) => {
|
||||
this.updateParams(input, filterParam, (page - 1) * safePageSize);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ mb: 3, maxWidth: 720 }}>
|
||||
@@ -340,16 +318,6 @@ class ArtikelSearchPage extends Component {
|
||||
)}
|
||||
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0, mb: -4 }}>
|
||||
{/* Same column width as the list; invisible, zero layout footprint. */}
|
||||
<Box aria-hidden sx={{ height: 0, overflow: 'hidden', width: '100%' }}>
|
||||
<Box
|
||||
ref={this.setRowMeasureRef}
|
||||
sx={{ visibility: 'hidden', pointerEvents: 'none' }}
|
||||
>
|
||||
<ResultRowMeasureSample />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{searchActive && !error && totalCount === 0 && (
|
||||
<Typography color="text.secondary">Keine Artikel gefunden.</Typography>
|
||||
)}
|
||||
@@ -363,29 +331,17 @@ class ArtikelSearchPage extends Component {
|
||||
pageSize={pageSize ?? 1}
|
||||
pageCount={showPagination ? pageCount : 5}
|
||||
currentPage={showPagination ? currentPage : 1}
|
||||
onPageChange={handlePageChange}
|
||||
onPageChange={this.handlePageChange}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<FillSpace
|
||||
aboveRef={this.paginationRef}
|
||||
debug
|
||||
onMeasurement={this.handleMeasurement}
|
||||
sx={{ overflow: 'auto' }}
|
||||
// Temporary dummy unit so the repeating fill is visible.
|
||||
measureElement={
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: BLUE_BG,
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">Einheit</Typography>
|
||||
</Box>
|
||||
}
|
||||
measureElement={<ResultRowMeasureSample />}
|
||||
>
|
||||
<Box ref={this.setListSlotRef}>
|
||||
<Box>
|
||||
{rows.length > 0 && (
|
||||
<List component={Paper} variant="outlined" disablePadding>
|
||||
{rows.map((row, index) => (
|
||||
|
||||
Reference in New Issue
Block a user