Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useMemo } from "react";
import { HydratedIntegrationFieldExistent } from "services/api";
import { isIntegrationFieldMapping } from "src/utils/manifest";
import {
getFieldDisplayName,
isIntegrationFieldMapping,
} from "src/utils/manifest";

import {
CheckboxItem,
Expand Down Expand Up @@ -36,7 +39,7 @@ export function OptionalFieldsV2() {
)
.map((field) => ({
id: field.fieldName,
label: field.displayName,
label: getFieldDisplayName(field),
isChecked: !!selectedOptionalFields?.[field.fieldName],
}))
.sort((a, b) => a.label.localeCompare(b.label)) || [],
Expand Down
9 changes: 7 additions & 2 deletions src/components/Configure/content/fields/RequiredFields.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useManifest } from "src/headless";
import { useProjectQuery } from "src/hooks/query";
import { isIntegrationFieldMapping } from "src/utils/manifest";
import {
getFieldDisplayName,
isIntegrationFieldMapping,
} from "src/utils/manifest";

import { Tag } from "components/ui-base/Tag";

Expand Down Expand Up @@ -39,7 +42,9 @@ export function RequiredFields() {
{requiredFields?.length
? requiredFields.map((field) => {
if (isIntegrationFieldMapping(field)) return null;
return <Tag key={field.fieldName}>{field.displayName}</Tag>;
return (
<Tag key={field.fieldName}>{getFieldDisplayName(field)}</Tag>
);
})
: "There are no required fields."}
</div>
Expand Down
5 changes: 3 additions & 2 deletions src/components/InstallWizard/steps/ReviewStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
useManifest,
} from "src/headless";
import { handleServerError } from "src/utils/handleServerError";
import { getFieldDisplayName } from "src/utils/manifest";

import { StepHeader } from "../components/StepHeader";
import { useWizard } from "../wizard/WizardContext";
Expand All @@ -34,7 +35,7 @@ export function ReviewStep() {
manifestObj?.getRequiredFields("no-mappings") ?? []
)
.filter((f) => "fieldName" in f)
.map((f) => ("fieldName" in f ? f.displayName || f.fieldName : ""));
.map(getFieldDisplayName);

// Selected optional fields
const selectedOptionalFields = (
Expand All @@ -44,7 +45,7 @@ export function ReviewStep() {
const fieldName = "fieldName" in f ? f.fieldName : "";
return fieldName && configObj?.getSelectedField(fieldName);
})
.map((f) => ("fieldName" in f ? f.displayName || f.fieldName : ""));
.map(getFieldDisplayName);

// Configured field mappings (source → destination)
const allMapFields = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useInstallIntegrationProps } from "context/InstallIIntegrationContextPr
import { useLocalConfig, useManifest } from "src/headless";
import { useProjectQuery } from "src/hooks/query/useProjectQuery";
import { useProvider } from "src/hooks/useProvider";
import { getFieldDisplayName } from "src/utils/manifest";

import type { CheckboxItem } from "components/ui-base/Checkbox/CheckboxPagination";

Expand All @@ -13,7 +14,7 @@ import { AdditionalFieldsContent } from "./additional/AdditionalFieldsContent";
import { FieldsContent } from "./fields/FieldsContent";
import { MappingsContent } from "./mappings/MappingsContent";
import { ObjectTabs } from "./ObjectTabs";
import { getFieldDisplayName, getFieldName } from "./subPageUtils";
import { getFieldName } from "./subPageUtils";
import { useSubPageNavigation } from "./useSubPageNavigation";

import styles from "./configureObjectsStep.module.css";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { HydratedIntegrationField } from "@generated/api/src";
import { ArrowRightIcon, WidthIcon } from "@radix-ui/react-icons";
import { getFieldDisplayName } from "src/utils/manifest";

import { SectionHeader } from "../../../components/SectionHeader";
import { getFieldDisplayName, getFieldName } from "../subPageUtils";
import { getFieldName } from "../subPageUtils";

import sharedStyles from "../configureObjectsStep.module.css";
import styles from "./fieldsContent.module.css";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ export function getFieldName(field: HydratedIntegrationField): string {
return isExistentField(field) ? field.fieldName : "";
}

export function getFieldDisplayName(field: HydratedIntegrationField): string {
if (isExistentField(field)) return field.displayName || field.fieldName;
return "";
}

/**
* Determine the initial sub-page for an object based on what data it has.
*/
Expand Down
104 changes: 104 additions & 0 deletions src/utils/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type {
HydratedIntegrationFieldExistent,
IntegrationFieldMapping,
} from "@generated/api/src";
import { describe, expect, it } from "@jest/globals";

