refactor: stop spreading challenge over the node (#44499)

* refactor: stop spreading challenge over the node

Instead of creating a Gatsby node with id of challenge.id, we create a
single node field that has all the challenge data.

While this makes the GraphQL queries more verbose, it means we're free
to create multiple nodes with the same challenge.id.

* test: update time-line test for new GQL schema

* test: update mocks with new GQL schema
This commit is contained in:
Oliver Eyton-Williams
2021-12-14 19:11:20 +01:00
committed by GitHub
parent 755f27093c
commit 3b560deab6
18 changed files with 593 additions and 440 deletions

View File

@ -20,13 +20,6 @@ const createByIdentityMap = {
exports.onCreateNode = function onCreateNode({ node, actions, getNode }) { exports.onCreateNode = function onCreateNode({ node, actions, getNode }) {
const { createNodeField } = actions; const { createNodeField } = actions;
if (node.internal.type === 'ChallengeNode') {
const { tests = [], block, dashedName, superBlock } = node;
const slug = `/learn/${superBlock}/${block}/${dashedName}`;
createNodeField({ node, name: 'slug', value: slug });
createNodeField({ node, name: 'blockName', value: blockNameify(block) });
createNodeField({ node, name: 'tests', value: tests });
}
if (node.internal.type === 'MarkdownRemark') { if (node.internal.type === 'MarkdownRemark') {
const slug = createFilePath({ node, getNode }); const slug = createFilePath({ node, getNode });
@ -71,37 +64,45 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
graphql(` graphql(`
{ {
allChallengeNode( allChallengeNode(
sort: { fields: [superOrder, order, challengeOrder] } sort: {
fields: [
challenge___superOrder
challenge___order
challenge___challengeOrder
]
}
) { ) {
edges { edges {
node { node {
block challenge {
challengeType block
fields { challengeType
slug fields {
slug
}
id
order
required {
link
src
}
challengeOrder
challengeFiles {
name
ext
contents
head
tail
}
solutions {
contents
ext
}
superBlock
superOrder
template
usesMultifileEditor
} }
id
order
required {
link
src
}
challengeOrder
challengeFiles {
name
ext
contents
head
tail
}
solutions {
contents
ext
}
superBlock
superOrder
template
usesMultifileEditor
} }
} }
} }
@ -137,12 +138,22 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
); );
const blocks = uniq( const blocks = uniq(
result.data.allChallengeNode.edges.map(({ node: { block } }) => block) result.data.allChallengeNode.edges.map(
({
node: {
challenge: { block }
}
}) => block
)
).map(block => blockNameify(block)); ).map(block => blockNameify(block));
const superBlocks = uniq( const superBlocks = uniq(
result.data.allChallengeNode.edges.map( result.data.allChallengeNode.edges.map(
({ node: { superBlock } }) => superBlock ({
node: {
challenge: { superBlock }
}
}) => superBlock
) )
); );
@ -256,6 +267,9 @@ exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions; const { createTypes } = actions;
const typeDefs = ` const typeDefs = `
type ChallengeNode implements Node { type ChallengeNode implements Node {
challenge: Challenge
}
type Challenge {
challengeFiles: [FileContents] challengeFiles: [FileContents]
notes: String notes: String
url: String url: String

View File

@ -1,4 +1,5 @@
const crypto = require('crypto'); const crypto = require('crypto');
const { blockNameify } = require('../../../utils/block-nameify');
function createChallengeNode(challenge, reporter) { function createChallengeNode(challenge, reporter) {
// challengeType 11 is for video challenges (they only have instructions) // challengeType 11 is for video challenges (they only have instructions)
@ -31,6 +32,17 @@ function createChallengeNode(challenge, reporter) {
type: challenge.challengeType === 7 ? 'CertificateNode' : 'ChallengeNode' type: challenge.challengeType === 7 ? 'CertificateNode' : 'ChallengeNode'
}; };
if (internal.type === 'ChallengeNode') {
const { tests = [], block, dashedName, superBlock } = challenge;
const slug = `/learn/${superBlock}/${block}/${dashedName}`;
challenge.fields = {
slug,
blockName: blockNameify(block),
tests
};
}
return JSON.parse( return JSON.parse(
JSON.stringify( JSON.stringify(
Object.assign( Object.assign(
@ -41,7 +53,8 @@ function createChallengeNode(challenge, reporter) {
internal, internal,
sourceInstanceName: 'challenge' sourceInstanceName: 'challenge'
}, },
challenge { challenge },
{ id: crypto.randomUUID() }
) )
) )
); );

View File

@ -1,136 +1,158 @@
const mockChallengeNodes = [ const mockChallengeNodes = [
{ {
fields: { challenge: {
slug: '/super-block-one/block-a/challenge-one', fields: {
blockName: 'Block A' slug: '/super-block-one/block-a/challenge-one',
}, blockName: 'Block A'
id: 'a', },
block: 'block-a', id: 'a',
title: 'Challenge One', block: 'block-a',
isPrivate: false, title: 'Challenge One',
superBlock: 'super-block-one', isPrivate: false,
dashedName: 'challenge-one' superBlock: 'super-block-one',
dashedName: 'challenge-one'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-one/block-a/challenge-two', fields: {
blockName: 'Block A' slug: '/super-block-one/block-a/challenge-two',
}, blockName: 'Block A'
id: 'b', },
block: 'block-a', id: 'b',
title: 'Challenge Two', block: 'block-a',
isPrivate: false, title: 'Challenge Two',
superBlock: 'super-block-one', isPrivate: false,
dashedName: 'challenge-two' superBlock: 'super-block-one',
dashedName: 'challenge-two'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-one/block-b/challenge-one', fields: {
blockName: 'Block B' slug: '/super-block-one/block-b/challenge-one',
}, blockName: 'Block B'
id: 'c', },
block: 'block-b', id: 'c',
title: 'Challenge One', block: 'block-b',
isPrivate: false, title: 'Challenge One',
superBlock: 'super-block-one', isPrivate: false,
dashedName: 'challenge-one' superBlock: 'super-block-one',
dashedName: 'challenge-one'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-one/block-b/challenge-two', fields: {
blockName: 'Block B' slug: '/super-block-one/block-b/challenge-two',
}, blockName: 'Block B'
},
id: 'd', id: 'd',
block: 'block-b', block: 'block-b',
title: 'Challenge Two', title: 'Challenge Two',
isPrivate: false, isPrivate: false,
superBlock: 'super-block-one', superBlock: 'super-block-one',
dashedName: 'challenge-two' dashedName: 'challenge-two'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-one/block-c/challenge-one', fields: {
blockName: 'Block C' slug: '/super-block-one/block-c/challenge-one',
}, blockName: 'Block C'
id: 'e', },
block: 'block-c', id: 'e',
title: 'Challenge One', block: 'block-c',
isPrivate: true, title: 'Challenge One',
superBlock: 'super-block-one', isPrivate: true,
dashedName: 'challenge-one' superBlock: 'super-block-one',
dashedName: 'challenge-one'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-two/block-a/challenge-one', fields: {
blockName: 'Block A' slug: '/super-block-two/block-a/challenge-one',
}, blockName: 'Block A'
id: 'f', },
block: 'block-a', id: 'f',
title: 'Challenge One', block: 'block-a',
isPrivate: false, title: 'Challenge One',
superBlock: 'super-block-two', isPrivate: false,
dashedName: 'challenge-one' superBlock: 'super-block-two',
dashedName: 'challenge-one'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-two/block-a/challenge-two', fields: {
blockName: 'Block A' slug: '/super-block-two/block-a/challenge-two',
}, blockName: 'Block A'
id: 'g', },
block: 'block-a', id: 'g',
title: 'Challenge Two', block: 'block-a',
isPrivate: false, title: 'Challenge Two',
superBlock: 'super-block-two', isPrivate: false,
dashedName: 'challenge-two' superBlock: 'super-block-two',
dashedName: 'challenge-two'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-two/block-b/challenge-one', fields: {
blockName: 'Block B' slug: '/super-block-two/block-b/challenge-one',
}, blockName: 'Block B'
id: 'h', },
block: 'block-b', id: 'h',
title: 'Challenge One', block: 'block-b',
isPrivate: false, title: 'Challenge One',
superBlock: 'super-block-two', isPrivate: false,
dashedName: 'challenge-one' superBlock: 'super-block-two',
dashedName: 'challenge-one'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-two/block-b/challenge-two', fields: {
blockName: 'Block B' slug: '/super-block-two/block-b/challenge-two',
}, blockName: 'Block B'
id: 'i', },
block: 'block-b', id: 'i',
title: 'Challenge Two', block: 'block-b',
isPrivate: false, title: 'Challenge Two',
superBlock: 'super-block-two', isPrivate: false,
dashedName: 'challenge-two' superBlock: 'super-block-two',
dashedName: 'challenge-two'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-three/block-a/challenge-one', fields: {
blockName: 'Block A' slug: '/super-block-three/block-a/challenge-one',
}, blockName: 'Block A'
id: 'j', },
block: 'block-a', id: 'j',
title: 'Challenge One', block: 'block-a',
isPrivate: false, title: 'Challenge One',
superBlock: 'super-block-three', isPrivate: false,
dashedName: 'challenge-one' superBlock: 'super-block-three',
dashedName: 'challenge-one'
}
}, },
{ {
fields: { challenge: {
slug: '/super-block-three/block-c/challenge-two', fields: {
blockName: 'Block C' slug: '/super-block-three/block-c/challenge-two',
}, blockName: 'Block C'
id: 'k', },
block: 'block-c', id: 'k',
title: 'Challenge Two', block: 'block-c',
isPrivate: false, title: 'Challenge Two',
superBlock: 'super-block-three', isPrivate: false,
dashedName: 'challenge-two' superBlock: 'super-block-three',
dashedName: 'challenge-two'
}
} }
]; ];

