Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image With Caption Content Block #995

Draft
wants to merge 6 commits into
base: saga
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion gatsby-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import {
createFundraiserSchemaCustomization,
createFundraisersFromMarkdown,
} from './gatsby/fundraisers/transformers'
import { createBlockPhotoResolvers } from './gatsby/generic-pages/resolvers'
import { sourceNeedsAssessments } from './gatsby/needs-assessment/sourceNeedsAssessmentData'
import {
createPhotoResolvers,
createPhotoSchemaCustomization,
} from './gatsby/photos/photos'
import transformers from './gatsby/transform-nodes'

/*
Customize the GraqphQL Schema
================================================================================
Expand Down Expand Up @@ -59,6 +59,7 @@ export const createResolvers: GatsbyNode['createResolvers'] = (args) => {
resolvers.resolveSubregionFields(args)
resolvers.resolveTeamMemberFields(args)
createPhotoResolvers(args)
createBlockPhotoResolvers(args)
}

/*
Expand Down
56 changes: 50 additions & 6 deletions gatsby/generic-pages/content-blocks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CreateNodeArgs } from 'gatsby'
import { getArrayProperty } from '../utils/untypedAccess/getArrayProperty'
import { getDateProperty } from '../utils/untypedAccess/getDateProperty'
import { getStringProperty } from '../utils/untypedAccess/getStringProperty'

import { BlockNodeInput } from '../../src/types/generic-page.d'
Expand All @@ -8,6 +9,7 @@ export const schema = `
union DABlockTypes =
DABlockTitle |
DABlockText |
DABlockImage |
DABlockYoutube |
DABlockTimeline

Expand All @@ -19,6 +21,21 @@ export const schema = `
text: String!
}

type DABlockImage implements Node {
relativePath: String!
alt: String!
image: ImageSharp!

caption: String
attribution: String!
dateUploaded: Date
date: Date
tags: [String!]!

alignmentPhoto: String
alignmentCaption: String
}

type DABlockYoutube implements Node {
title: String
embedUrl: String!
Expand Down Expand Up @@ -75,18 +92,15 @@ export const deriveBlockNode: DeriveBlockFn = (
case 'block-text':
return deriveTextBlockNode(block, parentId, createNodeArgs)

case 'block-image-with-caption':
return deriveImageBlockNode(block, parentId, createNodeArgs)

case 'block-youtube-embed':
return deriveYoutubeBlockNode(block, parentId, createNodeArgs)

case 'block-timeline':
return deriveTimelineBlockNode(block, parentId, createNodeArgs)

case 'block-image-with-caption':
reporter.warn(
`Dropping content block type="${blockType}", since it is not implemented yet.`,
)
return null

default:
reporter.warn(`Dropping unknown content block: type="${blockType}"`)
return null
Expand Down Expand Up @@ -135,6 +149,36 @@ export const deriveTextBlockNode: DeriveBlockFn = (
}
}

export const deriveImageBlockNode: DeriveBlockFn = (
block,
parentId,
{ createNodeId, createContentDigest },
) => {
const alt = getStringProperty(block, 'altText')
return {
relativePath: getStringProperty(block, 'asset'),
alt: alt,

caption: getStringProperty(block, 'caption'),
attribution: getStringProperty(block, 'attribution'),
dateUploaded: getDateProperty(block, 'dateUploaded'),
date: block?.date ? getDateProperty(block, 'date') : undefined,
tags: getArrayProperty(block, 'tags'),

alignmentPhoto: getStringProperty(block, 'alignmentPhoto'),
alignmentCaption: getStringProperty(block, 'alignmentCaption'),

// Gatsby Fields
id: createNodeId(`DABlockImage - ${alt}`),
parent: parentId,
children: [],
internal: {
type: 'DABlockImage',
contentDigest: createContentDigest(JSON.stringify(block)),
},
}
}

export const deriveYoutubeBlockNode: DeriveBlockFn = (
block,
parentId,
Expand Down
9 changes: 8 additions & 1 deletion gatsby/generic-pages/generic-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ describe('Processes Page Data', () => {
it('processes each type of page, section, and content block correctly', () => {
// build our input
const sectionData = factory.getSectionGridData({
contentBlocks: [factory.getBlockTitleData(), factory.getBlockTextData()],
contentBlocks: [
factory.getBlockTitleData(),
factory.getBlockTextData(),
factory.getBlockImageData(),
factory.getBlockYoutubeData(),
],
})

const pageData = factory.getPageData({
Expand All @@ -30,6 +35,8 @@ describe('Processes Page Data', () => {
blocks: [
factory.getBlockTitleNodeInput(),
factory.getBlockTextNodeInput(),
factory.getBlockImageNodeInput(),
factory.getBlockYoutubeNodeInput(),
],
})

Expand Down
11 changes: 11 additions & 0 deletions gatsby/generic-pages/resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CreateResolversArgs } from 'gatsby'
import { imageSharpResolver } from '../create-resolvers'

export const createBlockPhotoResolvers = (args: CreateResolversArgs) => {
const { createResolvers, getNode } = args
createResolvers({
DABlockImage: {
image: imageSharpResolver(getNode, 'relativePath'),
},
})
}
36 changes: 36 additions & 0 deletions gatsby/utils/untypedAccess/getDateProperty.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getDateProperty } from './getDateProperty'

describe('getDateProperty()', () => {
it('should return a Date property if present', () => {
expect(getDateProperty({ foo: new Date('2019-01-17') }, 'foo')).toEqual(
new Date('2019-01-17'),
)
expect(getDateProperty({ foo: '2019-01-17' }, 'foo')).toEqual(
new Date('2019-01-17'),
)
})

it('should throw an error if the object is undefined', () =>
expect(() => getDateProperty(undefined, 'foo')).toThrow(
`Received undefined when trying to access property 'foo'!`,
))

it('should throw an error if the property is undefined', () =>
expect(() => getDateProperty({}, 'foo')).toThrow(
`Object '{}' has no property 'foo'!`,
))

it('should throw an error if the property is not a Date or a datestring', () =>
expect(() => getDateProperty({ foo: [] }, 'foo')).toThrow(
`Property 'foo' on object '{\"foo\":[]}' does not match expected type!`,
))

it('should throw an error if the property is not a valid Date or datestring', () => {
expect(() => getDateProperty({ foo: new Date('invalid') }, 'foo')).toThrow(
`Property 'foo' on object '{\"foo\":null}' is an invalid Date!`,
)
expect(() => getDateProperty({ foo: 'invalid' }, 'foo')).toThrow(
`Property 'foo' on object '{\"foo\":\"invalid\"}' is an invalid Date!`,
)
})
})
38 changes: 38 additions & 0 deletions gatsby/utils/untypedAccess/getDateProperty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Helper function for untyped date access.
* Handles dates in string form.
*/
export const getDateProperty = (
o: Record<string, any> | undefined,
property: string,
): Date => {
if (o === undefined) {
throw new Error(
`Received undefined when trying to access property '${property}'!`,
)
}
if (!(property in o)) {
throw new Error(
`Object '${JSON.stringify(o)}' has no property '${property}'!`,
)
}

const v = o[property]
if (!(v instanceof Date) && !(typeof v === 'string')) {
throw new Error(
`Property '${property}' on object '${JSON.stringify(
o,
)}' does not match expected type!`,
)
}

const date = new Date(v)
if (isNaN(date.valueOf())) {
throw new Error(
`Property '${property}' on object '${JSON.stringify(
o,
)}' is an invalid Date!`,
)
}
return date
}
53 changes: 31 additions & 22 deletions src/components/section/ContentBlock.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,43 @@ describe('Blocks', () => {
factory.getBlockTitleNode({ text: 'My Title' }),
factory.getBlockTextNode({ text: 'My text.' }),
factory.getBlockYoutubeNode({ title: 'My video.' }),
factory.getBlockImageNode({ alt: 'My Alt' }),
]
})

