| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108 |
- import {
- SEARCH_SCOPE,
- DEFAULT_SEARCH_LIMIT,
- SEARCH_LIMITS,
- } from "@/lib/frontend/search/urlState";
- function isNonEmptyString(value) {
- return typeof value === "string" && value.trim().length > 0;
- }
- function normalizeBranch(value) {
- return isNonEmptyString(value) ? value.trim() : null;
- }
- function normalizeLimit(value) {
- const n = Number(value);
- if (!Number.isInteger(n)) return DEFAULT_SEARCH_LIMIT;
- return SEARCH_LIMITS.includes(n) ? n : DEFAULT_SEARCH_LIMIT;
- }
- /**
- * Normalize URL-derived search state for the current user and route context.
- *
- * Why this exists:
- * - The URL is shareable and can contain stale/foreign params.
- * - The UI route already defines the "branch context" (/:branch/search).
- * - Branch users must never get cross-branch scope semantics in the UI.
- *
- * Policy:
- * - branch users: force SINGLE on the current route branch
- * - admin/dev: SINGLE always uses the current route branch
- * - MULTI/ALL: do not carry a single-branch value (branch=null)
- *
- * @param {{
- * q: string|null,
- * scope: "single"|"multi"|"all",
- * branch: string|null,
- * branches: string[],
- * limit: number,
- * from: string|null,
- * to: string|null
- * }|null|undefined} state
- * @param {{ routeBranch: string, user: { role: string, branchId: string|null }|null }} options
- * @returns {{
- * q: string|null,
- * scope: "single"|"multi"|"all",
- * branch: string|null,
- * branches: string[],
- * limit: number,
- * from: string|null,
- * to: string|null
- * }}
- */
- export function normalizeSearchUrlStateForUser(
- state,
- { routeBranch, user } = {}
- ) {
- const route = normalizeBranch(routeBranch);
- const base = {
- q: isNonEmptyString(state?.q) ? state.q.trim() : null,
- scope: state?.scope || SEARCH_SCOPE.SINGLE,
- branch: normalizeBranch(state?.branch),
- branches: Array.isArray(state?.branches) ? state.branches : [],
- limit: normalizeLimit(state?.limit),
- from: isNonEmptyString(state?.from) ? state.from.trim() : null,
- to: isNonEmptyString(state?.to) ? state.to.trim() : null,
- };
- // Defensive: if no routeBranch is available (should not happen in this app),
- // fall back to the parsed branch value.
- if (!route) {
- return {
- ...base,
- branch: base.scope === SEARCH_SCOPE.SINGLE ? base.branch : null,
- branches: base.scope === SEARCH_SCOPE.MULTI ? base.branches : [],
- };
- }
- const role = user?.role;
- // Branch users: force SINGLE on the current route branch.
- if (role === "branch") {
- return {
- ...base,
- scope: SEARCH_SCOPE.SINGLE,
- branch: route,
- branches: [],
- };
- }
- // Admin/dev: SINGLE always uses the current route branch (route context wins over URL param).
- if (role === "admin" || role === "dev") {
- if (base.scope === SEARCH_SCOPE.SINGLE) {
- return { ...base, branch: route, branches: [] };
- }
- if (base.scope === SEARCH_SCOPE.MULTI) {
- return { ...base, branch: null };
- }
- // ALL
- return { ...base, scope: SEARCH_SCOPE.ALL, branch: null, branches: [] };
- }
- // Unknown roles: fail-safe to route-branch SINGLE.
- return { ...base, scope: SEARCH_SCOPE.SINGLE, branch: route, branches: [] };
- }
|