u
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - UI / Search input / FillSpace: Components call `actions.search({ target, filter, offset, pageSize })`.
|
||||
* - Reads data from `articleIndexAutomat`: Ensures the derived index is available
|
||||
* before filtering and slicing.
|
||||
* - Reads data from `articleIndexAutomat`: Derives and slices matching records
|
||||
* synchronously, updating continuously as category views load during launch.
|
||||
* - `categoryAutomat`: Provides category hierarchy metadata for faceted counts.
|
||||
*/
|
||||
|
||||
@@ -34,7 +34,7 @@ import { parseFilter } from '../components/article/searchFormat';
|
||||
* @property {string} filter Active category filter string.
|
||||
* @property {number} offset Current 0-based offset.
|
||||
* @property {number|null} pageSize Current page size.
|
||||
* @property {boolean} loading Whether a search is in flight.
|
||||
* @property {boolean} loading Whether a search or index load is in flight.
|
||||
*/
|
||||
|
||||
/** @type {ArticleSearchResult} */
|
||||
@@ -130,108 +130,121 @@ function buildCategoryFacets(matchedArticles) {
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Runs a client-side search over the article index with pagination.
|
||||
* Executes in-memory search and derivation synchronously.
|
||||
*
|
||||
* @param {string|object} targetOrParams Search term or options object.
|
||||
* @param {object} [options] Secondary options when first argument is target string.
|
||||
* @returns {Promise<ArticleSearchResult>} Search outcome.
|
||||
* @param {object} [params] Search parameters to merge.
|
||||
* @returns {ArticleSearchResult} Search outcome.
|
||||
*/
|
||||
async search(targetOrParams, options = {}) {
|
||||
runSearch(params = {}) {
|
||||
const currentState = articleSearchAutomat.state;
|
||||
const params = typeof targetOrParams === 'object' && targetOrParams !== null
|
||||
? targetOrParams
|
||||
: { target: targetOrParams, ...options };
|
||||
|
||||
const target = params.target !== undefined ? String(params.target ?? '') : currentState.target;
|
||||
const filter = params.filter !== undefined ? String(params.filter ?? '') : currentState.filter;
|
||||
const pageSize = params.pageSize !== undefined ? params.pageSize : currentState.pageSize;
|
||||
const offset = params.offset !== undefined ? Math.max(0, Number(params.offset) || 0) : currentState.offset;
|
||||
|
||||
articleSearchAutomat.setState({ target, filter, offset, pageSize, loading: true });
|
||||
try {
|
||||
const articles = await articleIndexAutomat.actions.ensureLoaded();
|
||||
const query = target.trim().toLowerCase();
|
||||
const articles = articleIndexAutomat.state.items ?? [];
|
||||
const query = target.trim().toLowerCase();
|
||||
|
||||
// Filter by text query across all tokens
|
||||
const textMatches = query
|
||||
? articles.filter((row) => matches(row, query))
|
||||
: articles;
|
||||
// Filter by text query across all tokens
|
||||
const textMatches = query
|
||||
? articles.filter((row) => matches(row, query))
|
||||
: articles;
|
||||
|
||||
// Build facets for all text matches (before category filtering)
|
||||
const categoryFacets = buildCategoryFacets(textMatches);
|
||||
// Build facets for all text matches (before category filtering)
|
||||
const categoryFacets = buildCategoryFacets(textMatches);
|
||||
|
||||
// Apply category filter if set
|
||||
const selectedCategories = parseFilter(filter);
|
||||
const filteredArticles = selectedCategories.length > 0
|
||||
? textMatches.filter((row) => matchesCategory(row, selectedCategories))
|
||||
: textMatches;
|
||||
// Apply category filter if set
|
||||
const selectedCategories = parseFilter(filter);
|
||||
const filteredArticles = selectedCategories.length > 0
|
||||
? textMatches.filter((row) => matchesCategory(row, selectedCategories))
|
||||
: textMatches;
|
||||
|
||||
const totalCount = filteredArticles.length;
|
||||
const safePageSize = pageSize != null && pageSize > 0 ? pageSize : null;
|
||||
const pageCount = safePageSize ? Math.max(1, Math.ceil(totalCount / safePageSize)) : 1;
|
||||
const currentPage = safePageSize ? Math.floor(offset / safePageSize) + 1 : 1;
|
||||
const recordset = safePageSize
|
||||
? filteredArticles.slice(offset, offset + safePageSize)
|
||||
: filteredArticles;
|
||||
const totalCount = filteredArticles.length;
|
||||
const safePageSize = pageSize != null && pageSize > 0 ? pageSize : null;
|
||||
const pageCount = safePageSize ? Math.max(1, Math.ceil(totalCount / safePageSize)) : 1;
|
||||
const currentPage = safePageSize ? Math.floor(offset / safePageSize) + 1 : 1;
|
||||
const recordset = safePageSize
|
||||
? filteredArticles.slice(offset, offset + safePageSize)
|
||||
: filteredArticles;
|
||||
|
||||
const result = {
|
||||
categoryFacets,
|
||||
totalCount,
|
||||
recordset,
|
||||
offset,
|
||||
pageSize: safePageSize,
|
||||
pageCount,
|
||||
currentPage,
|
||||
error: null,
|
||||
};
|
||||
const result = {
|
||||
categoryFacets,
|
||||
totalCount,
|
||||
recordset,
|
||||
offset,
|
||||
pageSize: safePageSize,
|
||||
pageCount,
|
||||
currentPage,
|
||||
error: null,
|
||||
};
|
||||
|
||||
articleSearchAutomat.setState({ result, loading: false });
|
||||
return result;
|
||||
} catch (error) {
|
||||
const result = {
|
||||
...emptyResult,
|
||||
pageSize,
|
||||
offset,
|
||||
error,
|
||||
};
|
||||
articleSearchAutomat.setState({ result, loading: false });
|
||||
throw error;
|
||||
articleSearchAutomat.setState({
|
||||
result,
|
||||
target,
|
||||
filter,
|
||||
offset,
|
||||
pageSize: safePageSize,
|
||||
loading: articleIndexAutomat.state.status === 'loading',
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs a client-side search over the article index with pagination.
|
||||
*
|
||||
* Does not block on network calls: evaluates immediately against whatever
|
||||
* articles are currently loaded, and ensures index derivation is running in the background.
|
||||
*
|
||||
* @param {string|object} [targetOrParams] Search term or options object.
|
||||
* @param {object} [options] Secondary options when first argument is target string.
|
||||
* @returns {Promise<ArticleSearchResult>} Search outcome.
|
||||
*/
|
||||
async search(targetOrParams, options = {}) {
|
||||
const params = typeof targetOrParams === 'object' && targetOrParams !== null
|
||||
? targetOrParams
|
||||
: targetOrParams !== undefined
|
||||
? { target: targetOrParams, ...options }
|
||||
: options;
|
||||
|
||||
// Kick off index loading in the background if not ready yet
|
||||
if (articleIndexAutomat.state.status === 'idle') {
|
||||
void articleIndexAutomat.actions.ensureLoaded().catch(() => {});
|
||||
}
|
||||
|
||||
return actions.runSearch(params);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the page size and re-derives the result set.
|
||||
*
|
||||
* @param {number} pageSize Number of articles per page.
|
||||
* @returns {Promise<ArticleSearchResult>}
|
||||
* @returns {ArticleSearchResult}
|
||||
*/
|
||||
setPageSize(pageSize) {
|
||||
const { target, filter, offset } = articleSearchAutomat.state;
|
||||
return actions.search({ target, filter, offset, pageSize });
|
||||
return actions.runSearch({ pageSize });
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets page number (1-based) and re-derives the result set.
|
||||
*
|
||||
* @param {number} page 1-based page number.
|
||||
* @returns {Promise<ArticleSearchResult>}
|
||||
* @returns {ArticleSearchResult}
|
||||
*/
|
||||
setPage(page) {
|
||||
const { target, filter, pageSize } = articleSearchAutomat.state;
|
||||
const safeSize = pageSize ?? 1;
|
||||
const safeSize = articleSearchAutomat.state.pageSize ?? 1;
|
||||
const offset = Math.max(0, (page - 1) * safeSize);
|
||||
return actions.search({ target, filter, offset, pageSize });
|
||||
return actions.runSearch({ offset });
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets 0-based offset and re-derives the result set.
|
||||
*
|
||||
* @param {number} offset 0-based record offset.
|
||||
* @returns {Promise<ArticleSearchResult>}
|
||||
* @returns {ArticleSearchResult}
|
||||
*/
|
||||
setOffset(offset) {
|
||||
const { target, filter, pageSize } = articleSearchAutomat.state;
|
||||
return actions.search({ target, filter, offset, pageSize });
|
||||
return actions.runSearch({ offset });
|
||||
},
|
||||
|
||||
/** Reset the search state. */
|
||||
@@ -250,10 +263,15 @@ const actions = {
|
||||
*/
|
||||
articleSearchAutomat = new Automat(initialState, actions, { name: 'articleSearch' });
|
||||
|
||||
// Continuously re-derive and update subscribers as category views load their article lists.
|
||||
articleIndexAutomat.subscribe(() => {
|
||||
const { target, filter, offset, pageSize, loading } = articleSearchAutomat.state;
|
||||
if (pageSize != null && !loading && articleIndexAutomat.state.status === 'ready') {
|
||||
void actions.search({ target, filter, offset, pageSize }).catch(() => {});
|
||||
actions.runSearch();
|
||||
});
|
||||
|
||||
// Update facet names whenever the category tree is loaded/updated.
|
||||
categoryAutomat.subscribe((catState) => {
|
||||
if (catState.treeStatus === 'succeeded') {
|
||||
actions.runSearch();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user