mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(client): source super block structure in graphql and store in redux (#62613)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -13,7 +13,15 @@ import {
|
||||
} from '../../templates/Challenges/redux/selectors';
|
||||
import { liveCerts } from '../../../config/cert-and-project-map';
|
||||
import { updateAllChallengesInfo } from '../../redux/actions';
|
||||
import { CertificateNode, ChallengeNode } from '../../redux/prop-types';
|
||||
import type {
|
||||
CertificateNode,
|
||||
ChallengeNode,
|
||||
SuperBlockStructure
|
||||
} from '../../redux/prop-types';
|
||||
import {
|
||||
updateSuperBlockStructures,
|
||||
superBlockStructuresSelector
|
||||
} from '../../templates/Introduction/redux';
|
||||
import { getIsDailyCodingChallenge } from '../../../../shared-dist/config/challenge-types';
|
||||
import {
|
||||
isValidDateString,
|
||||
@@ -26,6 +34,7 @@ const mapStateToProps = createSelector(
|
||||
challengeMetaSelector,
|
||||
completedChallengesInBlockSelector,
|
||||
completedPercentageSelector,
|
||||
superBlockStructuresSelector,
|
||||
(
|
||||
currentBlockIds: string[],
|
||||
{
|
||||
@@ -40,7 +49,8 @@ const mapStateToProps = createSelector(
|
||||
superBlock: string;
|
||||
},
|
||||
completedChallengesInBlock: number,
|
||||
completedPercent: number
|
||||
completedPercent: number,
|
||||
superBlockStructures: Record<string, SuperBlockStructure>
|
||||
) => ({
|
||||
currentBlockIds,
|
||||
challengeType,
|
||||
@@ -48,11 +58,15 @@ const mapStateToProps = createSelector(
|
||||
block,
|
||||
superBlock,
|
||||
completedChallengesInBlock,
|
||||
completedPercent
|
||||
completedPercent,
|
||||
superBlockStructures
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = { updateAllChallengesInfo };
|
||||
const mapDispatchToProps = {
|
||||
updateAllChallengesInfo,
|
||||
updateSuperBlockStructures
|
||||
};
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
@@ -68,7 +82,9 @@ function Progress({
|
||||
completedChallengesInBlock,
|
||||
completedPercent,
|
||||
t,
|
||||
updateAllChallengesInfo
|
||||
updateAllChallengesInfo,
|
||||
updateSuperBlockStructures,
|
||||
superBlockStructures: superBlockStructuresFromStore
|
||||
}: ProgressProps): JSX.Element {
|
||||
let blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
|
||||
// Always false for legacy full stack, since it has no projects.
|
||||
@@ -86,10 +102,31 @@ function Progress({
|
||||
}
|
||||
}
|
||||
|
||||
const { challengeNodes, certificateNodes } = useGetAllBlockIds();
|
||||
const { challengeNodes, certificateNodes, superBlockStructureNodes } =
|
||||
useGetAllChallengeData();
|
||||
|
||||
useEffect(() => {
|
||||
updateAllChallengesInfo({ challengeNodes, certificateNodes });
|
||||
}, [challengeNodes, certificateNodes, updateAllChallengesInfo]);
|
||||
|
||||
const structuresMap: Record<string, SuperBlockStructure> = {};
|
||||
|
||||
// The super block structures are pretty static, so we only want to
|
||||
// update them if we don't already have them in the store.
|
||||
if (Object.keys(superBlockStructuresFromStore).length === 0) {
|
||||
superBlockStructureNodes.forEach((node: SuperBlockStructure) => {
|
||||
structuresMap[node.superBlock] = node;
|
||||
});
|
||||
|
||||
updateSuperBlockStructures(structuresMap);
|
||||
}
|
||||
}, [
|
||||
challengeNodes,
|
||||
certificateNodes,
|
||||
superBlockStructureNodes,
|
||||
updateAllChallengesInfo,
|
||||
updateSuperBlockStructures,
|
||||
superBlockStructuresFromStore
|
||||
]);
|
||||
|
||||
const totalChallengesInBlock = currentBlockIds?.length ?? 0;
|
||||
const meta =
|
||||
@@ -119,13 +156,15 @@ function Progress({
|
||||
// and in completion-modal). Then we don't have to pass the data into redux.
|
||||
// This would mean that we have to memoize any complex calculations in the hook.
|
||||
// Otherwise, this will undo all the recent performance improvements.
|
||||
const useGetAllBlockIds = () => {
|
||||
const useGetAllChallengeData = () => {
|
||||
const {
|
||||
allChallengeNode: { nodes: challengeNodes },
|
||||
allCertificateNode: { nodes: certificateNodes }
|
||||
allCertificateNode: { nodes: certificateNodes },
|
||||
allSuperBlockStructure: { nodes: superBlockStructureNodes }
|
||||
}: {
|
||||
allChallengeNode: { nodes: ChallengeNode[] };
|
||||
allCertificateNode: { nodes: CertificateNode[] };
|
||||
allSuperBlockStructure: { nodes: SuperBlockStructure[] };
|
||||
} = useStaticQuery(graphql`
|
||||
query getBlockNode {
|
||||
allChallengeNode(
|
||||
@@ -154,10 +193,25 @@ const useGetAllBlockIds = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
allSuperBlockStructure {
|
||||
nodes {
|
||||
superBlock
|
||||
chapters {
|
||||
dashedName
|
||||
comingSoon
|
||||
modules {
|
||||
dashedName
|
||||
comingSoon
|
||||
moduleType
|
||||
blocks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return { challengeNodes, certificateNodes };
|
||||
return { challengeNodes, certificateNodes, superBlockStructureNodes };
|
||||
};
|
||||
|
||||
Progress.displayName = 'Progress';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HandlerProps } from 'react-reflex';
|
||||
import { SuperBlocks } from '../../../shared-dist/config/curriculum';
|
||||
import type { Chapter } from '../../../shared-dist/config/chapters';
|
||||
import { BlockLayouts, BlockTypes } from '../../../shared-dist/config/blocks';
|
||||
import type { ChallengeFile, Ext } from '../../../shared-dist/utils/polyvinyl';
|
||||
import { type CertTitle } from '../../config/cert-and-project-map';
|
||||
@@ -347,6 +348,20 @@ export type AllChallengesInfo = {
|
||||
certificateNodes: CertificateNode[];
|
||||
};
|
||||
|
||||
export type ChapterBasedSuperBlockStructure = {
|
||||
superBlock: SuperBlocks;
|
||||
chapters: Chapter[];
|
||||
};
|
||||
|
||||
export type BlockBasedSuperBlockStructure = {
|
||||
superBlock: SuperBlocks;
|
||||
blocks: string[];
|
||||
};
|
||||
|
||||
export type SuperBlockStructure =
|
||||
| ChapterBasedSuperBlockStructure
|
||||
| BlockBasedSuperBlockStructure;
|
||||
|
||||
export type AllChallengeNode = {
|
||||
edges: [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
// TODO: source the superblock structure via a GQL query, rather than directly
|
||||
// from the curriculum
|
||||
import superBlockStructure from '../../../curriculum/structure/superblocks/full-stack-developer.json';
|
||||
import { randomBetween } from '../utils/random-between';
|
||||
import { getSessionChallengeData } from '../utils/session-storage';
|
||||
import { superBlockStructuresSelector } from '../templates/Introduction/redux';
|
||||
import { ns as MainApp } from './action-types';
|
||||
|
||||
export const savedChallengesSelector = state =>
|
||||
@@ -121,6 +119,8 @@ export const createUserByNameSelector = username => state => {
|
||||
export const userFetchStateSelector = state => state[MainApp].userFetchState;
|
||||
export const allChallengesInfoSelector = state =>
|
||||
state[MainApp].allChallengesInfo;
|
||||
export const getSuperBlockStructure = (state, superBlock) =>
|
||||
superBlockStructuresSelector(state)[superBlock];
|
||||
|
||||
export const completedChallengesIdsSelector = createSelector(
|
||||
completedChallengesSelector,
|
||||
@@ -133,11 +133,24 @@ export const completedDailyCodingChallengesIdsSelector = createSelector(
|
||||
);
|
||||
|
||||
export const completionStateSelector = createSelector(
|
||||
[allChallengesInfoSelector, completedChallengesIdsSelector],
|
||||
(allChallengesInfo, completedChallengesIds) => {
|
||||
const chapters = superBlockStructure.chapters;
|
||||
[
|
||||
allChallengesInfoSelector,
|
||||
completedChallengesIdsSelector,
|
||||
superBlockStructuresSelector,
|
||||
state => state.challenge.challengeMeta
|
||||
],
|
||||
(
|
||||
allChallengesInfo,
|
||||
completedChallengesIds,
|
||||
superBlockStructures,
|
||||
challengeMeta
|
||||
) => {
|
||||
const { challengeNodes } = allChallengesInfo;
|
||||
|
||||
const structure = superBlockStructures[challengeMeta.superBlock];
|
||||
|
||||
const chapters = structure.chapters ?? [];
|
||||
|
||||
const getCompletionState = ({
|
||||
chapters,
|
||||
challenges,
|
||||
|
||||
@@ -35,7 +35,7 @@ import { isSignedInSelector, userSelector } from '../../../redux/selectors';
|
||||
import { mapFilesToChallengeFiles } from '../../../utils/ajax';
|
||||
import { standardizeRequestBody } from '../../../utils/challenge-request-helpers';
|
||||
import postUpdate$ from '../utils/post-update';
|
||||
import { SuperBlocks } from '../../../../../shared-dist/config/curriculum';
|
||||
import { chapterBasedSuperBlocks } from '../../../../../shared-dist/config/curriculum';
|
||||
import { actionTypes } from './action-types';
|
||||
import {
|
||||
closeModal,
|
||||
@@ -295,11 +295,11 @@ export default function completionEpic(action$, state$) {
|
||||
if (action.type !== submitActionTypes.submitComplete) return null;
|
||||
|
||||
const donationData =
|
||||
superBlock === SuperBlocks.FullStackDeveloper &&
|
||||
chapterBasedSuperBlocks.includes(superBlock) &&
|
||||
blockType !== 'review' &&
|
||||
isModuleNewlyCompletedSelector(state)
|
||||
? { module, superBlock }
|
||||
: superBlock !== SuperBlocks.FullStackDeveloper &&
|
||||
: !chapterBasedSuperBlocks.includes(superBlock) &&
|
||||
isBlockNewlyCompletedSelector(state)
|
||||
? { block, superBlock }
|
||||
: null;
|
||||
|
||||
@@ -5,18 +5,7 @@ import { Disclosure } from '@headlessui/react';
|
||||
|
||||
import { SuperBlocks } from '../../../../../shared-dist/config/curriculum';
|
||||
import DropDown from '../../../assets/icons/dropdown';
|
||||
// TODO: source the superblock structure via a GQL query, rather than directly
|
||||
// from the curriculum
|
||||
import fullStackCert from '../../../../../curriculum/structure/superblocks/full-stack-developer.json';
|
||||
import fullStackOpen from '../../../../../curriculum/structure/superblocks/full-stack-open.json';
|
||||
import a1Spanish from '../../../../../curriculum/structure/superblocks/a1-professional-spanish.json';
|
||||
import respWebDesignV9 from '../../../../../curriculum/structure/superblocks/responsive-web-design-v9.json';
|
||||
import javascriptV9 from '../../../../../curriculum/structure/superblocks/javascript-v9.json';
|
||||
import frontEndDevLibsV9 from '../../../../../curriculum/structure/superblocks/front-end-development-libraries-v9.json';
|
||||
import pythonV9 from '../../../../../curriculum/structure/superblocks/python-v9.json';
|
||||
import relationalDbV9 from '../../../../../curriculum/structure/superblocks/relational-databases-v9.json';
|
||||
import backEndDevApisV9 from '../../../../../curriculum/structure/superblocks/back-end-development-and-apis-v9.json';
|
||||
|
||||
import type { ChapterBasedSuperBlockStructure } from '../../../redux/prop-types';
|
||||
import { ChapterIcon } from '../../../assets/chapter-icon';
|
||||
import { type Chapter } from '../../../../../shared-dist/config/chapters';
|
||||
import {
|
||||
@@ -67,6 +56,7 @@ interface Challenge {
|
||||
interface SuperBlockAccordionProps {
|
||||
challenges: Challenge[];
|
||||
superBlock: SuperBlocks;
|
||||
structure: ChapterBasedSuperBlockStructure;
|
||||
chosenBlock: string;
|
||||
completedChallengeIds: string[];
|
||||
}
|
||||
@@ -182,37 +172,11 @@ const LinkBlock = ({
|
||||
export const SuperBlockAccordion = ({
|
||||
challenges,
|
||||
superBlock,
|
||||
structure,
|
||||
chosenBlock,
|
||||
completedChallengeIds
|
||||
}: SuperBlockAccordionProps) => {
|
||||
function getSuperblockStructure(superBlock: SuperBlocks): {
|
||||
chapters: Chapter[];
|
||||
} {
|
||||
switch (superBlock) {
|
||||
case SuperBlocks.FullStackOpen:
|
||||
return fullStackOpen;
|
||||
case SuperBlocks.FullStackDeveloper:
|
||||
return fullStackCert;
|
||||
case SuperBlocks.A1Spanish:
|
||||
return a1Spanish;
|
||||
case SuperBlocks.RespWebDesignV9:
|
||||
return respWebDesignV9;
|
||||
case SuperBlocks.JsV9:
|
||||
return javascriptV9;
|
||||
case SuperBlocks.FrontEndDevLibsV9:
|
||||
return frontEndDevLibsV9;
|
||||
case SuperBlocks.PythonV9:
|
||||
return pythonV9;
|
||||
case SuperBlocks.RelationalDbV9:
|
||||
return relationalDbV9;
|
||||
case SuperBlocks.BackEndDevApisV9:
|
||||
return backEndDevApisV9;
|
||||
default:
|
||||
throw new Error("The SuperBlock structure hasn't been imported.");
|
||||
}
|
||||
}
|
||||
|
||||
const superBlockStructure = getSuperblockStructure(superBlock);
|
||||
const superBlockStructure = structure;
|
||||
|
||||
const modules = superBlockStructure.chapters.flatMap<Module>(
|
||||
({ modules }) => modules
|
||||
|
||||
@@ -7,16 +7,25 @@ export const ns = 'curriculumMap';
|
||||
const initialState = {
|
||||
expandedState: {
|
||||
block: {}
|
||||
}
|
||||
},
|
||||
superBlockStructures: {}
|
||||
};
|
||||
|
||||
const types = createTypes(['resetExpansion', 'toggleBlock'], ns);
|
||||
const types = createTypes(
|
||||
['resetExpansion', 'toggleBlock', 'updateSuperBlockStructures'],
|
||||
ns
|
||||
);
|
||||
|
||||
export const resetExpansion = createAction(types.resetExpansion);
|
||||
export const toggleBlock = createAction(types.toggleBlock);
|
||||
export const updateSuperBlockStructures = createAction(
|
||||
types.updateSuperBlockStructures
|
||||
);
|
||||
|
||||
export const makeExpandedBlockSelector = block => state =>
|
||||
!!state[ns].expandedState.block[block];
|
||||
export const superBlockStructuresSelector = state =>
|
||||
state[ns].superBlockStructures || {};
|
||||
|
||||
export const reducer = handleActions(
|
||||
{
|
||||
@@ -35,6 +44,10 @@ export const reducer = handleActions(
|
||||
[payload]: !state.expandedState.block[payload]
|
||||
}
|
||||
}
|
||||
}),
|
||||
[types.updateSuperBlockStructures]: (state, { payload }) => ({
|
||||
...state,
|
||||
superBlockStructures: { ...payload }
|
||||
})
|
||||
},
|
||||
initialState
|
||||
|
||||
@@ -27,7 +27,11 @@ import {
|
||||
userFetchStateSelector,
|
||||
signInLoadingSelector
|
||||
} from '../../redux/selectors';
|
||||
import type { User } from '../../redux/prop-types';
|
||||
import type {
|
||||
SuperBlockStructure,
|
||||
User,
|
||||
ChapterBasedSuperBlockStructure
|
||||
} from '../../redux/prop-types';
|
||||
import { CertTitle, liveCerts } from '../../../config/cert-and-project-map';
|
||||
import { superBlockToCertMap } from '../../../../shared-dist/config/certification-settings';
|
||||
import {
|
||||
@@ -71,6 +75,7 @@ type SuperBlockProps = {
|
||||
currentChallengeId: string;
|
||||
data: {
|
||||
allChallengeNode: { nodes: ChallengeNode[] };
|
||||
allSuperBlockStructure: { nodes: SuperBlockStructure[] };
|
||||
};
|
||||
expandedState: {
|
||||
[key: string]: boolean;
|
||||
@@ -143,7 +148,8 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
|
||||
|
||||
const {
|
||||
data: {
|
||||
allChallengeNode: { nodes }
|
||||
allChallengeNode: { nodes },
|
||||
allSuperBlockStructure
|
||||
},
|
||||
isSignedIn,
|
||||
currentChallengeId,
|
||||
@@ -173,6 +179,10 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
|
||||
|
||||
const i18nTitle = i18next.t(`intro:${superBlock}.title`);
|
||||
|
||||
const currentSuperBlockStructure = allSuperBlockStructure.nodes.find(
|
||||
node => node.superBlock === superBlock
|
||||
);
|
||||
|
||||
const showCertification = liveCerts.some(
|
||||
cert => superBlockToCertMap[superBlock] === cert.certSlug
|
||||
);
|
||||
@@ -265,6 +275,9 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
|
||||
<SuperBlockAccordion
|
||||
challenges={superBlockChallenges}
|
||||
superBlock={superBlock}
|
||||
structure={
|
||||
currentSuperBlockStructure as ChapterBasedSuperBlockStructure
|
||||
}
|
||||
chosenBlock={initialExpandedBlock}
|
||||
completedChallengeIds={completedChallenges.map(c => c.id)}
|
||||
/>
|
||||
@@ -359,5 +372,20 @@ export const query = graphql`
|
||||
}
|
||||
}
|
||||
}
|
||||
allSuperBlockStructure {
|
||||
nodes {
|
||||
superBlock
|
||||
chapters {
|
||||
dashedName
|
||||
comingSoon
|
||||
modules {
|
||||
dashedName
|
||||
comingSoon
|
||||
moduleType
|
||||
blocks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const { getSuperblockStructure } = require('../../../curriculum/file-handler');
|
||||
|
||||
const { createChallengeNode } = require('./create-challenge-nodes');
|
||||
|
||||
exports.sourceNodes = function sourceChallengesSourceNodes(
|
||||
{ actions, reporter },
|
||||
{ actions, reporter, createNodeId, createContentDigest },
|
||||
pluginOptions
|
||||
) {
|
||||
const { source, onSourceChange, curriculumPath } = pluginOptions;
|
||||
@@ -67,9 +69,13 @@ exports.sourceNodes = function sourceChallengesSourceNodes(
|
||||
function sourceAndCreateNodes() {
|
||||
return source()
|
||||
.then(challenges => Promise.all(challenges))
|
||||
.then(challenges =>
|
||||
challenges.map(challenge => createVisibleChallenge(challenge))
|
||||
)
|
||||
.then(challenges => {
|
||||
// create challenge nodes
|
||||
challenges.forEach(challenge => createVisibleChallenge(challenge));
|
||||
// create superblock structure nodes
|
||||
createSuperBlockStructureNodes();
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e);
|
||||
reporter.panic(`fcc-source-challenges
|
||||
@@ -84,6 +90,50 @@ exports.sourceNodes = function sourceChallengesSourceNodes(
|
||||
createNode(createChallengeNode(challenge, reporter, options));
|
||||
}
|
||||
|
||||
function createSuperBlockStructureNodes() {
|
||||
const buildCurriculumPath = path.resolve(
|
||||
curriculumPath,
|
||||
'..',
|
||||
'..',
|
||||
'build-curriculum'
|
||||
);
|
||||
const buildCurriculum = require(buildCurriculumPath);
|
||||
const superBlockToFilename = buildCurriculum.superBlockToFilename;
|
||||
|
||||
if (!superBlockToFilename) {
|
||||
reporter.panic(
|
||||
'superBlockToFilename is missing from build-curriculum. This map is required.'
|
||||
);
|
||||
}
|
||||
|
||||
Object.keys(superBlockToFilename).forEach(superBlock => {
|
||||
const filename = superBlockToFilename[superBlock] || superBlock;
|
||||
try {
|
||||
const structure = getSuperblockStructure(filename);
|
||||
|
||||
const nodeId = createNodeId(`SuperBlockStructure-${superBlock}`);
|
||||
const nodeContent = JSON.stringify(structure);
|
||||
|
||||
createNode({
|
||||
...structure,
|
||||
superBlock,
|
||||
id: nodeId,
|
||||
parent: null,
|
||||
children: [],
|
||||
internal: {
|
||||
type: 'SuperBlockStructure',
|
||||
content: nodeContent,
|
||||
contentDigest: createContentDigest(structure)
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reporter.warn(
|
||||
`Could not load structure for ${superBlock} (${filename}): ${err.message}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
watcher.on('ready', () => sourceAndCreateNodes().then(resolve, reject));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user