Compare commits

...

1 commit

Author SHA1 Message Date
cathryngriffiths ccdcf93e33 feat: add file reference metafield support 2021-11-10 15:18:34 -05:00
17 changed files with 229 additions and 76 deletions

View file

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

View file

@ -111,6 +111,11 @@ export default function ProductDetails({product}) {
<> <>
<Seo product={product} /> <Seo product={product} />
<Product product={product} initialVariantId={initialVariant.id}> <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="grid grid-cols-1 md:grid-cols-[2fr,1fr] gap-x-8 my-16">
<div className="md:hidden mt-5 mb-8"> <div className="md:hidden mt-5 mb-8">
<Product.Title <Product.Title

View file

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

View file

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

View file

@ -32,6 +32,7 @@ const QUERY = gql`
query product( query product(
$country: CountryCode $country: CountryCode
$handle: String! $handle: String!
$includeReferenceMetafieldDetails: Boolean = true
$numProductMetafields: Int = 20 $numProductMetafields: Int = 20
$numProductVariants: Int = 250 $numProductVariants: Int = 250
$numProductMedia: Int = 6 $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/) 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). 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 ## 0.6.3 - 2021-11-10

View file

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

View file

@ -1,3 +1,5 @@
#import '../Image/ImageFragment.graphql'
fragment MetafieldFragment on Metafield { fragment MetafieldFragment on Metafield {
id id
type type
@ -7,4 +9,13 @@ fragment MetafieldFragment on Metafield {
createdAt createdAt
updatedAt updatedAt
description 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 * as Types from '../../graphql/types/types';
import {ImageFragmentFragment} from '../Image/ImageFragment';
export type MetafieldFragmentFragment = {__typename?: 'Metafield'} & Pick< export type MetafieldFragmentFragment = {__typename?: 'Metafield'} & Pick<
Types.Metafield, Types.Metafield,
| 'id' | 'id'
@ -10,4 +11,16 @@ export type MetafieldFragmentFragment = {__typename?: 'Metafield'} & Pick<
| 'createdAt' | 'createdAt'
| 'updatedAt' | 'updatedAt'
| 'description' | '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 {getParsedMetafield} from '../../../utilities/tests/metafields';
import {mountWithShopifyProvider} from '../../../utilities/tests/shopify_provider'; import {mountWithShopifyProvider} from '../../../utilities/tests/shopify_provider';
import {RawHtml} from '../../RawHtml'; import {RawHtml} from '../../RawHtml';
import {Image} from '../../Image';
import {StarRating} from '../components'; import {StarRating} from '../components';
import {getMediaImage} from '../../../utilities/tests/media';
describe('<Metafield />', () => { describe('<Metafield />', () => {
it('renders nothing when the metafield value is undefined', () => { it('renders nothing when the metafield value is undefined', () => {
@ -936,68 +938,98 @@ describe('<Metafield />', () => {
}); });
describe('with `file_reference` type metafield', () => { describe('with `file_reference` type metafield', () => {
it('renders the file reference as a string in a `span` by default', () => { describe('when the reference type is a MediaImage', () => {
const metafield = getParsedMetafield({type: 'file_reference'}); it('renders an Image component', () => {
const component = mountWithShopifyProvider( const metafield = getParsedMetafield({
<Metafield metafield={metafield} /> type: 'file_reference',
); reference: {__typename: 'MediaImage', ...getMediaImage()},
});
const component = mountWithShopifyProvider(
<Metafield metafield={metafield} />
);
expect(component).toContainReactComponent('span', { expect(component).toContainReactComponent(Image);
children: metafield.value, });
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', () => { describe('when the reference type is not a MediaImage', () => {
const metafield = getParsedMetafield({type: 'file_reference'}); it('renders the file reference as a string in a `span` by default', () => {
const component = mountWithShopifyProvider( const metafield = getParsedMetafield({type: 'file_reference'});
<Metafield metafield={metafield} as="p" /> const component = mountWithShopifyProvider(
); <Metafield metafield={metafield} />
);
expect(component).toContainReactComponent('p', { expect(component).toContainReactComponent('span', {
children: metafield.value, children: metafield.value,
});
}); });
});
it('passes the metafield as a render prop to the children render function', () => { it('renders the file reference as a string in the element specified by the `as` prop', () => {
const children = jest.fn().mockImplementation(() => { const metafield = getParsedMetafield({type: 'file_reference'});
return null; const component = mountWithShopifyProvider(
<Metafield metafield={metafield} as="p" />
);
expect(component).toContainReactComponent('p', {
children: metafield.value,
});
}); });
const metafield = getParsedMetafield({type: 'file_reference'});
mountWithShopifyProvider( it('passes the metafield as a render prop to the children render function', () => {
<Metafield metafield={metafield}>{children}</Metafield> const children = jest.fn().mockImplementation(() => {
); return null;
});
const metafield = getParsedMetafield({type: 'file_reference'});
expect(children).toHaveBeenCalledWith({ mountWithShopifyProvider(
...metafield, <Metafield metafield={metafield}>{children}</Metafield>
value: metafield.value, );
expect(children).toHaveBeenCalledWith({
...metafield,
value: metafield.value,
});
}); });
});
it('renders its children', () => { it('renders its children', () => {
const metafield = getParsedMetafield({type: 'file_reference'}); const metafield = getParsedMetafield({type: 'file_reference'});
const component = mountWithShopifyProvider( const component = mountWithShopifyProvider(
<Metafield metafield={metafield}> <Metafield metafield={metafield}>
{({value}) => { {({value}) => {
return <p>The reference is {value}</p>; return <p>The reference is {value}</p>;
}} }}
</Metafield> </Metafield>
); );
expect(component).toContainReactComponent('p', { expect(component).toContainReactComponent('p', {
children: [`The reference is `, metafield.value], children: [`The reference is `, metafield.value],
});
}); });
});
it('allows passthrough props', () => { it('allows passthrough props', () => {
const component = mountWithShopifyProvider( const component = mountWithShopifyProvider(
<Metafield <Metafield
metafield={getParsedMetafield({type: 'file_reference'})} metafield={getParsedMetafield({type: 'file_reference'})}
className="emphasized" className="emphasized"
/> />
); );
expect(component).toContainReactComponent('span', { expect(component).toContainReactComponent('span', {
className: 'emphasized', className: 'emphasized',
});
}); });
}); });
}); });

View file

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

View file

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

View file

@ -2187,20 +2187,36 @@ fragment Model3DFragment on Model3d {
`; `;
/** /**
*``` *```
* fragment MetafieldFragment on Metafield { * fragment MetafieldFragment on Metafield {
* id * id
* type * type
* namespace * namespace
* key * key
* value * value
* createdAt * createdAt
* updatedAt * updatedAt
* description * 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 { export const MetafieldFragment: string = `fragment MetafieldFragment on Metafield {
id id
type type
@ -2210,6 +2226,22 @@ export const MetafieldFragment: string = `fragment MetafieldFragment on Metafiel
createdAt createdAt
updatedAt updatedAt
description 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 * createdAt
* updatedAt * updatedAt
* description * description
* reference @include(if: $includeReferenceMetafieldDetails) {
* __typename
* ... on MediaImage {
* mediaContentType
* image {
* ...ImageFragment
* }
* }
* }
* } * }
* *
* fragment VariantFragment on ProductVariant { * 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 { * fragment SellingPlanFragment on SellingPlan {
* id * id
@ -2633,6 +2682,15 @@ fragment MetafieldFragment on Metafield {
createdAt createdAt
updatedAt updatedAt
description description
reference @include(if: $includeReferenceMetafieldDetails) {
__typename
... on MediaImage {
mediaContentType
image {
...ImageFragment
}
}
}
} }
fragment VariantFragment on ProductVariant { 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 { fragment SellingPlanFragment on SellingPlan {
id id

View file

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

View file

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

View file

@ -25,6 +25,9 @@ export function getPreviewImage(image: Partial<Image> = {}) {
url: image.url ?? faker.random.image(), url: image.url ?? faker.random.image(),
width: image.width ?? faker.datatype.number(), width: image.width ?? faker.datatype.number(),
height: image.height ?? 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( export function getRawMetafield(
metafield: Partial<Metafield> & {type?: MetafieldType} = {} metafield: Partial<Metafield> & {type?: MetafieldType} = {}
): Omit<Metafield, 'parentResource' | 'valueType'> { ): RawMetafield {
const type: MetafieldType = const type: MetafieldType =
metafield.type == null metafield.type == null
? faker.random.arrayElement(METAFIELDS) ? faker.random.arrayElement(METAFIELDS)
@ -62,6 +62,7 @@ export function getRawMetafield(
type, type,
updatedAt: metafield.updatedAt ?? faker.date.recent(), updatedAt: metafield.updatedAt ?? faker.date.recent(),
value: metafield.value ?? getMetafieldValue(type), value: metafield.value ?? getMetafieldValue(type),
reference: metafield.reference as any,
}; };
} }