feat: add file reference metafield support

This commit is contained in:
cathryngriffiths 2021-11-10 15:06:41 -05:00
parent 69576610c4
commit ccdcf93e33
17 changed files with 229 additions and 76 deletions

View file

@ -1,6 +1,6 @@
export default {
locale: 'en-us',
storeDomain: 'hydrogen-preview.myshopify.com',
storefrontToken: '3b580e70970c4528da70c98e097c2fa0',
storeDomain: 'react-advanced-hydrogen.myshopify.com',
storefrontToken: '267baac86464f30e5a6cd11451406690',
graphqlApiVersion: 'unstable',
};

View file

@ -111,6 +111,11 @@ export default function ProductDetails({product}) {
<>
<Seo product={product} />
<Product product={product} initialVariantId={initialVariant.id}>
<Product.Metafield
keyName="fileref"
namespace="my_fields"
className="w-full h-screen object-contain"
/>
<div className="grid grid-cols-1 md:grid-cols-[2fr,1fr] gap-x-8 my-16">
<div className="md:hidden mt-5 mb-8">
<Product.Title

View file

@ -132,6 +132,7 @@ const QUERY = gql`
$country: CountryCode
$numCollections: Int = 2
$numProducts: Int = 3
$includeReferenceMetafieldDetails: Boolean = false
$numProductMetafields: Int = 0
$numProductVariants: Int = 250
$numProductMedia: Int = 1

View file

@ -65,6 +65,7 @@ const QUERY = gql`
$handle: String!
$country: CountryCode
$numProducts: Int!
$includeReferenceMetafieldDetails: Boolean = false
$numProductMetafields: Int = 0
$numProductVariants: Int = 250
$numProductMedia: Int = 6

View file

@ -32,6 +32,7 @@ const QUERY = gql`
query product(
$country: CountryCode
$handle: String!
$includeReferenceMetafieldDetails: Boolean = true
$numProductMetafields: Int = 20
$numProductVariants: Int = 250
$numProductMedia: Int = 6

View file

@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
<!-- ## Unreleased -->
## Unreleased
- feat: add file reference metafield support
## 0.6.3 - 2021-11-10

View file

@ -6,6 +6,7 @@ import {StarRating} from './components/StarRating';
import {RawHtml} from '../RawHtml';
import {ParsedMetafield, Measurement, Rating} from '../../types';
import {MetafieldFragment as Fragment} from '../../graphql/graphql-constants';
import {Image} from '../Image';
export interface MetafieldProps {
/** A [Metafield object](/api/storefront/reference/common-objects/metafield) from the Storefront API. */
@ -103,6 +104,16 @@ export function Metafield<TTag extends ElementType>(
{JSON.stringify(metafield.value)}
</Wrapper>
);
case 'file_reference': {
if (
metafield.reference?.__typename === 'MediaImage' &&
metafield.reference.image != null
) {
return (
<Image image={metafield.reference.image} {...passthroughProps} />
);
}
}
default: {
const Wrapper = as ?? 'span';
return (

View file

@ -1,3 +1,5 @@
#import '../Image/ImageFragment.graphql'
fragment MetafieldFragment on Metafield {
id
type
@ -7,4 +9,13 @@ fragment MetafieldFragment on Metafield {
createdAt
updatedAt
description
reference @include(if: $includeReferenceMetafieldDetails) {
__typename
... on MediaImage {
mediaContentType
image {
...ImageFragment
}
}
}
}

View file

@ -1,5 +1,6 @@
import * as Types from '../../graphql/types/types';
import {ImageFragmentFragment} from '../Image/ImageFragment';
export type MetafieldFragmentFragment = {__typename?: 'Metafield'} & Pick<
Types.Metafield,
| 'id'
@ -10,4 +11,16 @@ export type MetafieldFragmentFragment = {__typename?: 'Metafield'} & Pick<
| 'createdAt'
| 'updatedAt'
| 'description'
>;
> & {
reference?: Types.Maybe<
| ({__typename: 'MediaImage'} & Pick<
Types.MediaImage,
'mediaContentType'
> & {
image?: Types.Maybe<{__typename?: 'Image'} & ImageFragmentFragment>;
})
| {__typename: 'Page'}
| {__typename: 'Product'}
| {__typename: 'ProductVariant'}
>;
};

View file

@ -3,7 +3,9 @@ import {Metafield} from '../Metafield.client';
import {getParsedMetafield} from '../../../utilities/tests/metafields';
import {mountWithShopifyProvider} from '../../../utilities/tests/shopify_provider';
import {RawHtml} from '../../RawHtml';
import {Image} from '../../Image';
import {StarRating} from '../components';
import {getMediaImage} from '../../../utilities/tests/media';
describe('<Metafield />', () => {
it('renders nothing when the metafield value is undefined', () => {
@ -936,68 +938,98 @@ describe('<Metafield />', () => {
});
describe('with `file_reference` type metafield', () => {
it('renders the file reference as a string in a `span` by default', () => {
const metafield = getParsedMetafield({type: 'file_reference'});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield} />
);
describe('when the reference type is a MediaImage', () => {
it('renders an Image component', () => {
const metafield = getParsedMetafield({
type: 'file_reference',
reference: {__typename: 'MediaImage', ...getMediaImage()},
});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield} />
);
expect(component).toContainReactComponent('span', {
children: metafield.value,
expect(component).toContainReactComponent(Image);
});
it.only('allows passthrough props', () => {
const metafield = getParsedMetafield({
type: 'file_reference',
reference: {__typename: 'MediaImage', ...getMediaImage()},
});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield} className="rounded-md" />
);
expect(component).toContainReactComponent(Image, {
className: 'rounded-md',
});
});
});
it('renders the file reference as a string in the element specified by the `as` prop', () => {
const metafield = getParsedMetafield({type: 'file_reference'});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield} as="p" />
);
describe('when the reference type is not a MediaImage', () => {
it('renders the file reference as a string in a `span` by default', () => {
const metafield = getParsedMetafield({type: 'file_reference'});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield} />
);
expect(component).toContainReactComponent('p', {
children: metafield.value,
expect(component).toContainReactComponent('span', {
children: metafield.value,
});
});
});
it('passes the metafield as a render prop to the children render function', () => {
const children = jest.fn().mockImplementation(() => {
return null;
it('renders the file reference as a string in the element specified by the `as` prop', () => {
const metafield = getParsedMetafield({type: 'file_reference'});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield} as="p" />
);
expect(component).toContainReactComponent('p', {
children: metafield.value,
});
});
const metafield = getParsedMetafield({type: 'file_reference'});
mountWithShopifyProvider(
<Metafield metafield={metafield}>{children}</Metafield>
);
it('passes the metafield as a render prop to the children render function', () => {
const children = jest.fn().mockImplementation(() => {
return null;
});
const metafield = getParsedMetafield({type: 'file_reference'});
expect(children).toHaveBeenCalledWith({
...metafield,
value: metafield.value,
mountWithShopifyProvider(
<Metafield metafield={metafield}>{children}</Metafield>
);
expect(children).toHaveBeenCalledWith({
...metafield,
value: metafield.value,
});
});
});
it('renders its children', () => {
const metafield = getParsedMetafield({type: 'file_reference'});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield}>
{({value}) => {
return <p>The reference is {value}</p>;
}}
</Metafield>
);
it('renders its children', () => {
const metafield = getParsedMetafield({type: 'file_reference'});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield}>
{({value}) => {
return <p>The reference is {value}</p>;
}}
</Metafield>
);
expect(component).toContainReactComponent('p', {
children: [`The reference is `, metafield.value],
expect(component).toContainReactComponent('p', {
children: [`The reference is `, metafield.value],
});
});
});
it('allows passthrough props', () => {
const component = mountWithShopifyProvider(
<Metafield
metafield={getParsedMetafield({type: 'file_reference'})}
className="emphasized"
/>
);
expect(component).toContainReactComponent('span', {
className: 'emphasized',
it('allows passthrough props', () => {
const component = mountWithShopifyProvider(
<Metafield
metafield={getParsedMetafield({type: 'file_reference'})}
className="emphasized"
/>
);
expect(component).toContainReactComponent('span', {
className: 'emphasized',
});
});
});
});

View file

@ -1,6 +1,6 @@
import {createContext} from 'react';
import {ProductOptionsHookValue} from '../../hooks';
import {GraphQLConnection, ParsedMetafield} from '../../types';
import {GraphQLConnection, ParsedMetafield, RawMetafield} from '../../types';
import {ProductProviderFragmentFragment} from './ProductProviderFragment';
import {Product} from './types';
import {Collection, Image} from '../../graphql/types/types';
@ -21,7 +21,7 @@ export type ProductContextType = Omit<
media?: ProductProviderFragmentFragment['media']['edges'][0]['node'][];
mediaConnection?: ProductProviderFragmentFragment['media'];
metafields?: ParsedMetafield[];
metafieldsConnection?: ProductProviderFragmentFragment['metafields'];
metafieldsConnection?: GraphQLConnection<RawMetafield>;
images?: Partial<Image>[];
imagesConnection?: GraphQLConnection<Partial<Image>>;
collections?: Partial<Collection>[];

View file

@ -1,5 +1,5 @@
import {SellingPlanGroup, Variant} from '../../hooks/useProductOptions';
import {GraphQLConnection} from '../../types';
import {GraphQLConnection, RawMetafield} from '../../types';
import {ProductProviderFragmentFragment} from './ProductProviderFragment';
import {ImageFragmentFragment} from '../Image/ImageFragment';
import {Collection} from '../../graphql/types/types';
@ -12,7 +12,7 @@ export interface Product {
handle?: ProductProviderFragmentFragment['descriptionHtml'];
id?: ProductProviderFragmentFragment['id'];
media?: ProductProviderFragmentFragment['media'];
metafields?: ProductProviderFragmentFragment['metafields'];
metafields?: GraphQLConnection<RawMetafield>;
priceRange?: Partial<ProductProviderFragmentFragment['priceRange']>;
title?: ProductProviderFragmentFragment['title'];
variants?: GraphQLConnection<Variant>;

View file

@ -2187,20 +2187,36 @@ fragment Model3DFragment on Model3d {
`;
/**
*```
* fragment MetafieldFragment on Metafield {
* id
* type
* namespace
* key
* value
* createdAt
* updatedAt
* description
* }
*```
*/
*```
* fragment MetafieldFragment on Metafield {
* id
* type
* namespace
* key
* value
* createdAt
* updatedAt
* description
* reference @include(if: $includeReferenceMetafieldDetails) {
* __typename
* ... on MediaImage {
* mediaContentType
* image {
* ...ImageFragment
* }
* }
* }
* }
* fragment ImageFragment on Image {
* id
* url
* altText
* width
* height
* }
*
*```
*/
export const MetafieldFragment: string = `fragment MetafieldFragment on Metafield {
id
type
@ -2210,6 +2226,22 @@ export const MetafieldFragment: string = `fragment MetafieldFragment on Metafiel
createdAt
updatedAt
description
reference @include(if: $includeReferenceMetafieldDetails) {
__typename
... on MediaImage {
mediaContentType
image {
...ImageFragment
}
}
}
}
fragment ImageFragment on Image {
id
url
altText
width
height
}
`;
@ -2344,6 +2376,15 @@ export const MoneyFragment: string = `fragment MoneyFragment on MoneyV2 {
* createdAt
* updatedAt
* description
* reference @include(if: $includeReferenceMetafieldDetails) {
* __typename
* ... on MediaImage {
* mediaContentType
* image {
* ...ImageFragment
* }
* }
* }
* }
*
* fragment VariantFragment on ProductVariant {
@ -2453,6 +2494,14 @@ export const MoneyFragment: string = `fragment MoneyFragment on MoneyV2 {
* }
* }
*
* fragment ImageFragment on Image {
* id
* url
* altText
* width
* height
* }
*
*
* fragment SellingPlanFragment on SellingPlan {
* id
@ -2633,6 +2682,15 @@ fragment MetafieldFragment on Metafield {
createdAt
updatedAt
description
reference @include(if: $includeReferenceMetafieldDetails) {
__typename
... on MediaImage {
mediaContentType
image {
...ImageFragment
}
}
}
}
fragment VariantFragment on ProductVariant {
@ -2742,6 +2800,14 @@ fragment Model3DFragment on Model3d {
}
}
fragment ImageFragment on Image {
id
url
altText
width
height
}
fragment SellingPlanFragment on SellingPlan {
id

View file

@ -1,6 +1,5 @@
import {useMemo} from 'react';
import {GraphQLConnection, ParsedMetafield} from '../../types';
import {Metafield} from '../../graphql/types/types';
import {GraphQLConnection, ParsedMetafield, RawMetafield} from '../../types';
import {flattenConnection, parseMetafieldValue} from '../../utilities';
/**
@ -8,7 +7,7 @@ import {flattenConnection, parseMetafieldValue} from '../../utilities';
* in an array of metafields whose `values` have been parsed according to the metafield `type`.
*/
export function useParsedMetafields(
metafields: GraphQLConnection<Partial<Metafield>> | undefined
metafields: GraphQLConnection<RawMetafield> | undefined
): ParsedMetafield[] {
return useMemo(() => {
if (metafields == null) {

View file

@ -1,7 +1,7 @@
import type {ServerResponse} from 'http';
import type {ServerComponentResponse} from './framework/Hydration/ServerComponentResponse.server';
import type {ServerComponentRequest} from './framework/Hydration/ServerComponentRequest.server';
import type {Metafield} from './graphql/types/types';
import type {Metafield, MediaImage} from './graphql/types/types';
export type Renderer = (
url: URL,
@ -71,8 +71,14 @@ export interface GraphQLConnection<T> {
edges?: {node: T}[];
}
export type RawMetafield = Partial<Metafield>;
export type ParsedMetafield = Omit<Partial<Metafield>, 'value'> & {
export type RawMetafield = Omit<Partial<Metafield>, 'reference'> & {
reference?: MediaImage;
};
export type ParsedMetafield = Omit<
Partial<Metafield>,
'value' | 'reference'
> & {
value?:
| string
| number
@ -81,6 +87,7 @@ export type ParsedMetafield = Omit<Partial<Metafield>, 'value'> & {
| Date
| Rating
| Measurement;
reference?: MediaImage | null;
};
export interface Rating {

View file

@ -25,6 +25,9 @@ export function getPreviewImage(image: Partial<Image> = {}) {
url: image.url ?? faker.random.image(),
width: image.width ?? faker.datatype.number(),
height: image.height ?? faker.datatype.number(),
originalSrc: '',
transformedSrc: '',
src: '',
};
}

View file

@ -46,7 +46,7 @@ export const METAFIELDS: MetafieldType[] = [
export function getRawMetafield(
metafield: Partial<Metafield> & {type?: MetafieldType} = {}
): Omit<Metafield, 'parentResource' | 'valueType'> {
): RawMetafield {
const type: MetafieldType =
metafield.type == null
? faker.random.arrayElement(METAFIELDS)
@ -62,6 +62,7 @@ export function getRawMetafield(
type,
updatedAt: metafield.updatedAt ?? faker.date.recent(),
value: metafield.value ?? getMetafieldValue(type),
reference: metafield.reference as any,
};
}