Skip to content

Commit

Permalink
refactor: mappable fields with static values version 2 (#53)
Browse files Browse the repository at this point in the history
Demo:
https://www.loom.com/share/79a23c05d0984ced80d6bddc9bc0fe1e?sid=eb855a66-4130-4034-a3a2-ed445235cbe6

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
benlife5 and github-actions[bot] authored Sep 25, 2024
1 parent 745fcb1 commit 3dfe038
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 273 deletions.
8 changes: 4 additions & 4 deletions THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -6800,7 +6800,7 @@ Apache-2.0

The following npm package may be included in this product:

- [email protected].30001660
- [email protected].30001663

This package contains the following license and notice below:

Expand Down Expand Up @@ -8135,7 +8135,7 @@ distributed under the terms of the MIT license above.

The following npm package may be included in this product:

- [email protected].25
- [email protected].28

This package contains the following license and notice below:

Expand Down Expand Up @@ -12403,7 +12403,7 @@ THE SOFTWARE.
The following npm packages may be included in this product:

- [email protected]
- [email protected].0
- [email protected].1
- [email protected]

These packages each contain the following license and notice below:
Expand Down Expand Up @@ -13893,7 +13893,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

The following npm package may be included in this product:

- browserslist@4.23.3
- browserslist@4.24.0

This package contains the following license and notice below:

Expand Down
152 changes: 76 additions & 76 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

42 changes: 24 additions & 18 deletions src/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,30 +68,38 @@ import { EntityField } from "@yext/visual-editor";

### YextEntityFieldSelector

Use this to allow Visual Editor users to choose an entity field or a constant value that will populate data into a component.
Use this to allow Visual Editor users to choose an entity field or static value that will populate data into a component.
The user can choose an entity field from a dropdown or select "Use Static Value". Regardless, the user should always
enter a static value as it will be used as a fallback value in the case that the entity is missing the selected entity field.

#### Props

| Name | Type | Description |
| ------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
| useDocument | function | See [@yext/pages useDocument](https://github.com/yext/pages/blob/main/packages/pages/src/util/README.md#usedocument) |
| label? | string | The user-facing label for the field. |
| filter?.types? | string[] | Determines which fields will be available based on field type. |
| filter?.includeSubfields? | boolean | |
| filter?.allowList? | types: string[] | Field names to include. Cannot be combined with disallowList. |
| filter?.disallowList? | types: string[] | Field names to exclude. Cannot be combined with allowList. |
| Name | Type | Description |
| ------------------------- | --------------- | -------------------------------------------------------------- |
| label? | string | The user-facing label for the field. |
| filter?.types? | string[] | Determines which fields will be available based on field type. |
| filter?.includeSubfields? | boolean | |
| filter?.allowList? | types: string[] | Field names to include. Cannot be combined with disallowList. |
| filter?.disallowList? | types: string[] | Field names to exclude. Cannot be combined with allowList. |

### resolveDataForEntityField
### resolveYextEntityField

Determines whether to use a constant value or an entity field. Used as [Puck's resolveData function](https://puckeditor.com/docs/api-reference/configuration/component-config#resolvedatadata-params).
Used in a component's render function to pull in the selected entity field's value from the document or use the static value.

#### Props

| Name | Type |
| ----------- | ------------------- |
| document | Record<string, any> |
| entityField | EntityFieldType |

### Usage

```tsx
import {
EntityFieldType,
YextEntityFieldSelector,
resolveDataForEntityField,
resolveYextEntityField,
} from "@yext/visual-editor";
import { config } from "../templates/myTemplate";
import { useDocument } from "@yext/pages/util";
Expand All @@ -109,7 +117,6 @@ const exampleFields: Fields<ExampleProps> = {
objectFields: {
//@ts-expect-error ts(2322)
entityField: YextEntityFieldSelector<typeof config>({
useDocument: useDocument,
label: "Entity Field",
filter: {
types: ["type.string"],
Expand All @@ -126,18 +133,17 @@ export const ExampleComponent: ComponentConfig<ExampleProps> = {
defaultProps: {
myField: {
entityField: {
name: "",
value: "This is an example", // default constant value
fieldName: "", // default to Use Static Value
staticValue: "Example Text", // default static value
},
},
},
label: "Example Component",
resolveData: (props, changed) =>
resolveDataForEntityField<ExampleProps>("myField", props, changed),
render: ({ myField }) => <Example myField={myField} />,
};

const Example = ({ myField }: ExampleProps) => {
return <p>{myField.entityField.value}</p>;
const document = useDocument();
return <p>{resolveYextEntityField(document, myField.entityField)}</p>;
};
```
107 changes: 16 additions & 91 deletions src/components/YextEntityFieldSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,37 @@
import React from "react";
import {
AutoField,
FieldLabel,
ComponentData,
DefaultComponentProps,
} from "@measured/puck";
import { Unlock } from "lucide-react";
import { AutoField, FieldLabel } from "@measured/puck";
import { RenderProps } from "../utils/renderEntityFields.tsx";
import {
getFilteredEntityFields,
RenderEntityFieldFilter,
} from "../utils/getFilteredEntityFields.ts";
import { resolveProp } from "../utils/resolveProp.ts";

export type EntityFieldType = {
name: string;
value: string;
fieldName: string;
staticValue: string;
};

export type RenderYextEntityFieldSelectorProps<T extends Record<string, any>> =
{
useDocument: (...args: any[]) => any;
label: string;
filter: RenderEntityFieldFilter<T>;
};

type ResolveDataForEntityFieldProps<T extends DefaultComponentProps> = Omit<
ComponentData<T, string>,
"type"
>;

type ResolveDataForEntityFieldChanged<T extends DefaultComponentProps> = {
changed: Partial<Record<keyof T, boolean>>;
lastData: Omit<ComponentData<T, string>, "type"> | null;
};

/**
* Allows the user to select an entity field from the document or set a constant value.
* Allows the user to select an entity field from the document and set a static value.
*/
export const YextEntityFieldSelector = <T extends Record<string, any>>(
props: RenderYextEntityFieldSelectorProps<T>
) => {
return {
type: "custom",
label: props.label,
render: ({ field, value, onChange, readOnly }: RenderProps) => {
render: ({ field, value, onChange }: RenderProps) => {
const filteredEntityFields = getFilteredEntityFields(props.filter);
const document = props.useDocument();

return (
<>
<FieldLabel
label={field.label || "Label is undefined"}
//@ts-expect-error ts(2367)
readOnly={readOnly === "name"}
>
<FieldLabel label={field.label || "Label is undefined"}>
<AutoField
field={{
type: "select",
Expand All @@ -68,50 +45,27 @@ export const YextEntityFieldSelector = <T extends Record<string, any>>(
}),
],
}}
onChange={(value) => {
onChange={(selectedEntityFieldName) => {
onChange({
name: value as unknown as string, // hack because the option value is a string so it comes back as a string even though TS thinks it's an object
value: resolveProp(document, value as unknown as string),
fieldName: selectedEntityFieldName,
staticValue: value.staticValue,
});
}}
value={value?.name}
//@ts-expect-error ts(2367)
readOnly={readOnly === "name"}
value={value?.fieldName}
/>
{value?.name && (
<button
type="button"
className={"entityField"}
onClick={() => {
onChange({ name: "", value: value?.value });
}}
//@ts-expect-error ts(2367)
disabled={readOnly === "name"}
>
<span className="entityField-unlock-icon">
<Unlock size={16} />
</span>
<span>Use a constant value</span>
</button>
)}
</FieldLabel>
<FieldLabel
//@ts-expect-error ts(2367)
label={readOnly === "value" ? "Value" : "Constant Value"}
//@ts-expect-error ts(2367)
readOnly={readOnly === "value"}
className="entityField-value"
label={"Static Value"}
className="entityField-staticValue"
>
<AutoField
//@ts-expect-error ts(2367)
readOnly={readOnly === "value"}
onChange={(value) =>
onChange={(newStaticValue) =>
onChange({
name: "",
value: value,
fieldName: value.fieldName,
staticValue: newStaticValue,
})
}
value={value?.value}
value={value?.staticValue}
field={{
type: "text",
}}
Expand All @@ -122,32 +76,3 @@ export const YextEntityFieldSelector = <T extends Record<string, any>>(
},
};
};

/**
* Locks or unlocks the constant value field on prop value update.
*/
export const resolveDataForEntityField = <T extends DefaultComponentProps>(
fieldName: keyof T,
{ props }: ResolveDataForEntityFieldProps<T>,
{ changed }: ResolveDataForEntityFieldChanged<T>
) => {
if (props && props[fieldName] && !props[fieldName].entityField?.name) {
return {
props,
readOnly: {
[fieldName.toString() + ".entityField"]: false,
},
};
}

if (!changed[fieldName]) {
return { props };
}

return {
props,
readOnly: {
[fieldName.toString() + ".entityField"]: "value",
},
};
};
18 changes: 1 addition & 17 deletions src/components/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,7 @@
margin-left: -1px;
}

.entityField:focus-visible {
outline: 2px solid var(--puck-color-azure-05);
outline-offset: 2px;
z-index: 1;
}

.entityField:hover {
background: var(--puck-color-azure-12);
transition: none;

.entityField-unlock-icon {
color: var(--puck-color-azure-04);
transition: none;
}
}

.entityField-value {
.entityField-staticValue {
margin-top: 10px;
display: block;
}
5 changes: 1 addition & 4 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
export { Editor } from "./Editor.tsx";
export { EntityField } from "./EntityField.tsx";
export {
YextEntityFieldSelector,
resolveDataForEntityField,
} from "./YextEntityFieldSelector.tsx";
export { YextEntityFieldSelector } from "./YextEntityFieldSelector.tsx";
export type {
RenderYextEntityFieldSelectorProps,
EntityFieldType,
Expand Down
9 changes: 9 additions & 0 deletions src/utils/getFilteredEntityFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ export const getFilteredEntityFields = <T extends Record<string, any>>(
);

if (filter?.allowList) {
const streamFieldNames = filteredEntityFields.map((field) => field.name);
filter.allowList.forEach((field) => {
if (!streamFieldNames.includes(field)) {
console.warn(
`The entity field filter allowList included ${field}, which does not exist in the stream.`
);
}
});

filteredEntityFields = filteredEntityFields.filter((field) =>
filter.allowList.includes(field.name)
);
Expand Down
2 changes: 1 addition & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { renderEntityFields } from "./renderEntityFields.tsx";
export { resolveVisualEditorData } from "./resolveVisualEditorData.ts";
export { resolveProp } from "./resolveProp.ts";
export { resolveYextEntityField } from "./resolveYextEntityField.ts";
20 changes: 0 additions & 20 deletions src/utils/resolveProp.ts

This file was deleted.

37 changes: 37 additions & 0 deletions src/utils/resolveYextEntityField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { EntityFieldType } from "../components/YextEntityFieldSelector.tsx";

export const resolveYextEntityField = <T>(
document: any,
entityField: EntityFieldType
): T => {
// return static value if fieldName is not set
if (entityField.fieldName === "") {
return entityField.staticValue as T;
}

try {
// check for the entity field in the document
const steps: string[] = entityField.fieldName.split(".");
let missedStep = false;
let current = document;
for (let i = 0; i < steps.length; i++) {
if (current?.[steps[i]] !== undefined) {
current = current[steps[i]];
} else {
missedStep = true;
break;
}
}
if (!missedStep) {
return current;
}
} catch (e) {
console.error("Error in resolveYextEntityField:", e);
}

// if field not found, return static value as a fallback
console.warn(
`The field ${entityField.fieldName} was not found in the document, defaulting to static value ${entityField.staticValue}.`
);
return entityField.staticValue as T;
};
Loading

0 comments on commit 3dfe038

Please sign in to comment.