@databiosphere/findable-ui 52.0.0 → 52.2.0
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.
- package/lib/common/analytics/entities.d.ts +5 -1
- package/lib/common/analytics/entities.js +1 -0
- package/lib/common/filters/adapters/tanstack/typeGuards.d.ts +7 -0
- package/lib/common/filters/adapters/tanstack/typeGuards.js +11 -0
- package/lib/common/filters/hooks/UseValidateFilterKeys/hook.d.ts +32 -0
- package/lib/common/filters/hooks/UseValidateFilterKeys/hook.js +16 -0
- package/lib/common/filters/hooks/UseValidateFilterKeys/utils.d.ts +18 -0
- package/lib/common/filters/hooks/UseValidateFilterKeys/utils.js +47 -0
- package/lib/common/filters/typeGuards.d.ts +1 -1
- package/lib/components/DataDictionary/dataDictionary.js +8 -0
- package/lib/components/DataDictionary/utils.d.ts +13 -0
- package/lib/components/DataDictionary/utils.js +16 -0
- package/lib/google/provider.js +2 -0
- package/lib/hooks/authentication/useLoginTracking.d.ts +9 -0
- package/lib/hooks/authentication/useLoginTracking.js +25 -0
- package/lib/nextauth/provider.js +2 -0
- package/lib/providers/exploreState/initializer/utils.d.ts +8 -1
- package/lib/providers/exploreState/initializer/utils.js +1 -1
- package/lib/views/DataDictionaryView/dataDictionaryView.js +6 -0
- package/lib/views/ExploreView/hooks/UseValidateFilterParam/hook.d.ts +4 -3
- package/lib/views/ExploreView/hooks/UseValidateFilterParam/hook.js +11 -46
- package/lib/views/ExploreView/hooks/UseValidateFilterParam/utils.d.ts +16 -0
- package/lib/views/ExploreView/hooks/UseValidateFilterParam/utils.js +29 -0
- package/package.json +1 -1
|
@@ -13,7 +13,8 @@ export declare enum EVENT_NAME {
|
|
|
13
13
|
FILE_DOWNLOADED = "file_downloaded",
|
|
14
14
|
FILTER_SELECTED = "filter_selected",
|
|
15
15
|
INDEX_ANALYZE_IN_TERRA_REQUESTED = "index_analyze_in_terra_requested",
|
|
16
|
-
INDEX_FILE_MANIFEST_REQUESTED = "index_file_manifest_requested"
|
|
16
|
+
INDEX_FILE_MANIFEST_REQUESTED = "index_file_manifest_requested",
|
|
17
|
+
LOGIN = "login"
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* Set of analytics event parameters.
|
|
@@ -78,4 +79,7 @@ export type EventParams = {
|
|
|
78
79
|
[EVENT_NAME.INDEX_FILE_MANIFEST_REQUESTED]: {
|
|
79
80
|
[EVENT_PARAM.ENTITY_NAME]: string;
|
|
80
81
|
};
|
|
82
|
+
[EVENT_NAME.LOGIN]: {
|
|
83
|
+
[EVENT_PARAM.TOOL_NAME]: string;
|
|
84
|
+
};
|
|
81
85
|
};
|
|
@@ -14,6 +14,7 @@ export var EVENT_NAME;
|
|
|
14
14
|
EVENT_NAME["FILTER_SELECTED"] = "filter_selected";
|
|
15
15
|
EVENT_NAME["INDEX_ANALYZE_IN_TERRA_REQUESTED"] = "index_analyze_in_terra_requested";
|
|
16
16
|
EVENT_NAME["INDEX_FILE_MANIFEST_REQUESTED"] = "index_file_manifest_requested";
|
|
17
|
+
EVENT_NAME["LOGIN"] = "login";
|
|
17
18
|
})(EVENT_NAME || (EVENT_NAME = {}));
|
|
18
19
|
/**
|
|
19
20
|
* Set of analytics event parameters.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ColumnFilter } from "@tanstack/react-table";
|
|
2
|
+
/**
|
|
3
|
+
* Returns true if the value is a valid TanStack ColumnFilter shape.
|
|
4
|
+
* @param value - Value to check.
|
|
5
|
+
* @returns true if the value has the expected shape.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isColumnFilter(value: unknown): value is ColumnFilter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the value is a valid TanStack ColumnFilter shape.
|
|
3
|
+
* @param value - Value to check.
|
|
4
|
+
* @returns true if the value has the expected shape.
|
|
5
|
+
*/
|
|
6
|
+
export function isColumnFilter(value) {
|
|
7
|
+
if (typeof value !== "object" || value === null)
|
|
8
|
+
return false;
|
|
9
|
+
const filter = value;
|
|
10
|
+
return typeof filter.id === "string" && "value" in filter;
|
|
11
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates the filter query parameter shape only (no key validation).
|
|
3
|
+
* Throws a DataExplorerError during render if the filter param is present
|
|
4
|
+
* but contains malformed JSON or an invalid entry shape.
|
|
5
|
+
* @param queryParamKey - URL query parameter key to read (e.g. "filter").
|
|
6
|
+
* @param validKeys - Must be undefined to use this overload.
|
|
7
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
8
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useValidateFilterKeys(queryParamKey: string, validKeys: undefined, entryValidator: (value: unknown) => boolean): void;
|
|
11
|
+
/**
|
|
12
|
+
* Validates the filter query parameter shape and keys against a valid set.
|
|
13
|
+
* Throws a DataExplorerError during render if the filter param is present
|
|
14
|
+
* but contains malformed JSON, an invalid entry shape, or a key that
|
|
15
|
+
* is not in the valid set.
|
|
16
|
+
* @param queryParamKey - URL query parameter key to read (e.g. "filter").
|
|
17
|
+
* @param validKeys - Set of valid keys.
|
|
18
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
19
|
+
* @param keyExtractor - Extracts the key string from a validated entry.
|
|
20
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useValidateFilterKeys(queryParamKey: string, validKeys: Set<string>, entryValidator: (value: unknown) => boolean, keyExtractor: (entry: unknown) => string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Validates the filter query parameter shape and, when validKeys is defined,
|
|
25
|
+
* keys against the valid set. Use when validKeys is dynamically determined.
|
|
26
|
+
* @param queryParamKey - URL query parameter key to read (e.g. "filter").
|
|
27
|
+
* @param validKeys - Set of valid keys, or undefined to skip key validation.
|
|
28
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
29
|
+
* @param keyExtractor - Extracts the key string from a validated entry.
|
|
30
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useValidateFilterKeys(queryParamKey: string, validKeys: Set<string> | undefined, entryValidator: (value: unknown) => boolean, keyExtractor: (entry: unknown) => string): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useRouter } from "next/router";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { validateFilterParam } from "./utils";
|
|
4
|
+
export function useValidateFilterKeys(queryParamKey, validKeys, entryValidator, keyExtractor) {
|
|
5
|
+
const { query } = useRouter();
|
|
6
|
+
const filterParam = query[queryParamKey];
|
|
7
|
+
const validationError = useMemo(() => {
|
|
8
|
+
if (validKeys && keyExtractor) {
|
|
9
|
+
return validateFilterParam(filterParam, validKeys, entryValidator, keyExtractor);
|
|
10
|
+
}
|
|
11
|
+
return validateFilterParam(filterParam, undefined, entryValidator);
|
|
12
|
+
}, [entryValidator, filterParam, keyExtractor, validKeys]);
|
|
13
|
+
if (validationError) {
|
|
14
|
+
throw validationError;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DataExplorerError } from "../../../../types/error";
|
|
2
|
+
/**
|
|
3
|
+
* Validates a filter query parameter value (shape only, no key validation).
|
|
4
|
+
* @param filterParam - Raw URL query param value.
|
|
5
|
+
* @param validKeys - Must be undefined to use this overload.
|
|
6
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
7
|
+
* @returns DataExplorerError if invalid, undefined if valid.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateFilterParam(filterParam: string | string[] | undefined, validKeys: undefined, entryValidator: (value: unknown) => boolean): DataExplorerError | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Validates a filter query parameter value against a set of valid keys.
|
|
12
|
+
* @param filterParam - Raw URL query param value.
|
|
13
|
+
* @param validKeys - Set of valid keys.
|
|
14
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
15
|
+
* @param keyExtractor - Extracts the key string from a validated entry.
|
|
16
|
+
* @returns DataExplorerError if invalid, undefined if valid.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateFilterParam(filterParam: string | string[] | undefined, validKeys: Set<string>, entryValidator: (value: unknown) => boolean, keyExtractor: (entry: unknown) => string): DataExplorerError | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DataExplorerError } from "../../../../types/error";
|
|
2
|
+
const INVALID_FILTER_PARAM = "Invalid filter parameter in URL";
|
|
3
|
+
const INVALID_FILTER_SHAPE = "Invalid filter entry shape in URL";
|
|
4
|
+
const UNKNOWN_FILTER_KEY = "Unknown filter key in URL";
|
|
5
|
+
export function validateFilterParam(filterParam, validKeys, entryValidator, keyExtractor) {
|
|
6
|
+
if (filterParam === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (typeof filterParam !== "string") {
|
|
9
|
+
return new DataExplorerError({ message: INVALID_FILTER_PARAM });
|
|
10
|
+
}
|
|
11
|
+
// Try JSON.parse directly first (Next.js router.query already decodes),
|
|
12
|
+
// then fall back to decodeURIComponent + JSON.parse for encoded values.
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = JSON.parse(filterParam);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(decodeURIComponent(filterParam));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return new DataExplorerError({ message: INVALID_FILTER_PARAM });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(parsed)) {
|
|
26
|
+
return new DataExplorerError({ message: INVALID_FILTER_PARAM });
|
|
27
|
+
}
|
|
28
|
+
// An empty array (e.g. "[]") is valid — it means no filters selected.
|
|
29
|
+
if (parsed.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
// Validate shape: every entry must match the expected shape.
|
|
32
|
+
if (!parsed.every(entryValidator)) {
|
|
33
|
+
return new DataExplorerError({ message: INVALID_FILTER_SHAPE });
|
|
34
|
+
}
|
|
35
|
+
// Validate keys: every extracted key must be in the valid set.
|
|
36
|
+
if (validKeys && keyExtractor) {
|
|
37
|
+
const invalidKey = parsed
|
|
38
|
+
.map(keyExtractor)
|
|
39
|
+
.find((key) => !validKeys.has(key));
|
|
40
|
+
if (invalidKey !== undefined) {
|
|
41
|
+
return new DataExplorerError({
|
|
42
|
+
message: `${UNKNOWN_FILTER_KEY}: ${invalidKey}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fade } from "@mui/material";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { isColumnFilter } from "../../common/filters/adapters/tanstack/typeGuards";
|
|
5
|
+
import { useValidateFilterKeys } from "../../common/filters/hooks/UseValidateFilterKeys/hook";
|
|
3
6
|
import { PROPERTY } from "../../hooks/useHtmlStyle/constants";
|
|
4
7
|
import { useHtmlStyle } from "../../hooks/useHtmlStyle/hook";
|
|
5
8
|
import { useLayoutSpacing } from "../../hooks/UseLayoutSpacing/hook";
|
|
9
|
+
import { DATA_DICTIONARY_URL_PARAMS } from "../../providers/dataDictionaryState/dictionaries/constants";
|
|
6
10
|
import { Description } from "./components/Description/description";
|
|
7
11
|
import { Entities } from "./components/Entities/entities";
|
|
8
12
|
import { ColumnFilterTags } from "./components/Filters/components/ColumnFilterTags/columnFilterTags";
|
|
@@ -18,6 +22,7 @@ import { Title as DefaultTitle } from "./components/Title/title";
|
|
|
18
22
|
import { View } from "./dataDictionary.styles";
|
|
19
23
|
import { useDataDictionaryConfig } from "./hooks/UseDataDictionaryConfig/hook";
|
|
20
24
|
import { useMeasureFilters } from "./hooks/UseMeasureFilters/hook";
|
|
25
|
+
import { extractColumnId, getValidColumnIds } from "./utils";
|
|
21
26
|
export const DataDictionary = ({ className, dictionary, EntitiesLayout = DefaultEntitiesLayout, FiltersLayout = DefaultFiltersLayout, Outline = DefaultOutline, OutlineLayout = DefaultOutlineLayout, Title = DefaultTitle, TitleLayout = DefaultTitleLayout, }) => {
|
|
22
27
|
// Get dictionary configuration.
|
|
23
28
|
const { classes, description, tableOptions, title } = useDataDictionaryConfig(dictionary);
|
|
@@ -30,6 +35,9 @@ export const DataDictionary = ({ className, dictionary, EntitiesLayout = Default
|
|
|
30
35
|
const entitiesSpacing = { ...spacing, top: dimensions.height };
|
|
31
36
|
// Table instance.
|
|
32
37
|
const table = useTable(dictionary, classes, tableOptions);
|
|
38
|
+
// Validate filter URL param keys against the table's column IDs.
|
|
39
|
+
const validColumnIds = useMemo(() => getValidColumnIds(table), [table]);
|
|
40
|
+
useValidateFilterKeys(DATA_DICTIONARY_URL_PARAMS.COLUMN_FILTERS, validColumnIds, isColumnFilter, extractColumnId);
|
|
33
41
|
// Dictionary outline.
|
|
34
42
|
const outline = buildClassesOutline(table);
|
|
35
43
|
// Update scroll-padding-top on the HTML element.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RowData, Table } from "@tanstack/react-table";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the column ID from a validated ColumnFilter entry.
|
|
4
|
+
* @param entry - Validated entry.
|
|
5
|
+
* @returns column ID.
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractColumnId(entry: unknown): string;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the set of valid column IDs from a TanStack table instance.
|
|
10
|
+
* @param table - TanStack table instance.
|
|
11
|
+
* @returns set of valid column IDs.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getValidColumnIds<T extends RowData>(table: Table<T>): Set<string>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the column ID from a validated ColumnFilter entry.
|
|
3
|
+
* @param entry - Validated entry.
|
|
4
|
+
* @returns column ID.
|
|
5
|
+
*/
|
|
6
|
+
export function extractColumnId(entry) {
|
|
7
|
+
return entry.id;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns the set of valid column IDs from a TanStack table instance.
|
|
11
|
+
* @param table - TanStack table instance.
|
|
12
|
+
* @returns set of valid column IDs.
|
|
13
|
+
*/
|
|
14
|
+
export function getValidColumnIds(table) {
|
|
15
|
+
return new Set(table.getAllColumns().map((column) => column.id));
|
|
16
|
+
}
|
package/lib/google/provider.js
CHANGED
|
@@ -8,6 +8,7 @@ import { useCredentialsReducer } from "../auth/hooks/useCredentialsReducer";
|
|
|
8
8
|
import { useSessionIdleTimer } from "../auth/hooks/useSessionIdleTimer";
|
|
9
9
|
import { useSessionActive } from "../hooks/authentication/session/useSessionActive";
|
|
10
10
|
import { useSessionCallbackUrl } from "../hooks/authentication/session/useSessionCallbackUrl";
|
|
11
|
+
import { useLoginTracking } from "../hooks/authentication/useLoginTracking";
|
|
11
12
|
import { AUTH_STATE, AUTHENTICATION_STATE } from "./constants";
|
|
12
13
|
import { useGoogleSignInService } from "./hooks/useGoogleSignInService";
|
|
13
14
|
import { useTokenReducer } from "./hooks/useTokenReducer";
|
|
@@ -35,6 +36,7 @@ export function GoogleSignInAuthenticationProvider({ children, SessionController
|
|
|
35
36
|
const { authDispatch, authState } = authReducer;
|
|
36
37
|
const { isAuthenticated } = authState;
|
|
37
38
|
const { authenticationState } = authenticationReducer;
|
|
39
|
+
useLoginTracking(isAuthenticated, authState.status);
|
|
38
40
|
useSessionActive(authState, authenticationState);
|
|
39
41
|
useSessionIdleTimer({
|
|
40
42
|
disabled: !isAuthenticated,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AUTH_STATUS } from "../../auth/types/auth";
|
|
2
|
+
/**
|
|
3
|
+
* Tracks a GA4 login event when the user transitions from unauthenticated to authenticated.
|
|
4
|
+
* Does not fire during initial session hydration or on mount if already authenticated.
|
|
5
|
+
* @param isAuthenticated - Current authentication state.
|
|
6
|
+
* @param status - Current auth status; tracking only begins after status has settled.
|
|
7
|
+
* @returns void.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useLoginTracking(isAuthenticated: boolean, status: AUTH_STATUS): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { AUTH_STATUS } from "../../auth/types/auth";
|
|
3
|
+
import { track } from "../../common/analytics/analytics";
|
|
4
|
+
import { EVENT_NAME, EVENT_PARAM } from "../../common/analytics/entities";
|
|
5
|
+
import { GOOGLE_SIGN_IN_PROVIDER_ID } from "../../google/constants";
|
|
6
|
+
/**
|
|
7
|
+
* Tracks a GA4 login event when the user transitions from unauthenticated to authenticated.
|
|
8
|
+
* Does not fire during initial session hydration or on mount if already authenticated.
|
|
9
|
+
* @param isAuthenticated - Current authentication state.
|
|
10
|
+
* @param status - Current auth status; tracking only begins after status has settled.
|
|
11
|
+
* @returns void.
|
|
12
|
+
*/
|
|
13
|
+
export function useLoginTracking(isAuthenticated, status) {
|
|
14
|
+
const wasAuthenticated = useRef(undefined);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (status !== AUTH_STATUS.SETTLED)
|
|
17
|
+
return;
|
|
18
|
+
if (wasAuthenticated.current === false && isAuthenticated) {
|
|
19
|
+
track(EVENT_NAME.LOGIN, {
|
|
20
|
+
[EVENT_PARAM.TOOL_NAME]: GOOGLE_SIGN_IN_PROVIDER_ID,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
wasAuthenticated.current = isAuthenticated;
|
|
24
|
+
}, [isAuthenticated, status]);
|
|
25
|
+
}
|
package/lib/nextauth/provider.js
CHANGED
|
@@ -6,6 +6,7 @@ import { useAuthenticationReducer } from "../auth/hooks/useAuthenticationReducer
|
|
|
6
6
|
import { useAuthReducer } from "../auth/hooks/useAuthReducer";
|
|
7
7
|
import { useSessionIdleTimer } from "../auth/hooks/useSessionIdleTimer";
|
|
8
8
|
import { useSessionCallbackUrl } from "../hooks/authentication/session/useSessionCallbackUrl";
|
|
9
|
+
import { useLoginTracking } from "../hooks/authentication/useLoginTracking";
|
|
9
10
|
import { useNextAuthService } from "./hooks/useNextAuthService";
|
|
10
11
|
import { SessionController } from "./session-controller";
|
|
11
12
|
/**
|
|
@@ -23,6 +24,7 @@ export function NextAuthAuthenticationProvider({ children, refetchInterval = 0,
|
|
|
23
24
|
const service = useNextAuthService();
|
|
24
25
|
const { authDispatch, authState } = authReducer;
|
|
25
26
|
const { isAuthenticated } = authState;
|
|
27
|
+
useLoginTracking(isAuthenticated, authState.status);
|
|
26
28
|
const { callbackUrl } = useSessionCallbackUrl();
|
|
27
29
|
useSessionIdleTimer({
|
|
28
30
|
crossTab: true,
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { SiteConfig } from "../../../config/entities";
|
|
1
|
+
import { CategoryGroupConfig, EntityConfig, SiteConfig } from "../../../config/entities";
|
|
2
2
|
import { ExploreState } from "../../exploreState";
|
|
3
|
+
/**
|
|
4
|
+
* Returns entity related configured category group config where entity config takes precedence over site config.
|
|
5
|
+
* @param siteConfig - Site config.
|
|
6
|
+
* @param entityConfig - Entity config.
|
|
7
|
+
* @returns entity related category group config.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getEntityCategoryGroupConfig(siteConfig: SiteConfig, entityConfig: EntityConfig): CategoryGroupConfig | undefined;
|
|
3
10
|
/**
|
|
4
11
|
* Returns the explore state reducer initial arguments.
|
|
5
12
|
* @param config - Site config.
|
|
@@ -80,7 +80,7 @@ function columnFiltersToSelectedFilters(columnFilters) {
|
|
|
80
80
|
* @param entityConfig - Entity config.
|
|
81
81
|
* @returns entity related category group config.
|
|
82
82
|
*/
|
|
83
|
-
function getEntityCategoryGroupConfig(siteConfig, entityConfig) {
|
|
83
|
+
export function getEntityCategoryGroupConfig(siteConfig, entityConfig) {
|
|
84
84
|
const siteCategoryGroupConfig = siteConfig.categoryGroupConfig;
|
|
85
85
|
const entityCategoryGroupConfig = entityConfig.categoryGroupConfig;
|
|
86
86
|
return entityCategoryGroupConfig ?? siteCategoryGroupConfig;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { isColumnFilter } from "../../common/filters/adapters/tanstack/typeGuards";
|
|
3
|
+
import { useValidateFilterKeys } from "../../common/filters/hooks/UseValidateFilterKeys/hook";
|
|
2
4
|
import { DataDictionary } from "../../components/DataDictionary/dataDictionary";
|
|
3
5
|
import { useStateSyncManager } from "../../hooks/stateSyncManager/hook";
|
|
4
6
|
import { DataDictionaryContext } from "../../providers/dataDictionary/context";
|
|
5
7
|
import { clearMeta } from "../../providers/dataDictionaryState/actions/clearMeta/dispatch";
|
|
6
8
|
import { stateToUrl } from "../../providers/dataDictionaryState/actions/stateToUrl/dispatch";
|
|
7
9
|
import { urlToState } from "../../providers/dataDictionaryState/actions/urlToState/dispatch";
|
|
10
|
+
import { DATA_DICTIONARY_URL_PARAMS } from "../../providers/dataDictionaryState/dictionaries/constants";
|
|
8
11
|
import { useDataDictionaryState } from "../../providers/dataDictionaryState/hooks/UseDataDictionaryState/hook";
|
|
9
12
|
import { buildStateSyncManagerContext } from "./utils";
|
|
10
13
|
export const DataDictionaryView = ({ className, dictionary, }) => {
|
|
11
14
|
const { dataDictionaryDispatch, dataDictionaryState } = useDataDictionaryState();
|
|
15
|
+
// Shape-only validation (no key check) — must run before useStateSyncManager
|
|
16
|
+
// to prevent malformed entries from being dispatched into state.
|
|
17
|
+
useValidateFilterKeys(DATA_DICTIONARY_URL_PARAMS.COLUMN_FILTERS, undefined, isColumnFilter);
|
|
12
18
|
useStateSyncManager({
|
|
13
19
|
actions: { clearMeta, stateToUrl, urlToState },
|
|
14
20
|
dispatch: dataDictionaryDispatch,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Validates the filter query parameter from the URL.
|
|
2
|
+
* Validates the filter query parameter from the URL for the ExploreView.
|
|
3
3
|
* Throws a DataExplorerError during render if the filter param is present
|
|
4
|
-
* but contains malformed JSON
|
|
4
|
+
* but contains malformed JSON, an invalid filter shape, or a categoryKey
|
|
5
|
+
* that does not exist in the configured categories, allowing the
|
|
5
6
|
* ErrorBoundary to catch and display the error page.
|
|
6
|
-
* @returns
|
|
7
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
7
8
|
*/
|
|
8
9
|
export declare function useValidateFilterParam(): void;
|
|
@@ -1,54 +1,19 @@
|
|
|
1
|
-
import { useRouter } from "next/router";
|
|
2
1
|
import { useMemo } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { useValidateFilterKeys } from "../../../../common/filters/hooks/UseValidateFilterKeys/hook";
|
|
3
|
+
import { isSelectedFilter } from "../../../../common/filters/typeGuards";
|
|
4
|
+
import { useConfig } from "../../../../hooks/useConfig";
|
|
4
5
|
import { EXPLORE_URL_PARAMS } from "../../../../providers/exploreState/constants";
|
|
5
|
-
import {
|
|
6
|
-
const INVALID_FILTER_PARAM_ERROR = "Invalid filter parameter in URL";
|
|
6
|
+
import { extractCategoryKey, getValidCategoryKeys } from "./utils";
|
|
7
7
|
/**
|
|
8
|
-
* Validates the filter query parameter from the URL.
|
|
8
|
+
* Validates the filter query parameter from the URL for the ExploreView.
|
|
9
9
|
* Throws a DataExplorerError during render if the filter param is present
|
|
10
|
-
* but contains malformed JSON
|
|
10
|
+
* but contains malformed JSON, an invalid filter shape, or a categoryKey
|
|
11
|
+
* that does not exist in the configured categories, allowing the
|
|
11
12
|
* ErrorBoundary to catch and display the error page.
|
|
12
|
-
* @returns
|
|
13
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
13
14
|
*/
|
|
14
15
|
export function useValidateFilterParam() {
|
|
15
|
-
const {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (filterParam === undefined)
|
|
19
|
-
return undefined;
|
|
20
|
-
if (typeof filterParam !== "string") {
|
|
21
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
22
|
-
}
|
|
23
|
-
let decoded;
|
|
24
|
-
try {
|
|
25
|
-
decoded = decodeURIComponent(filterParam);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
29
|
-
}
|
|
30
|
-
// Parse and validate the filter param.
|
|
31
|
-
// An empty array (e.g. "[]") is valid — it means no filters selected.
|
|
32
|
-
// A non-empty array with no valid entries is invalid.
|
|
33
|
-
let parsed;
|
|
34
|
-
try {
|
|
35
|
-
parsed = JSON.parse(decoded);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
39
|
-
}
|
|
40
|
-
if (!Array.isArray(parsed)) {
|
|
41
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
42
|
-
}
|
|
43
|
-
if (parsed.length === 0)
|
|
44
|
-
return undefined;
|
|
45
|
-
const filters = parseFilterParam(decoded);
|
|
46
|
-
if (filters.length === 0) {
|
|
47
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
}, [filterParam]);
|
|
51
|
-
if (validationError) {
|
|
52
|
-
throw validationError;
|
|
53
|
-
}
|
|
16
|
+
const { config, entityConfig } = useConfig();
|
|
17
|
+
const validKeys = useMemo(() => getValidCategoryKeys(config, entityConfig), [config, entityConfig]);
|
|
18
|
+
useValidateFilterKeys(EXPLORE_URL_PARAMS.FILTER, validKeys, isSelectedFilter, extractCategoryKey);
|
|
54
19
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EntityConfig, SiteConfig } from "../../../../config/entities";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the categoryKey from a validated SelectedFilter entry.
|
|
4
|
+
* @param entry - Validated entry.
|
|
5
|
+
* @returns category key.
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractCategoryKey(entry: unknown): string;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the set of valid category keys for the given entity, derived from
|
|
10
|
+
* the entity's category group config. Returns undefined if no category group
|
|
11
|
+
* config is available (skipping key validation).
|
|
12
|
+
* @param config - Site config.
|
|
13
|
+
* @param entityConfig - Entity config.
|
|
14
|
+
* @returns set of valid category keys, or undefined.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getValidCategoryKeys(config: SiteConfig, entityConfig: EntityConfig): Set<string> | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getEntityCategoryGroupConfig } from "../../../../providers/exploreState/initializer/utils";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the categoryKey from a validated SelectedFilter entry.
|
|
4
|
+
* @param entry - Validated entry.
|
|
5
|
+
* @returns category key.
|
|
6
|
+
*/
|
|
7
|
+
export function extractCategoryKey(entry) {
|
|
8
|
+
return entry.categoryKey;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns the set of valid category keys for the given entity, derived from
|
|
12
|
+
* the entity's category group config. Returns undefined if no category group
|
|
13
|
+
* config is available (skipping key validation).
|
|
14
|
+
* @param config - Site config.
|
|
15
|
+
* @param entityConfig - Entity config.
|
|
16
|
+
* @returns set of valid category keys, or undefined.
|
|
17
|
+
*/
|
|
18
|
+
export function getValidCategoryKeys(config, entityConfig) {
|
|
19
|
+
const categoryGroupConfig = getEntityCategoryGroupConfig(config, entityConfig);
|
|
20
|
+
if (!categoryGroupConfig)
|
|
21
|
+
return undefined;
|
|
22
|
+
const keys = new Set();
|
|
23
|
+
for (const group of categoryGroupConfig.categoryGroups) {
|
|
24
|
+
for (const categoryConfig of group.categoryConfigs) {
|
|
25
|
+
keys.add(categoryConfig.key);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return keys;
|
|
29
|
+
}
|