it('can render multiple blocks', () => {
const { getByText } = render(<Blocks blocks={blocks} />)
const { getByText, getByTestId } = render(<Blocks blocks={blocks} />)

const title = getByText('My Title')
expect(title).toBeTruthy()
const titleElem = getByText('My Title')
expect(titleElem).toBeTruthy()

const text = getByText('My text.')
expect(text).toBeTruthy()
const textElem = getByText('My text.')
expect(textElem).toBeTruthy()

const youTubeTitle = getByText('My video.')
expect(youTubeTitle).toBeTruthy()
const youTubeElem = getByText('My video.')
expect(youTubeElem).toBeTruthy()

const imageElem = getByTestId('BlockImage')
expect(imageElem).toBeTruthy()
})

it('gracefully drops unimplemented block types', () => {
blocks = blocks.concat([
factory.getBlockTimelineNode(),
factory.getBlockImageNode(),
factory.getBlockCardNode(),
])

const { getByText, queryByText } = render(<Blocks blocks={blocks} />)
const title = getByText('My Title')
expect(title).toBeTruthy()
const titleElem = getByText('My Title')
expect(titleElem).toBeTruthy()

const text = getByText('My text.')
expect(text).toBeTruthy()
const textElem = getByText('My text.')
expect(textElem).toBeTruthy()

const timelineEntry = queryByText('2020')
expect(timelineEntry).toBeFalsy()
const timelineEntryElem = queryByText('2020')
expect(timelineEntryElem).toBeFalsy()

// TODO: add tests asserting image & card are dropped
// TODO: add tests asserting card are dropped
})

