@acorex/platform 20.9.12 → 20.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3001,6 +3001,8 @@ function axpReplaceEntityListRouteQueryParams(router, queryParams) {
3001
3001
  //#endregion
3002
3002
 
3003
3003
  const DEFAULT_TRAILING_SEGMENTS = ['list', 'view', 'edit', 'create'];
3004
+ /** Query params managed by entity list UI; ignored when matching generic (no-query) menu paths. */
3005
+ const ENTITY_LIST_UI_QUERY_KEYS = new Set(['view']);
3004
3006
  //#region ---- Entity Route Helpers ----
3005
3007
  /**
3006
3008
  * Parses `/{app}/m/{module}/e/{entity}/...` routes into a stable entity key.
@@ -3021,15 +3023,89 @@ function axpParseEntityRouteKey(segments) {
3021
3023
  function axpEntityRouteKeysEqual(left, right) {
3022
3024
  return left.app === right.app && left.module === right.module && left.entity === right.entity;
3023
3025
  }
3026
+ function axpParseQueryString(queryString) {
3027
+ return new URLSearchParams(queryString);
3028
+ }
3029
+ function axpMenuDeclaredNavigationParams(queryString) {
3030
+ const params = axpParseQueryString(queryString);
3031
+ const navigationParams = new URLSearchParams();
3032
+ params.forEach((value, key) => {
3033
+ if (!ENTITY_LIST_UI_QUERY_KEYS.has(key)) {
3034
+ navigationParams.append(key, value);
3035
+ }
3036
+ });
3037
+ return navigationParams;
3038
+ }
3039
+ function axpMenuHasDeclaredNavigationParams(itemQueryString) {
3040
+ return axpMenuDeclaredNavigationParams(itemQueryString).toString().length > 0;
3041
+ }
3042
+ /**
3043
+ * True when every navigation query param declared on the menu path equals the current URL.
3044
+ */
3045
+ function axpMenuDeclaredQueryParamsMatch(itemQueryString, currentQueryString) {
3046
+ const itemParams = axpMenuDeclaredNavigationParams(itemQueryString);
3047
+ const declaredPairs = [];
3048
+ itemParams.forEach((value, key) => {
3049
+ declaredPairs.push([key, value]);
3050
+ });
3051
+ if (declaredPairs.length === 0) {
3052
+ return axpGenericEntityListMenuQueryParamsMatch(currentQueryString);
3053
+ }
3054
+ const currentParams = axpParseQueryString(currentQueryString);
3055
+ return declaredPairs.every(([key, value]) => currentParams.get(key) === value);
3056
+ }
3057
+ /**
3058
+ * Generic entity list menu items (no declared query params) match only when the URL has no
3059
+ * navigation query params beyond UI-managed keys such as `view`.
3060
+ */
3061
+ function axpGenericEntityListMenuQueryParamsMatch(currentQueryString) {
3062
+ const currentParams = axpParseQueryString(currentQueryString);
3063
+ let hasNavigationParam = false;
3064
+ currentParams.forEach((_value, key) => {
3065
+ if (!ENTITY_LIST_UI_QUERY_KEYS.has(key)) {
3066
+ hasNavigationParam = true;
3067
+ }
3068
+ });
3069
+ return !hasNavigationParam;
3070
+ }
3071
+ /**
3072
+ * Counts declared menu query params that match the current URL (for best-match selection).
3073
+ */
3074
+ function axpMenuQueryParamSpecificity(itemPath, currentPath) {
3075
+ const itemQueryString = itemPath.split('#')[0].split('?')[1] ?? '';
3076
+ const currentQueryString = currentPath.split('#')[0].split('?')[1] ?? '';
3077
+ const itemParams = axpMenuDeclaredNavigationParams(itemQueryString);
3078
+ const currentParams = axpParseQueryString(currentQueryString);
3079
+ let specificity = 0;
3080
+ itemParams.forEach((value, key) => {
3081
+ if (currentParams.get(key) === value) {
3082
+ specificity++;
3083
+ }
3084
+ });
3085
+ return specificity;
3086
+ }
3024
3087
  /**
3025
3088
  * Matches entity list menu entries against list/detail/create/edit routes for the same entity.
3026
3089
  */
3027
- function axpTryMatchEntityMenuRoute(itemSegments, pathSegments, currentBase) {
3090
+ function axpTryMatchEntityMenuRoute(itemSegments, pathSegments, currentBase, itemQueryString, currentQueryString) {
3028
3091
  const menuEntity = axpParseEntityRouteKey(itemSegments);
3029
3092
  const currentEntity = axpParseEntityRouteKey(pathSegments);
3030
3093
  if (!menuEntity || !currentEntity || !axpEntityRouteKeysEqual(menuEntity, currentEntity)) {
3031
3094
  return null;
3032
3095
  }
3096
+ const itemHasDeclaredParams = axpMenuHasDeclaredNavigationParams(itemQueryString);
3097
+ if (itemHasDeclaredParams) {
3098
+ if (!axpIsEntityListPath(currentBase)) {
3099
+ return { isMatch: false, isPartial: false };
3100
+ }
3101
+ if (!axpMenuDeclaredQueryParamsMatch(itemQueryString, currentQueryString)) {
3102
+ return { isMatch: false, isPartial: false };
3103
+ }
3104
+ return { isMatch: true, isPartial: false };
3105
+ }
3106
+ if (axpIsEntityListPath(currentBase) && !axpGenericEntityListMenuQueryParamsMatch(currentQueryString)) {
3107
+ return { isMatch: false, isPartial: false };
3108
+ }
3033
3109
  const isPartial = pathSegments.length > menuEntity.entityEndIndex ||
3034
3110
  axpIsEntityDetailsPath(currentBase) ||
3035
3111
  !axpIsEntityListPath(currentBase);
@@ -3044,10 +3120,10 @@ function axpMenuPathsMatch(itemPath, currentPath, trailingSegmentsToIgnore = DEF
3044
3120
  const itemNoHash = itemPath.split('#')[0];
3045
3121
  const currentNoHash = currentPath.split('#')[0];
3046
3122
  const [itemBase, itemQueryString = ''] = itemNoHash.split('?');
3047
- const [currentBase] = currentNoHash.split('?');
3123
+ const [currentBase, currentQueryString = ''] = currentNoHash.split('?');
3048
3124
  const itemSegments = itemBase.split('/').filter((segment) => segment.length > 0);
3049
3125
  const pathSegments = currentBase.split('/').filter((segment) => segment.length > 0);
3050
- const entityMatch = axpTryMatchEntityMenuRoute(itemSegments, pathSegments, `/${currentBase}`);
3126
+ const entityMatch = axpTryMatchEntityMenuRoute(itemSegments, pathSegments, `/${currentBase}`, itemQueryString, currentQueryString);
3051
3127
  if (entityMatch) {
3052
3128
  return entityMatch;
3053
3129
  }
@@ -3072,8 +3148,8 @@ function axpMenuPathsMatch(itemPath, currentPath, trailingSegmentsToIgnore = DEF
3072
3148
  }
3073
3149
  const isPartialMatch = effectiveMenuLength < pathLength;
3074
3150
  const isExactMatch = effectiveMenuLength === pathLength;
3075
- const itemParams = new URLSearchParams(itemQueryString);
3076
- const currentParams = new URLSearchParams(currentNoHash.split('?')[1] ?? '');
3151
+ const itemParams = axpParseQueryString(itemQueryString);
3152
+ const currentParams = axpParseQueryString(currentQueryString);
3077
3153
  let queryMatches = true;
3078
3154
  const itemParamPairs = [];
3079
3155
  itemParams.forEach((value, key) => {
@@ -3098,6 +3174,7 @@ function axpMenuPathsMatch(itemPath, currentPath, trailingSegmentsToIgnore = DEF
3098
3174
  function axpFindMenuItemByRoute(items, currentPath, trailingSegmentsToIgnore = DEFAULT_TRAILING_SEGMENTS) {
3099
3175
  let bestMatch = null;
3100
3176
  let bestEffectiveLength = -1;
3177
+ let bestQuerySpecificity = -1;
3101
3178
  const visit = (menuItems, ancestors) => {
3102
3179
  for (const item of menuItems) {
3103
3180
  if (item.children?.length) {
@@ -3122,8 +3199,11 @@ function axpFindMenuItemByRoute(items, currentPath, trailingSegmentsToIgnore = D
3122
3199
  if (menuEntity) {
3123
3200
  effectiveMenuLength = menuEntity.entityEndIndex;
3124
3201
  }
3125
- if (effectiveMenuLength > bestEffectiveLength) {
3202
+ const querySpecificity = axpMenuQueryParamSpecificity(item.path, currentPath);
3203
+ if (effectiveMenuLength > bestEffectiveLength ||
3204
+ (effectiveMenuLength === bestEffectiveLength && querySpecificity > bestQuerySpecificity)) {
3126
3205
  bestEffectiveLength = effectiveMenuLength;
3206
+ bestQuerySpecificity = querySpecificity;
3127
3207
  bestMatch = { item, ancestors, isPartialMatch: matchResult.isPartial };
3128
3208
  }
3129
3209
  }