normalizeState.js 3.1 KB

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