normalizeState.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import {
  2. SEARCH_SCOPE,
  3. DEFAULT_SEARCH_LIMIT,
  4. SEARCH_LIMITS,
  5. } from "@/lib/frontend/search/urlState";
  6. function isNonEmptyString(value) {
  7. return typeof value === "string" && value.trim().length > 0;
  8. }
  9. function normalizeBranch(value) {
  10. return isNonEmptyString(value) ? value.trim() : null;
  11. }
  12. function normalizeLimit(value) {
  13. const n = Number(value);
  14. if (!Number.isInteger(n)) return DEFAULT_SEARCH_LIMIT;
  15. return SEARCH_LIMITS.includes(n) ? n : DEFAULT_SEARCH_LIMIT;
  16. }
  17. /**
  18. * Normalize URL-derived search state for the current user and route context.
  19. *
  20. * Why this exists:
  21. * - The URL is shareable and can contain stale/foreign params.
  22. * - The UI route already defines the "branch context" (/:branch/search).
  23. * - Branch users must never get cross-branch scope semantics in the UI.
  24. *
  25. * Policy:
  26. * - branch users: force SINGLE on the current route branch
  27. * - admin/dev: SINGLE always uses the current route branch
  28. * - MULTI/ALL: do not carry a single-branch value (branch=null)
  29. *
  30. * @param {{
  31. * q: string|null,
  32. * scope: "single"|"multi"|"all",
  33. * branch: string|null,
  34. * branches: string[],
  35. * limit: number,
  36. * from: string|null,
  37. * to: string|null
  38. * }|null|undefined} state
  39. * @param {{ routeBranch: string, user: { role: string, branchId: string|null }|null }} options
  40. * @returns {{
  41. * q: string|null,
  42. * scope: "single"|"multi"|"all",
  43. * branch: string|null,
  44. * branches: string[],
  45. * limit: number,
  46. * from: string|null,
  47. * to: string|null
  48. * }}
  49. */
  50. export function normalizeSearchUrlStateForUser(
  51. state,
  52. { routeBranch, user } = {}
  53. ) {
  54. const route = normalizeBranch(routeBranch);
  55. const base = {
  56. q: isNonEmptyString(state?.q) ? state.q.trim() : null,
  57. scope: state?.scope || SEARCH_SCOPE.SINGLE,
  58. branch: normalizeBranch(state?.branch),
  59. branches: Array.isArray(state?.branches) ? state.branches : [],
  60. limit: normalizeLimit(state?.limit),
  61. from: isNonEmptyString(state?.from) ? state.from.trim() : null,
  62. to: isNonEmptyString(state?.to) ? state.to.trim() : null,
  63. };
  64. // Defensive: if no routeBranch is available (should not happen in this app),
  65. // fall back to the parsed branch value.
  66. if (!route) {
  67. return {
  68. ...base,
  69. branch: base.scope === SEARCH_SCOPE.SINGLE ? base.branch : null,
  70. branches: base.scope === SEARCH_SCOPE.MULTI ? base.branches : [],
  71. };
  72. }
  73. const role = user?.role;
  74. // Branch users: force SINGLE on the current route branch.
  75. if (role === "branch") {
  76. return {
  77. ...base,
  78. scope: SEARCH_SCOPE.SINGLE,
  79. branch: route,
  80. branches: [],
  81. };
  82. }
  83. // Admin/dev: SINGLE always uses the current route branch (route context wins over URL param).
  84. if (role === "admin" || role === "dev") {
  85. if (base.scope === SEARCH_SCOPE.SINGLE) {
  86. return { ...base, branch: route, branches: [] };
  87. }
  88. if (base.scope === SEARCH_SCOPE.MULTI) {
  89. return { ...base, branch: null };
  90. }
  91. // ALL
  92. return { ...base, scope: SEARCH_SCOPE.ALL, branch: null, branches: [] };
  93. }
  94. // Unknown roles: fail-safe to route-branch SINGLE.
  95. return { ...base, scope: SEARCH_SCOPE.SINGLE, branch: route, branches: [] };
  96. }