import { getFieldDisplayName, isIntegrationFieldMapping } from "./manifest";

describe("isIntegrationFieldMapping", () => {
it("treats a field without fieldName as a mapping", () => {
expect(isIntegrationFieldMapping({ mapToName: "priority" })).toBe(true);
});

it("treats a field with fieldName as an existent field", () => {
expect(
isIntegrationFieldMapping({ fieldName: "email", displayName: "Email" }),
).toBe(false);
});
});

describe("getFieldDisplayName", () => {
describe("existent fields", () => {
it("uses the provider displayName when the builder defined no mapping", () => {
const field: HydratedIntegrationFieldExistent = {
fieldName: "email",
displayName: "Email",
};

expect(getFieldDisplayName(field)).toBe("Email");
});

it("falls back to fieldName when there is no displayName", () => {
const field = {
fieldName: "estimate_number",
displayName: "",
} as HydratedIntegrationFieldExistent;

expect(getFieldDisplayName(field)).toBe("estimate_number");
});

// The reported bug. A nested field has no provider metadata, so the server
// hydrates displayName as TitleCase(fieldName) and the raw path renders.
it("prefers mapToDisplayName over a JSONPath displayName", () => {
const field: HydratedIntegrationFieldExistent = {
fieldName: "$['Customer']['Email']",
displayName: "$['Customer']['Email']",
mapToName: "customer_email",
mapToDisplayName: "Customer Email",
};

expect(getFieldDisplayName(field)).toBe("Customer Email");
});

// displayName outranks mapToName, and the server always populates it, so a
// nested field needs mapToDisplayName to render a readable label.
it("falls back to displayName when mapToDisplayName is absent", () => {
const field: HydratedIntegrationFieldExistent = {
fieldName: "$['Customer']['First_name']",
displayName: "$['Customer']['First_name']",
mapToName: "customer_first_name",
};

expect(getFieldDisplayName(field)).toBe("$['Customer']['First_name']");
});

it("prefers mapToDisplayName over a usable provider displayName", () => {
const field: HydratedIntegrationFieldExistent = {
fieldName: "firstname",
displayName: "First Name",
mapToName: "customer_first_name",
mapToDisplayName: "Customer First Name",
};

expect(getFieldDisplayName(field)).toBe("Customer First Name");
});

it("prefers the provider displayName over mapToName", () => {
const field: HydratedIntegrationFieldExistent = {
fieldName: "phone",
displayName: "Business Phone",
mapToName: "customer_phone",
};

expect(getFieldDisplayName(field)).toBe("Business Phone");
});
});

describe("field mappings", () => {
it("uses mapToDisplayName when present", () => {
const field: IntegrationFieldMapping = {
mapToName: "priority",
mapToDisplayName: "Priority",
};

expect(getFieldDisplayName(field)).toBe("Priority");
});

it("falls back to mapToName", () => {
const field: IntegrationFieldMapping = { mapToName: "priority" };

expect(getFieldDisplayName(field)).toBe("priority");
});
});
});
30 changes: 30 additions & 0 deletions src/utils/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,36 @@ export function isIntegrationFieldMapping(
return !(field as HydratedIntegrationFieldExistent).fieldName;
}

/**
* Returns the label to show for a field in the read UI.
*
* Precedence: mapToDisplayName > displayName > mapToName > fieldName.
*
* mapToDisplayName wins so a builder can give a nested field a readable label.
* The provider has no metadata for a nested path, so the server falls back to
* TitleCase(fieldName) and the raw JSONPath would otherwise show, e.g.
* "$['Customer']['Email']".
*
* displayName sits above mapToName so a provider's own label is preferred over a
* builder's machine name. Note the server always populates displayName, so
* mapToName is only reached for an IntegrationFieldMapping, which has none.
*
* @param field HydratedIntegrationField
* @returns string
*/
export function getFieldDisplayName(field: HydratedIntegrationField): string {
// displayName and fieldName only exist on HydratedIntegrationFieldExistent.
// On an IntegrationFieldMapping they are undefined and get skipped.
const existent = field as HydratedIntegrationFieldExistent;

return (
field.mapToDisplayName ||
Comment thread
anushat05 marked this conversation as resolved.
existent.displayName ||
field.mapToName ||
existent.fieldName
);
Comment thread
anushat05 marked this conversation as resolved.
}

/**
* Returns the required existent fields from an object (mappings excluded).
* For required mapping fields use getRequiredMapFieldsFromObject.
Expand Down
Loading