Skip to content

Commit

Permalink
feat: Add external transformers (#126)
Browse files Browse the repository at this point in the history
* Adds external transformers

* Rename image element fields to match original

* Adds default element transformer

* Adds transformer tests

* exports transformers from package

* Changes default transform to strip assets

* Changes transforms from tuple to object with in and out props

* Hanldes elements not included in the map
  • Loading branch information
SHession authored Sep 21, 2021
1 parent 70bfe54 commit 23087ea
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 10 deletions.
58 changes: 58 additions & 0 deletions src/elements/__tests__/transform.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
transformElementIn,
transformElementOut,
} from "../transformer/transform";

describe("transform", () => {
describe("transformIn", () => {
it("should not allow elements which are the wrong type", () => {
// @ts-expect-error -- we should not be able to transform a malformed element
transformElementIn("code", {});

transformElementIn("code", {
assets: [],
// @ts-expect-error -- we should not be able to transform a malformed element
fields: { nonExistantField: "123" },
});
});
it("should partially transform elements with no fields", () => {
const codeElement = { assets: [], fields: {} };
const result = transformElementIn("code", codeElement);

expect(result).toEqual({});
});
it("should partially transform elements with some fields", () => {
const codeElement = { assets: [], fields: { html: "123" } };
const result = transformElementIn("code", codeElement);

expect(result).toEqual({ html: "123" });
});
it("should completely transform elements with all fields", () => {
const codeElement = {
assets: [],
fields: { html: "123", langaue: "HTML" },
};
const result = transformElementIn("code", codeElement);

expect(result).toEqual({ html: "123", langaue: "HTML" });
});
});
describe("transformOut", () => {
it("should not allow elements which are the wrong type", () => {
// @ts-expect-error -- we should not be able to transform a malformed element
transformElementOut("code", {});

// @ts-expect-error -- we should not be able to transform a malformed element
transformElementOut("code", { html: "123" });
});
it("should completely transform elements with all fields", () => {
const codeElement = { html: "123", language: "HTML" };
const result = transformElementOut("code", codeElement);

expect(result).toEqual({
assets: [],
fields: { html: "123", language: "HTML" },
});
});
});
});
6 changes: 3 additions & 3 deletions src/elements/image/ImageElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const createImageFields = (
openImageSelector: (setMedia: SetMedia, mediaId?: string) => void
) => {
return {
altText: createTextField({
alt: createTextField({
rows: 2,
validators: [htmlMaxLength(1000), htmlRequired()],
}),
Expand All @@ -57,7 +57,7 @@ export const createImageFields = (
},
validators: [htmlMaxLength(600)],
}),
displayCreditInformation: createCustomField(true, true),
displayCredit: createCustomField(true, true),
imageType: createCustomField("Photograph", [
{ text: "Photograph", value: "Photograph" },
{ text: "Illustration", value: "Illustration" },
Expand All @@ -77,7 +77,7 @@ export const createImageFields = (
source: createTextField({
validators: [htmlMaxLength(250), htmlRequired()],
}),
weighting: createCustomField("none-selected", [
role: createCustomField("none-selected", [
{ text: "inline (default)", value: "none-selected" },
{ text: "supporting", value: "supporting" },
{ text: "showcase", value: "showcase" },
Expand Down
14 changes: 7 additions & 7 deletions src/elements/image/ImageElementForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export const ImageElementForm: React.FunctionComponent<Props> = ({
<Column width={2 / 5}>
<FieldLayoutVertical>
<CustomDropdownView
field={fields.weighting}
field={fields.role}
label="Weighting"
errors={errors.weighting}
errors={errors.role}
/>
<ImageView
field={fields.mainImage}
Expand All @@ -82,16 +82,16 @@ export const ImageElementForm: React.FunctionComponent<Props> = ({
label="Caption"
/>
<FieldWrapper
field={fields.altText}
errors={errors.altText}
field={fields.alt}
errors={errors.alt}
label={
<>
<AltText>Alt text</AltText>
<Button
priority="secondary"
size="xsmall"
iconSide="left"
onClick={() => fields.altText.update(fieldValues.caption)}
onClick={() => fields.alt.update(fieldValues.caption)}
>
Copy from caption
</Button>
Expand All @@ -115,8 +115,8 @@ export const ImageElementForm: React.FunctionComponent<Props> = ({
</Column>
</Columns>
<CustomCheckboxView
field={fields.displayCreditInformation}
errors={errors.displayCreditInformation}
field={fields.displayCredit}
errors={errors.displayCredit}
label="Display credit information"
/>
</FieldLayoutVertical>
Expand Down
101 changes: 101 additions & 0 deletions src/elements/image/imageElementDataTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { FieldNameToValueMap } from "../../plugin/fieldViews/helpers";
import type { TransformIn, TransformOut } from "../transformer/types/Transform";
import type { Asset, createImageFields, MainImageData } from "./ImageElement";

type ImageFields = {
alt: string;
caption: string;
displayCredit: string;
imageType: string;
isMandatory: string;
mediaApiUri: string;
mediaId: string;
photographer: string;
role: string;
source: string;
suppliersReference: string;
};

type ExternalImageData = {
assets: Asset[];
fields: ImageFields;
};

type PartialExternalImageData = {
assets: Asset[];
fields: Partial<ImageFields>;
};

export const transformElementIn: TransformIn<
PartialExternalImageData,
ReturnType<typeof createImageFields>
> = ({ assets, fields }) => {
const {
alt,
caption,
displayCredit,
imageType,
mediaApiUri,
mediaId,
photographer,
role,
source,
suppliersReference,
} = fields;

const mainImage: MainImageData | undefined = {
assets,
suppliersReference: suppliersReference ?? "",
mediaId,
mediaApiUri,
};

return {
alt,
caption,
displayCredit: displayCredit === "true",
imageType,
photographer,
role,
source,
mainImage,
};
};

export const transformElementOut: TransformOut<
ExternalImageData,
ReturnType<typeof createImageFields>
> = ({
alt,
caption,
displayCredit,
imageType,
photographer,
role,
source,
mainImage,
}: FieldNameToValueMap<
ReturnType<typeof createImageFields>
>): ExternalImageData => {
return {
assets: mainImage.assets,
fields: {
alt,
caption,
displayCredit: displayCredit.toString(),
imageType,
isMandatory: "true",
mediaApiUri: mainImage.mediaApiUri ?? "",
mediaId: mainImage.mediaId ?? "",
photographer,
role,
source,
suppliersReference: mainImage.suppliersReference,
},
};
};

export const transformElement = {
in: transformElementIn,
out: transformElementOut,
};
46 changes: 46 additions & 0 deletions src/elements/transformer/defaultTransform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { FieldNameToValueMap } from "../../plugin/fieldViews/helpers";
import type { FieldDescriptions } from "../../plugin/types/Element";
import type { TransformIn, TransformOut } from "./types/Transform";

type FlexibleModelElement<FDesc extends FieldDescriptions<string>> = {
fields: Partial<Omit<FieldNameToValueMap<FDesc>, "assets">> & {
isMandatory?: boolean;
};
assets?: string[];
};

export const transformElementDataIn = <
FDesc extends FieldDescriptions<string>
>(): TransformIn<FlexibleModelElement<FDesc>, FDesc> => ({ fields }) => {
return ({ ...fields } as unknown) as FieldNameToValueMap<FDesc>;
};

export const transformElementDataOut = <
FDesc extends FieldDescriptions<string>
>(
isMandatory?: boolean
): TransformOut<FlexibleModelElement<FDesc>, FDesc> => ({
assets,
...fields
}: FieldNameToValueMap<FDesc>) => {
const baseFields = {
assets: assets || [],
fields: { ...fields },
} as FlexibleModelElement<FDesc>;

if (isMandatory === undefined) {
return baseFields;
}

return {
...baseFields,
fields: { ...fields, isMandatory },
};
};

export const transformElement = <FDesc extends FieldDescriptions<string>>() => {
return {
in: transformElementDataIn<FDesc>(),
out: transformElementDataOut<FDesc>(),
};
};
50 changes: 50 additions & 0 deletions src/elements/transformer/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { codeFields } from "../code/CodeElementSpec";
import type { embedFields } from "../embed/EmbedSpec";
import { transformElement as imageElementTransform } from "../image/imageElementDataTransformer";
import type { pullquoteFields } from "../pullquote/PullquoteSpec";
import { transformElement as defaultElementTransform } from "./defaultTransform";

const transformMap = {
code: defaultElementTransform<typeof codeFields>(),
embed: defaultElementTransform<typeof embedFields>(),
image: imageElementTransform,
pullquote: defaultElementTransform<typeof pullquoteFields>(),
} as const;

type TransformMap = typeof transformMap;
type TransformMapIn<Name extends keyof TransformMap> = TransformMap[Name]["in"];
type TransformMapOut<
Name extends keyof TransformMap
> = TransformMap[Name]["out"];

export const transformElementIn = <Name extends keyof TransformMap>(
elementName: Name,
values: Parameters<TransformMapIn<Name>>[0]
): ReturnType<TransformMapIn<Name>> | undefined => {
const transformer = transformMap[elementName];

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This may be used in a JS context and be falst
if (transformer) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Required due to typesafety complexity between transformer and params
const result = transformer.in((values as unknown) as any);
return result as ReturnType<TransformMapIn<Name>>;
} else {
return undefined;
}
};

export const transformElementOut = <Name extends keyof TransformMap>(
elementName: Name,
values: Parameters<TransformMapOut<Name>>[0]
): ReturnType<TransformMapOut<Name>> | undefined => {
const transformer = transformMap[elementName];

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This may be used in a JS context and be falst
if (transformer) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Required due to typesafety complexity between transformer and params
const result = transformer.out((values as unknown) as any);
return result as ReturnType<TransformMapOut<Name>>;
} else {
return undefined;
}
};
12 changes: 12 additions & 0 deletions src/elements/transformer/types/Transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { FieldNameToValueMap } from "../../../plugin/fieldViews/helpers";
import type { FieldDescriptions } from "../../../plugin/types/Element";

export type TransformIn<
ExternalData,
FDesc extends FieldDescriptions<string>
> = (data: ExternalData) => Partial<FieldNameToValueMap<FDesc>>;

export type TransformOut<
ExternalData,
FDesc extends FieldDescriptions<string>
> = (data: FieldNameToValueMap<FDesc>) => ExternalData;
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ export { createEmbedElement } from "./elements/embed/EmbedSpec";
export { pullquoteElement } from "./elements/pullquote/PullquoteSpec";
export { codeElement } from "./elements/code/CodeElementSpec";
export { createImageElement } from "./elements/image/ImageElement";
export {
transformElementIn,
transformElementOut,
} from "./elements/transformer/transform";

0 comments on commit 23087ea

Please sign in to comment.