it('throws an error when a block has an unknown type', () => {
Expand All @@ -73,28 +76,34 @@ describe('Block', () => {
it('can render a title block', () => {
const block = factory.getBlockTitleNode({ text: 'My Title' })
const { getByText } = render(<Block block={block} />)
const title = getByText('My Title')
expect(title).toBeTruthy()
const titleElem = getByText('My Title')
expect(titleElem).toBeTruthy()
})

it('can render a text block', () => {
const block = factory.getBlockTextNode({ text: 'My text.' })
const { getByText } = render(<Block block={block} />)
const text = getByText('My text.')
expect(text).toBeTruthy()
const textElem = getByText('My text.')
expect(textElem).toBeTruthy()
})

it('can render a youtube block', () => {
const block = factory.getBlockYoutubeNode({ title: 'My video.' })
const { getByText } = render(<Block block={block} />)
const text = getByText('My video.')
expect(text).toBeTruthy()
const youtubeElem = getByText('My video.')
expect(youtubeElem).toBeTruthy()
})

it('can render an image block', () => {
const block = factory.getBlockImageNode({ alt: 'My Alt' })
const { getByTestId } = render(<Block block={block} />)
const imageElem = getByTestId('BlockImage')
expect(imageElem).toBeTruthy()
})

test.todo('can render a timeline block')
test.todo('can render a links list block')
test.todo('can render an updates list block')
test.todo('can render an image block')
test.todo('can render a card block')

it('throws an error when a block has an unknown type', () => {
Expand Down
8 changes: 6 additions & 2 deletions src/components/section/ContentBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { FC } from 'react'
import {
BlockImage as BlockImageType,
BlockLinksList as BlockLinksListType,
BlockNode,
BlockText as BlockTextType,
BlockTitle as BlockTitleType,
BlockUpdatesList as BlockUpdatesListType,
BlockYoutube as BlockYoutubeType,
} from '../../types/generic-page.d'
import { BlockImage } from './blocks/BlockImage'
import { BlockLinksList } from './blocks/BlockLinksList'
import { BlockText } from './blocks/BlockText'
import { BlockTitle } from './blocks/BlockTitle'
Expand Down Expand Up @@ -60,12 +62,14 @@ export const Block: FC<BlockProps> = ({ block, className }) => {
return (
<BlockYouTube block={block as BlockYoutubeType} className={className} />
)
case 'DABlockImage':
return (
<BlockImage block={block as BlockImageType} className={className} />
)

// wishlist
case 'DABlockTimeline':
return null
case 'DABlockImage':
return null
case 'DABlockCard':
return null

Expand Down
12 changes: 12 additions & 0 deletions src/components/section/blocks/BlockImage.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react'
import { factory } from '../../../types/generic-page.test-helpers'
import { BlockImage } from './BlockImage'

describe('BlockImage', () => {
it('renders the image and related content', () => {
const block = factory.getBlockImageNode({ alt: 'My Alt' })
const { getByTestId } = render(<BlockImage block={block} />)
const title = getByTestId('BlockImage')
expect(title).toBeTruthy()
})
})
20 changes: 20 additions & 0 deletions src/components/section/blocks/BlockImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { GatsbyImage } from 'gatsby-plugin-image'
import { FC } from 'react'
import { BlockImage as BlockImageType } from '../../../types/generic-page.d'

type BlockImageProps = {
block: BlockImageType
className?: string | undefined
}

export const BlockImage: FC<BlockImageProps> = ({ block, className }) => {
return (
<div className={`${className}`} data-testId="BlockImage">
<GatsbyImage
className="w-full"
alt={block.alt}
image={block.image.gatsbyImageData}
/>
</div>
)
}
Loading