mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
fix(client): show project previews (#58761)
This commit is contained in:
committed by
GitHub
parent
552791879c
commit
0c754bf690
@@ -6,7 +6,11 @@ import { Table, Spacer } from '@freecodecamp/ui';
|
||||
|
||||
import { Link } from '../components/helpers';
|
||||
import ProjectModal from '../components/SolutionViewer/project-modal';
|
||||
import type { CompletedChallenge, User } from '../redux/prop-types';
|
||||
import type {
|
||||
ChallengeData,
|
||||
CompletedChallenge,
|
||||
User
|
||||
} from '../redux/prop-types';
|
||||
import { certsToProjects } from '../../config/cert-and-project-map';
|
||||
|
||||
import { SolutionDisplayWidget } from '../components/solution-display-widget';
|
||||
@@ -15,7 +19,7 @@ import ExamResultsModal from '../components/SolutionViewer/exam-results-modal';
|
||||
|
||||
import { openModal } from '../templates/Challenges/redux/actions';
|
||||
|
||||
import { regeneratePathAndHistory } from '../../../shared/utils/polyvinyl';
|
||||
import { regenerateMissingProperties } from '../../../shared/utils/polyvinyl';
|
||||
import '../components/layouts/project-links.css';
|
||||
import { Certification } from '../../../shared/config/certification-settings';
|
||||
interface ShowProjectLinksProps {
|
||||
@@ -164,13 +168,14 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
const challengeData: CompletedChallenge | null = completedChallenge
|
||||
const challengeData: ChallengeData | null = completedChallenge
|
||||
? {
|
||||
...completedChallenge,
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
challengeFiles:
|
||||
completedChallenge?.challengeFiles?.map(regeneratePathAndHistory) ??
|
||||
null
|
||||
completedChallenge?.challengeFiles?.map(
|
||||
regenerateMissingProperties
|
||||
) ?? null
|
||||
}
|
||||
: null;
|
||||
|
||||
|
||||
@@ -10,9 +10,12 @@ import { Table, Button, Modal, Spacer } from '@freecodecamp/ui';
|
||||
import envData from '../../../../config/env.json';
|
||||
import { getLangCode } from '../../../../../shared/config/i18n';
|
||||
import { getCertIds, getPathFromID } from '../../../../utils';
|
||||
import { regeneratePathAndHistory } from '../../../../../shared/utils/polyvinyl';
|
||||
import { regenerateMissingProperties } from '../../../../../shared/utils/polyvinyl';
|
||||
import CertificationIcon from '../../../assets/icons/certification';
|
||||
import { CompletedChallenge } from '../../../redux/prop-types';
|
||||
import type {
|
||||
ChallengeData,
|
||||
CompletedChallenge
|
||||
} from '../../../redux/prop-types';
|
||||
import ProjectPreviewModal from '../../../templates/Challenges/components/project-preview-modal';
|
||||
import ExamResultsModal from '../../SolutionViewer/exam-results-modal';
|
||||
import { openModal } from '../../../templates/Challenges/redux/actions';
|
||||
@@ -160,13 +163,14 @@ function TimelineInner({
|
||||
);
|
||||
}
|
||||
|
||||
const challengeData: CompletedChallenge | null = completedChallenge
|
||||
const challengeData: ChallengeData | null = completedChallenge
|
||||
? {
|
||||
...completedChallenge,
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
challengeFiles:
|
||||
completedChallenge?.challengeFiles?.map(regeneratePathAndHistory) ??
|
||||
null
|
||||
completedChallenge?.challengeFiles?.map(
|
||||
regenerateMissingProperties
|
||||
) ?? null
|
||||
}
|
||||
: null;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import ScrollableAnchor, { configureAnchors } from 'react-scrollable-anchor';
|
||||
import { connect } from 'react-redux';
|
||||
import { Table, Button, Spacer } from '@freecodecamp/ui';
|
||||
|
||||
import { regeneratePathAndHistory } from '../../../../shared/utils/polyvinyl';
|
||||
import { regenerateMissingProperties } from '../../../../shared/utils/polyvinyl';
|
||||
import ProjectPreviewModal from '../../templates/Challenges/components/project-preview-modal';
|
||||
import ExamResultsModal from '../SolutionViewer/exam-results-modal';
|
||||
import { openModal } from '../../templates/Challenges/redux/actions';
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
} from '../../../../shared/config/certification-settings';
|
||||
import env from '../../../config/env.json';
|
||||
|
||||
import {
|
||||
import type {
|
||||
ChallengeData,
|
||||
ClaimedCertifications,
|
||||
CompletedChallenge,
|
||||
GeneratedExamResults,
|
||||
@@ -206,7 +207,7 @@ function CertificationSettings(props: CertificationSettingsProps) {
|
||||
const [challengeFiles, setChallengeFiles] = useState<
|
||||
CompletedChallenge['challengeFiles'] | null
|
||||
>(null);
|
||||
const [challengeData, setChallengeData] = useState<CompletedChallenge | null>(
|
||||
const [challengeData, setChallengeData] = useState<ChallengeData | null>(
|
||||
null
|
||||
);
|
||||
const [solution, setSolution] = useState<string | null>();
|
||||
@@ -245,8 +246,9 @@ function CertificationSettings(props: CertificationSettingsProps) {
|
||||
? {
|
||||
...completedProject,
|
||||
challengeFiles:
|
||||
completedProject?.challengeFiles?.map(regeneratePathAndHistory) ??
|
||||
null
|
||||
completedProject?.challengeFiles?.map(
|
||||
regenerateMissingProperties
|
||||
) ?? null
|
||||
}
|
||||
: null;
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@ export type SavedChallengeFile = {
|
||||
|
||||
export type SavedChallengeFiles = SavedChallengeFile[];
|
||||
|
||||
export type CompletedChallenge = {
|
||||
export interface CompletedChallenge {
|
||||
id: string;
|
||||
solution?: string | null;
|
||||
githubLink?: string;
|
||||
@@ -376,7 +376,11 @@ export type CompletedChallenge = {
|
||||
| Pick<ChallengeFile, 'contents' | 'ext' | 'fileKey' | 'name'>[]
|
||||
| null;
|
||||
examResults?: GeneratedExamResults;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChallengeData extends CompletedChallenge {
|
||||
challengeFiles: ChallengeFile[] | null;
|
||||
}
|
||||
|
||||
export type FileKey =
|
||||
| 'scriptjs'
|
||||
|
||||
@@ -15,11 +15,11 @@ import { challengeTypes } from '../../../../../shared/config/challenge-types';
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import { MAX_MOBILE_WIDTH } from '../../../../config/misc';
|
||||
|
||||
import {
|
||||
import type {
|
||||
ChallengeData,
|
||||
ChallengeFiles,
|
||||
ChallengeMeta,
|
||||
ChallengeNode,
|
||||
CompletedChallenge,
|
||||
ResizeProps,
|
||||
SavedChallenge,
|
||||
SavedChallengeFiles,
|
||||
@@ -113,7 +113,7 @@ interface ShowClassicProps extends Pick<PreviewProps, 'previewMounted'> {
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
projectPreview: {
|
||||
challengeData: CompletedChallenge;
|
||||
challengeData: ChallengeData;
|
||||
};
|
||||
};
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import type { CompletedChallenge } from '../../../redux/prop-types';
|
||||
import type { ChallengeData } from '../../../redux/prop-types';
|
||||
import {
|
||||
closeModal,
|
||||
setEditorFocusability,
|
||||
@@ -15,14 +15,14 @@ import Preview from './preview';
|
||||
import './project-preview-modal.css';
|
||||
|
||||
interface ProjectPreviewMountedPayload {
|
||||
challengeData: CompletedChallenge | null;
|
||||
challengeData: ChallengeData | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
closeModal: (arg: string) => void;
|
||||
isOpen: boolean;
|
||||
projectPreviewMounted: (payload: ProjectPreviewMountedPayload) => void;
|
||||
challengeData: CompletedChallenge | null;
|
||||
challengeData: ChallengeData | null;
|
||||
setEditorFocusability: (focusability: boolean) => void;
|
||||
previewTitle: string;
|
||||
closeText: string;
|
||||
|
||||
@@ -354,13 +354,18 @@ function* previewProjectSolutionSaga({ payload }) {
|
||||
try {
|
||||
if (canBuildChallenge(challengeData)) {
|
||||
const buildData = yield buildChallengeData(challengeData);
|
||||
if (buildData.error) throw Error(buildData.error);
|
||||
|
||||
if (challengeHasPreview(challengeData)) {
|
||||
const document = yield getContext('document');
|
||||
yield call(updateProjectPreview, buildData, document);
|
||||
} else {
|
||||
throw Error('Project does not have a preview');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error('Unable to show project preview');
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
86
e2e/completed-project-preview.spec.ts
Normal file
86
e2e/completed-project-preview.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
import tributePage from './fixtures/tribute-page.json';
|
||||
|
||||
import { authedRequest } from './utils/request';
|
||||
|
||||
const unlockedProfile = {
|
||||
isLocked: false,
|
||||
showAbout: true,
|
||||
showCerts: true,
|
||||
showDonation: true,
|
||||
showHeatMap: true,
|
||||
showLocation: true,
|
||||
showName: true,
|
||||
showPoints: true,
|
||||
showPortfolio: true,
|
||||
showTimeLine: true
|
||||
};
|
||||
|
||||
async function expectPreviewToBeShown(page: Page) {
|
||||
await page
|
||||
.getByRole('button', { name: 'View Solution for Build a Tribute Page' })
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'View Project' }).click();
|
||||
const modalHeading = page.getByRole('heading', {
|
||||
name: 'Build a Tribute Page',
|
||||
exact: true
|
||||
});
|
||||
await expect(modalHeading).toBeVisible();
|
||||
|
||||
const projectPreview = page.frameLocator('#fcc-project-preview-frame');
|
||||
await expect(projectPreview.getByText('Tribute page text')).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe('Completed project preview', () => {
|
||||
test.use({ storageState: 'playwright/.auth/development-user.json' });
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
execSync('node ./tools/scripts/seed/seed-demo-user');
|
||||
|
||||
await authedRequest({
|
||||
request,
|
||||
method: 'post',
|
||||
endpoint: '/modern-challenge-completed',
|
||||
data: {
|
||||
id: tributePage.id,
|
||||
challengeType: 14,
|
||||
files: [tributePage.htmlFile, tributePage.cssFile]
|
||||
}
|
||||
});
|
||||
|
||||
await authedRequest({
|
||||
request,
|
||||
endpoint: '/update-my-profileui',
|
||||
method: 'put',
|
||||
data: {
|
||||
profileUI: unlockedProfile
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('it should be viewable on the timeline', async ({ page }) => {
|
||||
await page.goto('/developmentuser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '@developmentuser' })
|
||||
).toBeVisible();
|
||||
|
||||
await expectPreviewToBeShown(page);
|
||||
});
|
||||
|
||||
test('it should be viewable on the settings page', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
|
||||
await expectPreviewToBeShown(page);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
import { authedRequest } from './utils/request';
|
||||
@@ -6,6 +8,10 @@ import { allowTrailingSlash } from './utils/url';
|
||||
const nextChallengeURL =
|
||||
'/learn/data-analysis-with-python/data-analysis-with-python-projects/demographic-data-analyzer';
|
||||
|
||||
test.beforeAll(() => {
|
||||
execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(
|
||||
'/learn/data-analysis-with-python/data-analysis-with-python-projects/mean-variance-standard-deviation-calculator'
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"tribute-page-css": {
|
||||
"contents": "#image{max-width:100%;height:auto;display:block;margin:0 auto;}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"tribute-page-html": {
|
||||
"contents": "<head><link rel=\"stylesheet\" href=\"styles.css\"></head><body><main id=\"main\"><div id=\"title\">D</div><div id=\"img-div\"><img id=\"image\" src=\"\" alt=\"D\" ><div id=\"img-caption\">s</div></div><div id=\"tribute-info\">s</div><a id=\"tribute-link\" href=\"\" target=\"_blank\">L</a></main></body></html>"
|
||||
}
|
||||
}
|
||||
17
e2e/fixtures/tribute-page.json
Normal file
17
e2e/fixtures/tribute-page.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd18",
|
||||
"htmlFile": {
|
||||
"contents": "<head><link rel=\"stylesheet\" href=\"styles.css\"></head><body><main id=\"main\"><div id=\"title\">D</div><div id=\"img-div\"><img id=\"image\" src=\"\" alt=\"D\" ><div id=\"img-caption\">s</div></div><div id=\"tribute-info\">s</div><a id=\"tribute-link\" href=\"\" target=\"_blank\">L</a> Tribute page text</main></body></html>",
|
||||
"key": "indexhtml",
|
||||
"ext": "html",
|
||||
"name": "index",
|
||||
"history": ["index.html"]
|
||||
},
|
||||
"cssFile": {
|
||||
"contents": "#image{max-width:100%;height:auto;display:block;margin:0 auto;}",
|
||||
"key": "stylescss",
|
||||
"ext": "css",
|
||||
"name": "styles",
|
||||
"history": ["styles.css"]
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import { execSync } from 'child_process';
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import { SuperBlocks } from '../shared/config/curriculum';
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
import tributePageHtml from './fixtures/tribute-page-html.json';
|
||||
import tributePageCss from './fixtures/tribute-page-css.json';
|
||||
import tributePage from './fixtures/tribute-page.json';
|
||||
import curriculum from './fixtures/js-ads-projects.json';
|
||||
import { authedRequest } from './utils/request';
|
||||
|
||||
@@ -255,8 +254,8 @@ test.describe('Completion modal should be shown after submitting a project', ()
|
||||
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
|
||||
const tributeContent = [
|
||||
tributePageHtml['tribute-page-html'].contents,
|
||||
tributePageCss['tribute-page-css'].contents
|
||||
tributePage.htmlFile.contents,
|
||||
tributePage.cssFile.contents
|
||||
];
|
||||
|
||||
await page.goto(
|
||||
|
||||
@@ -4,14 +4,16 @@ import invariant from 'invariant';
|
||||
const exts = ['js', 'html', 'css', 'jsx', 'ts', 'py'] as const;
|
||||
export type Ext = (typeof exts)[number];
|
||||
|
||||
export type IncompleteChallengeFile = {
|
||||
export interface IncompleteChallengeFile {
|
||||
fileKey: string;
|
||||
ext: Ext;
|
||||
name: string;
|
||||
contents: string;
|
||||
};
|
||||
head?: string;
|
||||
tail?: string;
|
||||
}
|
||||
|
||||
export type ChallengeFile = IncompleteChallengeFile & {
|
||||
export interface ChallengeFile extends IncompleteChallengeFile {
|
||||
editableRegionBoundaries?: number[];
|
||||
editableContents?: string;
|
||||
usesMultifileEditor?: boolean;
|
||||
@@ -22,7 +24,7 @@ export type ChallengeFile = IncompleteChallengeFile & {
|
||||
source?: string | null;
|
||||
path: string;
|
||||
history: string[];
|
||||
};
|
||||
}
|
||||
|
||||
type PolyProps = {
|
||||
name: string;
|
||||
@@ -118,12 +120,14 @@ export function setContent(
|
||||
|
||||
// This is currently only used to add back properties that are not stored in the
|
||||
// database.
|
||||
export function regeneratePathAndHistory(file: IncompleteChallengeFile) {
|
||||
export function regenerateMissingProperties(file: IncompleteChallengeFile) {
|
||||
const newPath = file.name + '.' + file.ext;
|
||||
const newFile = {
|
||||
...file,
|
||||
path: newPath,
|
||||
history: [newPath]
|
||||
history: [newPath],
|
||||
head: file.head ?? '',
|
||||
tail: file.tail ?? ''
|
||||
};
|
||||
return newFile;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user