View File

@ -42,19 +42,19 @@ const linkSpacingStyle = {
function renderLandingMap(nodes: ChallengeNode[]) { function renderLandingMap(nodes: ChallengeNode[]) {
nodes = nodes.filter( nodes = nodes.filter(
node => node.superBlock !== SuperBlocks.CodingInterviewPrep ({ challenge }) => challenge.superBlock !== SuperBlocks.CodingInterviewPrep
); );
return ( return (
<ul data-test-label='certifications'> <ul data-test-label='certifications'>
{nodes.map((node, i) => ( {nodes.map(({ challenge }, i) => (
<li key={i}> <li key={i}>
<Link <Link
className='btn link-btn btn-lg' className='btn link-btn btn-lg'
to={`/learn/${node.superBlock}/`} to={`/learn/${challenge.superBlock}/`}
> >
<div style={linkSpacingStyle}> <div style={linkSpacingStyle}>
{generateIconComponent(node.superBlock, 'map-icon')} {generateIconComponent(challenge.superBlock, 'map-icon')}
{i18next.t(`intro:${node.superBlock}.title`)} {i18next.t(`intro:${challenge.superBlock}.title`)}
</div> </div>
<LinkButton /> <LinkButton />
</Link> </Link>
@ -68,18 +68,20 @@ function renderLearnMap(
nodes: ChallengeNode[], nodes: ChallengeNode[],
currentSuperBlock: MapProps['currentSuperBlock'] currentSuperBlock: MapProps['currentSuperBlock']
) { ) {
nodes = nodes.filter(node => node.superBlock !== currentSuperBlock); nodes = nodes.filter(
({ challenge }) => challenge.superBlock !== currentSuperBlock
);
return curriculumLocale === 'english' ? ( return curriculumLocale === 'english' ? (
<ul data-test-label='learn-curriculum-map'> <ul data-test-label='learn-curriculum-map'>
{nodes.map((node, i) => ( {nodes.map(({ challenge }, i) => (
<li key={i}> <li key={i}>
<Link <Link
className='btn link-btn btn-lg' className='btn link-btn btn-lg'
to={`/learn/${node.superBlock}/`} to={`/learn/${challenge.superBlock}/`}
> >
<div style={linkSpacingStyle}> <div style={linkSpacingStyle}>
{generateIconComponent(node.superBlock, 'map-icon')} {generateIconComponent(challenge.superBlock, 'map-icon')}
{createSuperBlockTitle(node.superBlock)} {createSuperBlockTitle(challenge.superBlock)}
</div> </div>
</Link> </Link>
</li> </li>
@ -88,16 +90,18 @@ function renderLearnMap(
) : ( ) : (
<ul data-test-label='learn-curriculum-map'> <ul data-test-label='learn-curriculum-map'>
{nodes {nodes
.filter(node => isAuditedCert(curriculumLocale, node.superBlock)) .filter(({ challenge }) =>
.map((node, i) => ( isAuditedCert(curriculumLocale, challenge.superBlock)
)
.map(({ challenge }, i) => (
<li key={i}> <li key={i}>
<Link <Link
className='btn link-btn btn-lg' className='btn link-btn btn-lg'
to={`/learn/${node.superBlock}/`} to={`/learn/${challenge.superBlock}/`}
> >
<div style={linkSpacingStyle}> <div style={linkSpacingStyle}>
{generateIconComponent(node.superBlock, 'map-icon')} {generateIconComponent(challenge.superBlock, 'map-icon')}
{createSuperBlockTitle(node.superBlock)} {createSuperBlockTitle(challenge.superBlock)}
</div> </div>
</Link> </Link>
</li> </li>
@ -115,16 +119,19 @@ function renderLearnMap(
<Spacer /> <Spacer />
</div> </div>
{nodes {nodes
.filter(node => !isAuditedCert(curriculumLocale, node.superBlock)) .filter(
.map((node, i) => ( ({ challenge }) =>
!isAuditedCert(curriculumLocale, challenge.superBlock)
)
.map(({ challenge }, i) => (
<li key={i}> <li key={i}>
<Link <Link
className='btn link-btn btn-lg' className='btn link-btn btn-lg'
to={`/learn/${node.superBlock}/`} to={`/learn/${challenge.superBlock}/`}
> >
<div style={linkSpacingStyle}> <div style={linkSpacingStyle}>
{generateIconComponent(node.superBlock, 'map-icon')} {generateIconComponent(challenge.superBlock, 'map-icon')}
{createSuperBlockTitle(node.superBlock)} {createSuperBlockTitle(challenge.superBlock)}
</div> </div>
</Link> </Link>
</li> </li>
@ -145,12 +152,14 @@ export function Map({
const data: MapData = useStaticQuery(graphql` const data: MapData = useStaticQuery(graphql`
query SuperBlockNodes { query SuperBlockNodes {
allChallengeNode( allChallengeNode(
sort: { fields: [superOrder] } sort: { fields: [challenge___superOrder] }
filter: { order: { eq: 0 }, challengeOrder: { eq: 0 } } filter: { challenge: { order: { eq: 0 }, challengeOrder: { eq: 0 } } }
) { ) {
nodes { nodes {
superBlock challenge {
dashedName superBlock
dashedName
}
} }
} }
} }

View File

@ -269,11 +269,13 @@ function useIdToNameMap(): Map<string, string> {
allChallengeNode { allChallengeNode {
edges { edges {
node { node {
fields { challenge {
slug fields {
slug
}
id
title
} }
id
title
} }
} }
} }
@ -289,12 +291,14 @@ function useIdToNameMap(): Map<string, string> {
edges.forEach( edges.forEach(
({ ({
node: { node: {
// @ts-expect-error Graphql needs typing challenge: {
id, // @ts-expect-error Graphql needs typing
// @ts-expect-error Graphql needs typing id,
title, // @ts-expect-error Graphql needs typing
// @ts-expect-error Graphql needs typing title,
fields: { slug } // @ts-expect-error Graphql needs typing
fields: { slug }
}
} }
}) => { }) => {
idToNameMap.set(id, { challengeTitle: title, challengePath: slug }); idToNameMap.set(id, { challengeTitle: title, challengePath: slug });

View File

@ -12,29 +12,35 @@ beforeEach(() => {
edges: [ edges: [
{ {
node: { node: {
fields: { challenge: {
slug: '' fields: {
}, slug: ''
id: '5e46f802ac417301a38fb92b', },
title: 'Page View Time Series Visualizer' id: '5e46f802ac417301a38fb92b',
title: 'Page View Time Series Visualizer'
}
} }
}, },
{ {
node: { node: {
fields: { challenge: {
slug: '' fields: {
}, slug: ''
id: '5e4f5c4b570f7e3a4949899f', },
title: 'Sea Level Predictor' id: '5e4f5c4b570f7e3a4949899f',
title: 'Sea Level Predictor'
}
} }
}, },
{ {
node: { node: {
fields: { challenge: {
slug: '' fields: {
}, slug: ''
id: '5e46f7f8ac417301a38fb92a', },
title: 'Medical Data Visualizer' id: '5e46f7f8ac417301a38fb92a',
title: 'Medical Data Visualizer'
}
} }
} }
] ]

View File

@ -53,7 +53,9 @@ interface LearnPageProps {
user: User; user: User;
data: { data: {
challengeNode: { challengeNode: {
fields: Slug; challenge: {
fields: Slug;
};
}; };
}; };
executeGA: (payload: Record<string, unknown>) => void; executeGA: (payload: Record<string, unknown>) => void;
@ -70,7 +72,9 @@ function LearnPage({
executeGA, executeGA,
data: { data: {
challengeNode: { challengeNode: {
fields: { slug } challenge: {
fields: { slug }
}
} }
} }
}: LearnPageProps) { }: LearnPageProps) {
@ -117,9 +121,11 @@ export default connect(mapStateToProps, mapDispatchToProps)(LearnPage);
export const query = graphql` export const query = graphql`
query FirstChallenge { query FirstChallenge {
challengeNode(order: { eq: 0 }, challengeOrder: { eq: 0 }) { challengeNode(challenge: { order: { eq: 0 }, challengeOrder: { eq: 0 } }) {
fields { challenge {
slug fields {
slug
}
} }
} }
} }

View File

@ -128,55 +128,57 @@ export interface VideoLocaleIds {
} }
export type ChallengeNode = { export type ChallengeNode = {
block: string; challenge: {
challengeOrder: number; block: string;
challengeType: number; challengeOrder: number;
dashedName: string; challengeType: number;
description: string; dashedName: string;
challengeFiles: ChallengeFiles;
fields: Fields;
forumTopicId: number;
guideUrl: string;
head: string[];
helpCategory: string;
id: string;
instructions: string;
isComingSoon: boolean;
internal?: {
content: string;
contentDigest: string;
description: string; description: string;
fieldOwners: string[]; challengeFiles: ChallengeFiles;
ignoreType: boolean | null; fields: Fields;
mediaType: string; forumTopicId: number;
owner: string; guideUrl: string;
type: string; head: string[];
helpCategory: string;
id: string;
instructions: string;
isComingSoon: boolean;
internal?: {
content: string;
contentDigest: string;
description: string;
fieldOwners: string[];
ignoreType: boolean | null;
mediaType: string;
owner: string;
type: string;
};
notes: string;
removeComments: boolean;
isLocked: boolean;
isPrivate: boolean;
order: number;
question: Question;
required: Required[];
solutions: {
[T in FileKey]: FileKeyChallenge;
};
sourceInstanceName: string;
superOrder: number;
superBlock: SuperBlocks;
tail: string[];
template: string;
tests: Test[];
time: string;
title: string;
translationPending: boolean;
url: string;
usesMultifileEditor: boolean;
videoId: string;
videoLocaleIds?: VideoLocaleIds;
bilibiliIds?: BilibiliIds;
videoUrl: string;
}; };
notes: string;
removeComments: boolean;
isLocked: boolean;
isPrivate: boolean;
order: number;
question: Question;
required: Required[];
solutions: {
[T in FileKey]: FileKeyChallenge;
};
sourceInstanceName: string;
superOrder: number;
superBlock: SuperBlocks;
tail: string[];
template: string;
tests: Test[];
time: string;
title: string;
translationPending: boolean;
url: string;
usesMultifileEditor: boolean;
videoId: string;
videoLocaleIds?: VideoLocaleIds;
bilibiliIds?: BilibiliIds;
videoUrl: string;
}; };
export type AllChallengeNode = { export type AllChallengeNode = {

View File

@ -212,7 +212,9 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
componentDidMount() { componentDidMount() {
const { const {
data: { data: {
challengeNode: { title } challengeNode: {
challenge: { title }
}
} }
} = this.props; } = this.props;
this.initializeComponent(title); this.initializeComponent(title);
@ -222,16 +224,20 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
const { const {
data: { data: {
challengeNode: { challengeNode: {
title: prevTitle, challenge: {
fields: { tests: prevTests } title: prevTitle,
fields: { tests: prevTests }
}
} }
} }
} = prevProps; } = prevProps;
const { const {
data: { data: {
challengeNode: { challengeNode: {
title: currentTitle, challenge: {
fields: { tests: currTests } title: currentTitle,
fields: { tests: currTests }
}
} }
} }
} = this.props; } = this.props;
@ -250,11 +256,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
openModal, openModal,
data: { data: {
challengeNode: { challengeNode: {
challengeFiles, challenge: {
fields: { tests }, challengeFiles,
challengeType, fields: { tests },
removeComments, challengeType,
helpCategory removeComments,
helpCategory
}
} }
}, },
pageContext: { pageContext: {
@ -282,7 +290,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
cancelTests(); cancelTests();
} }
getChallenge = () => this.props.data.challengeNode; getChallenge = () => this.props.data.challengeNode.challenge;
getBlockNameTitle(t: TFunction) { getBlockNameTitle(t: TFunction) {
const { block, superBlock, title } = this.getChallenge(); const { block, superBlock, title } = this.getChallenge();
@ -337,8 +345,10 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
challengeFiles, challengeFiles,
data: { data: {
challengeNode: { challengeNode: {
fields: { tests }, challenge: {
usesMultifileEditor fields: { tests },
usesMultifileEditor
}
} }
} }
} = this.props; } = this.props;
@ -489,40 +499,42 @@ export default connect(
export const query = graphql` export const query = graphql`
query ClassicChallenge($slug: String!) { query ClassicChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
block challenge {
title block
description title
instructions description
notes instructions
removeComments notes
challengeType removeComments
helpCategory challengeType
videoUrl helpCategory
superBlock videoUrl
translationPending superBlock
forumTopicId translationPending
fields { forumTopicId
blockName fields {
slug blockName
tests { slug
text tests {
testString text
testString
}
}
required {
link
src
}
usesMultifileEditor
challengeFiles {
fileKey
ext
name
contents
head
tail
editableRegionBoundaries
} }
}
required {
link
src
}
usesMultifileEditor
challengeFiles {
fileKey
ext
name
contents
head
tail
editableRegionBoundaries
} }
} }
} }

View File

@ -49,7 +49,9 @@ class ShowCodeAlly extends Component<ShowCodeAllyProps> {
const { const {
updateChallengeMeta, updateChallengeMeta,
data: { data: {
challengeNode: { challengeType, title } challengeNode: {
challenge: { challengeType, title }
}
}, },
pageContext: { challengeMeta } pageContext: { challengeMeta }
} = this.props; } = this.props;
@ -60,9 +62,11 @@ class ShowCodeAlly extends Component<ShowCodeAllyProps> {
const { const {
data: { data: {
challengeNode: { challengeNode: {
title, challenge: {
fields: { blockName }, title,
url fields: { blockName },
url
}
} }
}, },
webhookToken = null webhookToken = null
@ -94,12 +98,14 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShowCodeAlly);
// GraphQL // GraphQL
export const query = graphql` export const query = graphql`
query CodeAllyChallenge($slug: String!) { query CodeAllyChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
title challenge {
challengeType title
url challengeType
fields { url
blockName fields {
blockName
}
} }
} }
} }

View File

@ -280,13 +280,23 @@ const useCurrentBlockIds = (blockName: string) => {
allChallengeNode: { edges } allChallengeNode: { edges }
}: { allChallengeNode: AllChallengeNode } = useStaticQuery(graphql` }: { allChallengeNode: AllChallengeNode } = useStaticQuery(graphql`
query getCurrentBlockNodes { query getCurrentBlockNodes {
allChallengeNode(sort: { fields: [superOrder, order, challengeOrder] }) { allChallengeNode(
sort: {
fields: [
challenge___superOrder
challenge___order
challenge___challengeOrder
]
}
) {
edges { edges {
node { node {
fields { challenge {
blockName fields {
blockName
}
id
} }
id
} }
} }
} }
@ -294,9 +304,9 @@ const useCurrentBlockIds = (blockName: string) => {
`); `);
const currentBlockIds = edges const currentBlockIds = edges
.filter(edge => edge.node.fields.blockName === blockName) .filter(edge => edge.node.challenge.fields.blockName === blockName)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return
.map(edge => edge.node.id); .map(edge => edge.node.challenge.id);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return currentBlockIds; return currentBlockIds;
}; };

View File

@ -123,16 +123,20 @@ class BackEnd extends Component<BackEndProps> {
const { const {
data: { data: {
challengeNode: { challengeNode: {
title: prevTitle, challenge: {
fields: { tests: prevTests } title: prevTitle,
fields: { tests: prevTests }
}
} }
} }
} = prevProps; } = prevProps;
const { const {
data: { data: {
challengeNode: { challengeNode: {
title: currentTitle, challenge: {
fields: { tests: currTests } title: currentTitle,
fields: { tests: currTests }
}
} }
} }
} = this.props; } = this.props;
@ -149,10 +153,12 @@ class BackEnd extends Component<BackEndProps> {
updateChallengeMeta, updateChallengeMeta,
data: { data: {
challengeNode: { challengeNode: {
fields: { tests }, challenge: {
title, fields: { tests },
challengeType, title,
helpCategory challengeType,
helpCategory
}
} }
}, },
pageContext: { challengeMeta } pageContext: { challengeMeta }
@ -182,15 +188,17 @@ class BackEnd extends Component<BackEndProps> {
const { const {
data: { data: {
challengeNode: { challengeNode: {
fields: { blockName }, challenge: {
challengeType, fields: { blockName },
forumTopicId, challengeType,
title, forumTopicId,
description, title,
instructions, description,
translationPending, instructions,
superBlock, translationPending,
block superBlock,
block
}
} }
}, },
isChallengeCompleted, isChallengeCompleted,
@ -278,22 +286,24 @@ export default connect(
export const query = graphql` export const query = graphql`
query BackendChallenge($slug: String!) { query BackendChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
forumTopicId challenge {
title forumTopicId
description title
instructions description
challengeType instructions
helpCategory challengeType
superBlock helpCategory
block superBlock
translationPending block
fields { translationPending
blockName fields {
slug blockName
tests { slug
text tests {
testString text
testString
}
} }
} }
} }

View File

@ -76,7 +76,9 @@ class Project extends Component<ProjectProps> {
const { const {
challengeMounted, challengeMounted,
data: { data: {
challengeNode: { title, challengeType, helpCategory } challengeNode: {
challenge: { title, challengeType, helpCategory }
}
}, },
pageContext: { challengeMeta }, pageContext: { challengeMeta },
updateChallengeMeta updateChallengeMeta
@ -94,13 +96,17 @@ class Project extends Component<ProjectProps> {
componentDidUpdate(prevProps: ProjectProps): void { componentDidUpdate(prevProps: ProjectProps): void {
const { const {
data: { data: {
challengeNode: { title: prevTitle } challengeNode: {
challenge: { title: prevTitle }
}
} }
} = prevProps; } = prevProps;
const { const {
challengeMounted, challengeMounted,
data: { data: {
challengeNode: { title: currentTitle, challengeType, helpCategory } challengeNode: {
challenge: { title: currentTitle, challengeType, helpCategory }
}
}, },
pageContext: { challengeMeta }, pageContext: { challengeMeta },
updateChallengeMeta updateChallengeMeta
@ -130,15 +136,17 @@ class Project extends Component<ProjectProps> {
const { const {
data: { data: {
challengeNode: { challengeNode: {
challengeType, challenge: {
fields: { blockName }, challengeType,
forumTopicId, fields: { blockName },
title, forumTopicId,
description, title,
instructions, description,
superBlock, instructions,
block, superBlock,
translationPending block,
translationPending
}
} }
}, },
isChallengeCompleted, isChallengeCompleted,
@ -215,19 +223,21 @@ export default connect(
export const query = graphql` export const query = graphql`
query ProjectChallenge($slug: String!) { query ProjectChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
forumTopicId challenge {
title forumTopicId
description title
instructions description
challengeType instructions
helpCategory challengeType
superBlock helpCategory
block superBlock
translationPending block
fields { translationPending
blockName fields {
slug blockName
slug
}
} }
} }
} }

View File

@ -97,7 +97,9 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
const { const {
challengeMounted, challengeMounted,
data: { data: {
challengeNode: { title, challengeType, helpCategory } challengeNode: {
challenge: { title, challengeType, helpCategory }
}
}, },
pageContext: { challengeMeta }, pageContext: { challengeMeta },
updateChallengeMeta updateChallengeMeta
@ -115,13 +117,17 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
componentDidUpdate(prevProps: ShowVideoProps): void { componentDidUpdate(prevProps: ShowVideoProps): void {
const { const {
data: { data: {
challengeNode: { title: prevTitle } challengeNode: {
challenge: { title: prevTitle }
}
} }
} = prevProps; } = prevProps;
const { const {
challengeMounted, challengeMounted,
data: { data: {
challengeNode: { title: currentTitle, challengeType, helpCategory } challengeNode: {
challenge: { title: currentTitle, challengeType, helpCategory }
}
}, },
pageContext: { challengeMeta }, pageContext: { challengeMeta },
updateChallengeMeta updateChallengeMeta
@ -169,16 +175,18 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
const { const {
data: { data: {
challengeNode: { challengeNode: {
fields: { blockName }, challenge: {
title, fields: { blockName },
description, title,
superBlock, description,
block, superBlock,
translationPending, block,
videoId, translationPending,
videoLocaleIds, videoId,
bilibiliIds, videoLocaleIds,
question: { text, answers, solution } bilibiliIds,
question: { text, answers, solution }
}
} }
}, },
openCompletionModal, openCompletionModal,
@ -313,34 +321,36 @@ export default connect(
export const query = graphql` export const query = graphql`
query VideoChallenge($slug: String!) { query VideoChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
videoId challenge {
videoLocaleIds { videoId
espanol videoLocaleIds {
italian espanol
portuguese italian
portuguese
}
bilibiliIds {
aid
bvid
cid
}
title
description
challengeType
helpCategory
superBlock
block
fields {
blockName
slug
}
question {
text
answers
solution
}
translationPending
} }
bilibiliIds {
aid
bvid
cid
}
title
description
challengeType
helpCategory
superBlock
block
fields {
blockName
slug
}
question {
text
answers
solution
}
translationPending
} }
} }
`; `;

View File

@ -101,7 +101,7 @@ export class Block extends Component<BlockProps> {
} = this.props; } = this.props;
let completedCount = 0; let completedCount = 0;
const challengesWithCompleted = challenges.map(challenge => { const challengesWithCompleted = challenges.map(({ challenge }) => {
const { id } = challenge; const { id } = challenge;
const isCompleted = completedChallengeIds.some( const isCompleted = completedChallengeIds.some(
(completedChallengeId: string) => completedChallengeId === id (completedChallengeId: string) => completedChallengeId === id
@ -112,7 +112,7 @@ export class Block extends Component<BlockProps> {
return { ...challenge, isCompleted }; return { ...challenge, isCompleted };
}); });
const isProjectBlock = challenges.some(challenge => { const isProjectBlock = challenges.some(({ challenge }) => {
const isJsProject = const isJsProject =
challenge.order === 10 && challenge.challengeType === 5; challenge.order === 10 && challenge.challengeType === 5;

View File

@ -21,7 +21,7 @@ function renderMenuItems({
edges?: Array<{ node: ChallengeNode }>; edges?: Array<{ node: ChallengeNode }>;
}) { }) {
return edges return edges
.map(({ node }) => node) .map(({ node: { challenge } }) => challenge)
.map(({ title, fields: { slug } }) => ( .map(({ title, fields: { slug } }) => (
<Link key={'intro-' + slug} to={slug}> <Link key={'intro-' + slug} to={slug}>
<ListGroupItem>{title}</ListGroupItem> <ListGroupItem>{title}</ListGroupItem>
@ -42,7 +42,8 @@ function IntroductionPage({
html, html,
frontmatter: { block } frontmatter: { block }
} = markdownRemark; } = markdownRemark;
const firstLesson = allChallengeNode && allChallengeNode.edges[0].node; const firstLesson =
allChallengeNode && allChallengeNode.edges[0].node.challenge;
const firstLessonPath = firstLesson const firstLessonPath = firstLesson
? firstLesson.fields.slug ? firstLesson.fields.slug
: '/strange-place'; : '/strange-place';
@ -97,15 +98,23 @@ export const query = graphql`
html html
} }
allChallengeNode( allChallengeNode(
filter: { block: { eq: $block } } filter: { challenge: { block: { eq: $block } } }
sort: { fields: [superOrder, order, challengeOrder] } sort: {
fields: [
challenge___superOrder
challenge___order
challenge___challengeOrder
]
}
) { ) {
edges { edges {
node { node {
fields { challenge {
slug fields {
slug
}
title
} }
title
} }
} }
} }

View File

@ -141,15 +141,15 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
if (isSignedIn) { if (isSignedIn) {
// see if currentChallenge is in this superBlock // see if currentChallenge is in this superBlock
const currentChallengeEdge = edges.find( const currentChallengeEdge = edges.find(
edge => edge.node.id === currentChallengeId edge => edge.node.challenge.id === currentChallengeId
); );
return currentChallengeEdge return currentChallengeEdge
? currentChallengeEdge.node.block ? currentChallengeEdge.node.challenge.block
: edge.node.block; : edge.node.challenge.block;
} }
return edge.node.block; return edge.node.challenge.block;
}; };
const initializeExpandedState = () => { const initializeExpandedState = () => {
@ -173,7 +173,9 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
} = props; } = props;
const nodesForSuperBlock = edges.map(({ node }) => node); const nodesForSuperBlock = edges.map(({ node }) => node);
const blockDashedNames = uniq(nodesForSuperBlock.map(({ block }) => block)); const blockDashedNames = uniq(
nodesForSuperBlock.map(({ challenge: { block } }) => block)
);
const i18nSuperBlock = t(`intro:${superBlock}.title`); const i18nSuperBlock = t(`intro:${superBlock}.title`);
const i18nTitle = const i18nTitle =
superBlock === SuperBlocks.CodingInterviewPrep superBlock === SuperBlocks.CodingInterviewPrep
@ -206,7 +208,7 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
<Block <Block
blockDashedName={blockDashedName} blockDashedName={blockDashedName}
challenges={nodesForSuperBlock.filter( challenges={nodesForSuperBlock.filter(
node => node.block === blockDashedName node => node.challenge.block === blockDashedName
)} )}
superBlock={superBlock} superBlock={superBlock}
/> />
@ -263,22 +265,30 @@ export const query = graphql`
} }
} }
allChallengeNode( allChallengeNode(
sort: { fields: [superOrder, order, challengeOrder] } sort: {
filter: { superBlock: { eq: $superBlock } } fields: [
challenge___superOrder
challenge___order
challenge___challengeOrder
]
}
filter: { challenge: { superBlock: { eq: $superBlock } } }
) { ) {
edges { edges {
node { node {
fields { challenge {
slug fields {
blockName slug
blockName
}
id
block
challengeType
title
order
superBlock
dashedName
} }
id
block
challengeType
title
order
superBlock
dashedName
} }
} }
} }

View File

@ -45,12 +45,12 @@ const views = {
function getNextChallengePath(_node, index, nodeArray) { function getNextChallengePath(_node, index, nodeArray) {
const next = nodeArray[index + 1]; const next = nodeArray[index + 1];
return next ? next.node.fields.slug : '/learn'; return next ? next.node.challenge.fields.slug : '/learn';
} }
function getPrevChallengePath(_node, index, nodeArray) { function getPrevChallengePath(_node, index, nodeArray) {
const prev = nodeArray[index - 1]; const prev = nodeArray[index - 1];
return prev ? prev.node.fields.slug : '/learn'; return prev ? prev.node.challenge.fields.slug : '/learn';
} }
function getTemplateComponent(challengeType) { function getTemplateComponent(challengeType) {
@ -58,7 +58,7 @@ function getTemplateComponent(challengeType) {
} }
exports.createChallengePages = function (createPage) { exports.createChallengePages = function (createPage) {
return function ({ node: challenge }, index, allChallengeEdges) { return function ({ node: { challenge } }, index, allChallengeEdges) {
const { const {
superBlock, superBlock,
block, block,
@ -103,8 +103,8 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) {
const { block, challengeOrder, usesMultifileEditor } = challenge; const { block, challengeOrder, usesMultifileEditor } = challenge;
const challengesInBlock = allChallengeEdges const challengesInBlock = allChallengeEdges
.filter(({ node }) => node.block === block) .filter(({ node: { challenge } }) => challenge.block === block)
.map(({ node }) => node); .map(({ node: { challenge } }) => challenge);
const lastChallenge = challengesInBlock[challengesInBlock.length - 1]; const lastChallenge = challengesInBlock[challengesInBlock.length - 1];
const solutionToLastChallenge = sortChallengeFiles( const solutionToLastChallenge = sortChallengeFiles(
lastChallenge.solutions[0] ?? [] lastChallenge.solutions[0] ?? []