Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 100 additions & 12 deletions packages/visual-editor/src/components/Locator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ import {
createSearchHeadlessConfig,
} from "../utils/searchHeadlessConfig.ts";
import { BackgroundStyle } from "../utils/themeConfigOptions.ts";
import { StreamDocument } from "../utils/types/StreamDocument.ts";
import {
StreamDocument,
LocatorConfig,
EntityTypeScope,
LocatorSourcePageSetInfo,
} from "../utils/types/StreamDocument.ts";
import { getValueFromQueryString } from "../utils/urlQueryString.tsx";
import { Body } from "./atoms/body.tsx";
import { Heading } from "./atoms/heading.tsx";
Expand Down Expand Up @@ -102,22 +107,101 @@ const translateDistanceUnit = (
return t("kilometer", { count, defaultValue: "kilometer" });
};

const getEntityType = (entityTypeEnvVar?: string) => {
/**
* Retrieves the first applicable entity type for entities indexed by the locator. Reads from:
* 1. Puck metadata's entityTypeEnvVar
* 2. The typeConfig.locatorConfig.entityType field of _pageset in the stream document
* 3. The first element of __.locatorSourcePageSets in the stream document
*
* Used by the FilterSearch component, since including multiple entity types will result in
* duplicate results for builtin.location.
*/
const getAnyEntityType = (entityTypeEnvVar?: string) => {
const entityDocument: StreamDocument = useDocument();
if (!entityDocument._pageset && entityTypeEnvVar) {
return entityDocument._env?.[entityTypeEnvVar] || DEFAULT_ENTITY_TYPE;
}
Comment on lines 121 to 123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

entityTypeEnvVar precedence is currently inverted.

The docblock says env var is priority #1, but Line 121 only consults it when _pageset is missing. If both exist, env-var override is ignored.

🐛 Proposed fix
 const getAnyEntityType = (entityTypeEnvVar?: string) => {
   const entityDocument: StreamDocument = useDocument();
-  if (!entityDocument._pageset && entityTypeEnvVar) {
-    return entityDocument._env?.[entityTypeEnvVar] || DEFAULT_ENTITY_TYPE;
+  if (entityTypeEnvVar) {
+    const envEntityType = entityDocument._env?.[entityTypeEnvVar];
+    if (envEntityType) {
+      return envEntityType;
+    }
   }
 
   const pageSet = parseJsonObject(entityDocument?._pageset);

Also applies to: 125-135

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/components/Locator.tsx` around lines 121 - 123,
The precedence of entityTypeEnvVar is inverted: change the logic in the Locator
component (where entityDocument, entityTypeEnvVar, DEFAULT_ENTITY_TYPE, and
_pageset are used) to check the env override first—if entityTypeEnvVar is set
and entityDocument._env?.[entityTypeEnvVar] exists return that; otherwise fall
back to entityDocument._pageset (if present) and finally DEFAULT_ENTITY_TYPE;
apply the same env-first ordering to the similar block covering lines 125-135.


const pageSet = parseJsonObject(entityDocument?._pageset);
const locatorConfig: LocatorConfig = pageSet?.typeConfig?.locatorConfig;
const entityType = locatorConfig?.entityType;
if (entityType) {
return entityType;
}

// there should always be at least one source page set
const locatorSourcePageSets = getLocatorSourcePageSets();
return locatorSourcePageSets?.[0]?.entityType || DEFAULT_ENTITY_TYPE;
};

/**
* Returns a combined list of (entityTypeId, savedFilterId) pairs from the locator source entity
* page sets in __.locatorSourcePageSets and the independent entity type scopes in
* typeConfig.locatorConfig.entityTypeScopes of the _pageset field.
*/
const getAllEntityTypeScopes = () => {
const streamDocument: StreamDocument = useDocument();
const pageSet = parseJsonObject(streamDocument?._pageset);
const locatorConfig: LocatorConfig = pageSet?.typeConfig?.locatorConfig;

// legacy support for single page set locators
if (locatorConfig?.entityType) {
return [
{
entityType: locatorConfig.entityType,
savedFilter: locatorConfig?.savedFilter,
},
];
}

const locatorSourcePageSets = getLocatorSourcePageSets();
const locatorSourcePageSetScopes = locatorSourcePageSets.map(
(v) =>
({
entityType: v.entityType,
savedFilter: v.savedFilter,
}) as EntityTypeScope
);
const independentConfigScopes = locatorConfig?.entityTypeScopes || [];
return [...locatorSourcePageSetScopes, ...independentConfigScopes];
};

/** Retrieves the parsed JSON object from a string; returns an empty object if parsing fails. */
const parseJsonObject = (jsonString: string | undefined) => {
if (!jsonString) {
return {};
}
try {
const entityType = JSON.parse(entityDocument._pageset).typeConfig
.locatorConfig.entityType;
return entityType || DEFAULT_ENTITY_TYPE;
return JSON.parse(jsonString);
} catch {
return DEFAULT_ENTITY_TYPE;
return {};
}
};

function getFacetFieldOptions(entityType: string): DynamicOption<string>[] {
const getLocatorSourcePageSets = () => {
const doc: StreamDocument = useDocument();
const locatorSourcePageSets: Record<string, LocatorSourcePageSetInfo> =
parseJsonObject(doc?.__?.locatorSourcePageSets);
return Object.values(locatorSourcePageSets);
};

function getFacetFieldOptions(entityTypes: string[]): DynamicOption<string>[] {
const facetFields: DynamicOption<string>[] = [];
const addedValues: Set<string> = new Set<string>();
entityTypes.forEach((entityType) =>
getFacetFieldOptionsForEntityType(entityType).forEach((option) => {
if (option?.value && !addedValues.has(option.value)) {
facetFields.push(option);
addedValues.add(option.value);
}
})
);
return facetFields.sort((a, b) => a.label.localeCompare(b.label));
}

function getFacetFieldOptionsForEntityType(
entityType: string
): DynamicOption<string>[] {
let filterOptions: DynamicOption<string>[] = [];
switch (entityType) {
case "location":
Expand Down Expand Up @@ -435,7 +519,7 @@ function getFacetFieldOptions(entityType: string): DynamicOption<string>[] {
default:
filterOptions = [];
}
return filterOptions.sort((a, b) => a.label.localeCompare(b.label));
return filterOptions;
}

export interface LocatorProps {
Expand Down Expand Up @@ -551,8 +635,11 @@ const locatorFields: Fields<LocatorProps> = {
type: "dynamicSelect",
dropdownLabel: msg("fields.field", "Field"),
getOptions: () => {
const entityType = getEntityType();
return getFacetFieldOptions(entityType);
const entityTypeScopes = getAllEntityTypeScopes();
const entityTypes = entityTypeScopes
.map((scope) => scope?.entityType)
.filter((s) => s !== undefined);
return getFacetFieldOptions(entityTypes);
},
placeholderOptionLabel: msg(
"fields.options.selectAField",
Expand Down Expand Up @@ -735,7 +822,7 @@ const LocatorInternal = ({

const { t, i18n } = useTranslation();
const preferredUnit = getPreferredDistanceUnit(i18n.language);
const entityType = getEntityType(puck.metadata?.entityTypeEnvVar);
const entityType = getAnyEntityType(puck.metadata?.entityTypeEnvVar);
const streamDocument = useDocument();
const resultCount = useSearchState(
(state) => state.vertical.resultsCount || 0
Expand Down Expand Up @@ -1011,7 +1098,7 @@ const LocatorInternal = ({
.executeFilterSearch(queryParam, false, [
{
fieldApiName: LOCATION_FIELD,
entityType: entityType,
entityType: entityType, // only use a single entity type to avoid duplicate results
fetchEntities: false,
},
])
Expand Down Expand Up @@ -1256,6 +1343,7 @@ const LocatorInternal = ({
</Heading>
<FilterSearch
searchFields={[
// only use a single entity type to avoid duplicate results
{ fieldApiName: LOCATION_FIELD, entityType: entityType },
]}
onSelect={(params) => handleFilterSelect(params)}
Expand Down
6 changes: 3 additions & 3 deletions packages/visual-editor/src/components/LocatorResultCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { msg, pt } from "../utils/i18n/platform.ts";
import { PhoneAtom } from "./atoms/phone.tsx";
import { useTemplateProps } from "../hooks/useDocument.tsx";
import { resolveComponentData } from "../utils/resolveComponentData.tsx";
import { resolveUrlTemplateOfChild } from "../utils/urls/resolveUrlTemplate.ts";
import { resolveUrlFromSourcePageSets } from "../utils/urls/resolveUrlFromSourcePageSets.ts";
import { HoursStatusAtom } from "./atoms/hoursStatus.tsx";
import { HoursTableAtom } from "./atoms/hoursTable.tsx";
import { YextField } from "../editor/YextField.tsx";
Expand Down Expand Up @@ -760,7 +760,7 @@ export const LocatorResultCard = React.memo(
"TAP_TO_CALL"
);

const resolvedUrl = resolveUrlTemplateOfChild(
const resolvedUrl = resolveUrlFromSourcePageSets(
location,
streamDocument,
relativePrefixToRoot
Expand Down Expand Up @@ -899,7 +899,7 @@ export const LocatorResultCard = React.memo(
</div>
)}
<div className="flex flex-col lg:flex-row gap-2 lg:gap-4 w-full items-center md:items-stretch lg:items-center">
{props.primaryCTA.liveVisibility && (
{props.primaryCTA.liveVisibility && resolvedUrl && (
<CTA
link={resolvedUrl}
label={
Expand Down
22 changes: 22 additions & 0 deletions packages/visual-editor/src/utils/types/StreamDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type StreamDocument = {
visualEditorConfig?: string;
isPrimaryLocale?: boolean; // deprecated, use pathInfo.primaryLocale instead
entityPageSetUrlTemplates?: string;
locatorSourcePageSets?: string;
};
};

Expand All @@ -28,3 +29,24 @@ export type PathInfoShape = {
sourceEntityPageSetTemplate?: string;
[key: string]: any; // allow any other fields
};

export type LocatorConfig = {
source?: string;
experienceKey?: string;
entityType?: string; // deprecated
savedFilter?: string; // deprecated
entityTypeScopes?: EntityTypeScope[];
[key: string]: any; // allow any other fields
};

export type EntityTypeScope = {
entityType?: string;
savedFilter?: string;
};

export type LocatorSourcePageSetInfo = {
pathInfo?: PathInfoShape;
entityType?: string;
savedFilter?: number;
[key: string]: any;
};
Loading
Loading