From 89141ca0cbf0bf9f4ea5e9c5c3635aca81116e33 Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Tue, 12 Sep 2023 23:10:14 -0500 Subject: [PATCH 01/17] fixes np-3 --- src/components/Button.tsx | 1 + src/components/home/Home.tsx | 25 ++++- src/components/template/CustomPlan.tsx | 8 +- src/components/template/Modal.tsx | 8 +- src/components/template/NewPlan.tsx | 132 +++++++++++++++++++++++++ src/components/template/Page.tsx | 11 ++- 6 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 src/components/template/NewPlan.tsx diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 9e220931b..70f1b6471 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -32,6 +32,7 @@ export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { font?: keyof typeof fontClasses; icon?: React.ReactNode; isLoading?: boolean; + disabled?: boolean; } const Button: FC = ({ diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx index 2943bece6..06cf7d030 100644 --- a/src/components/home/Home.tsx +++ b/src/components/home/Home.tsx @@ -15,6 +15,7 @@ import TemplateModal from '../template/Modal'; * A list of the user's plans */ export default function PlansPage(): JSX.Element { + const [startNew, setStartNew] = useState(false); const [openTemplateModal, setOpenTemplateModal] = useState(false); const userPlanQuery = trpc.plan.getUserPlans.useQuery(undefined, { staleTime: Infinity, @@ -31,7 +32,7 @@ export default function PlansPage(): JSX.Element { }); const { data } = userPlanQuery; - const [planPage, setPlanPage] = useState<0 | 1>(1); + const [planPage, setPlanPage] = useState<0 | 1 | 2>(1); const router = useRouter(); const [showHomeOnboardingModal, setShowHomeOnboardingModal] = useState(false); @@ -125,10 +126,24 @@ export default function PlansPage(): JSX.Element { { setPlanPage(0); setOpenTemplateModal(true); + setStartNew(true); + }} + /> + + + + + + { + setPlanPage(1); + setOpenTemplateModal(true); }} /> @@ -137,10 +152,10 @@ export default function PlansPage(): JSX.Element { { - setPlanPage(1); + setPlanPage(2); setOpenTemplateModal(true); }} /> @@ -193,6 +208,6 @@ const DropdownItem = ({ text, onClick, className = '', ...props }: ItemProps) => className={`flex w-full min-w-max cursor-pointer items-center gap-x-3 border-b border-neutral-300 px-2 py-2 text-sm hover:bg-neutral-200 ${className}`} {...props} > - {text} + {text} ); diff --git a/src/components/template/CustomPlan.tsx b/src/components/template/CustomPlan.tsx index 30e3ee31f..20d493955 100644 --- a/src/components/template/CustomPlan.tsx +++ b/src/components/template/CustomPlan.tsx @@ -27,6 +27,7 @@ export default function CustomPlan({ onDismiss }: { onDismiss: () => void }) { const [takenCourses, setTakenCourses] = useState([]); const fileInputRef = useRef(null); const [file, setFile] = useState(); + const [isDisabled, setIsDisabled] = useState(true); const [planNameError, setPlanNameError] = useState(false); const [majorError, setMajorError] = useState(false); @@ -178,6 +179,7 @@ export default function CustomPlan({ onDismiss }: { onDismiss: () => void }) { .filter((credit) => !credit.transfer) .map((credit) => ({ courseCode: credit.courseCode, semesterCode: credit.semesterCode })), ); + setIsDisabled(false); }; const dropRef = useRef(null); @@ -296,7 +298,7 @@ export default function CustomPlan({ onDismiss }: { onDismiss: () => void }) { void }) { { name: 'Create Plan', onClick: handleSubmit, - color: 'primary', + color: file ? 'primary' : 'secondary', loading, 'data-testid': 'create-plan-btn', + disabled: isDisabled, }, ]} > @@ -457,5 +460,6 @@ export interface PageProps { color: ButtonProps['color']; loading?: boolean; 'data-testid'?: string; + disabled?: boolean; }[]; } diff --git a/src/components/template/Modal.tsx b/src/components/template/Modal.tsx index c311e5b39..bbbfe80e6 100644 --- a/src/components/template/Modal.tsx +++ b/src/components/template/Modal.tsx @@ -1,16 +1,18 @@ import dynamic from 'next/dynamic'; +import NewPlan from './NewPlan'; import TemplateView from './TemplateView'; interface TemplateModalProps { setOpenTemplateModal: (flag: boolean) => void; - page: 0 | 1; + page: 0 | 1 | 2; } export default function TemplateModal({ setOpenTemplateModal, page }: TemplateModalProps) { const CustomPlan = dynamic(() => import('@/components/template/CustomPlan'), { ssr: false }); const modalScreens = [ - setOpenTemplateModal(false)} />, - setOpenTemplateModal(false)} />, + setOpenTemplateModal(false)} />, + setOpenTemplateModal(false)} />, + setOpenTemplateModal(false)} />, ]; return (
void }) { + const [name, setName] = useState(''); + const [major, setMajor] = useState(null); + const [planNameError, setPlanNameError] = useState(false); + const [majorError, setMajorError] = useState(false); + const setErrors = () => { + setPlanNameError(name === ''); + setMajorError(major === null); + }; + + const router = useRouter(); + const utils = trpc.useContext(); + + const createUserPlan = trpc.user.createUserPlan.useMutation({ + async onSuccess() { + await utils.user.getUser.invalidate(); + }, + }); + + const { results, updateQuery } = useSearch({ + getData: async () => (majors ? majors.map((major) => ({ filMajor: `${major}` })) : []), + initialQuery: '', + filterFn: (major, query) => major.filMajor.toLowerCase().includes(query.toLowerCase()), + constraints: [0, 100], + }); + + const [loading, setLoading] = useState(false); + + async function handleSubmit() { + if (name !== '' && major !== null) { + setLoading(true); + const planId = await createUserPlan.mutateAsync({ + name, + major, + transferCredits: [], + takenCourses: [], + }); + router.push(`/app/plans/${planId}`); + } + } + + // TODO(https://nebula-labs.atlassian.net/browse/NP-85): Refactor parseTranscript. + return ( + { + if (name !== '' && major !== null) { + handleSubmit(); + return; + } + + setErrors(); + }, + color: 'primary', + loading, + 'data-testid': 'create-plan-btn', + }, + ]} + > +

Plan Name

+
+ { + setName(e.target.value); + }} + /> + + Please provide a plan name + +
+ +

Choose your major

+
+ setMajor(value)} + onInputChange={(query: string) => updateQuery(query)} + options={results.map((major: { filMajor: string }) => major.filMajor)} + autoFocus + > +
+ + Please select a valid major + +
+ ); +} + +export interface PageProps { + title: string; + subtitle: string; + close: () => void; + actions: { + name: string; + onClick: () => void; + color: ButtonProps['color']; + loading?: boolean; + 'data-testid'?: string; + disabled?: boolean; + }[]; +} diff --git a/src/components/template/Page.tsx b/src/components/template/Page.tsx index 6a5ddcf68..dbb7ad655 100644 --- a/src/components/template/Page.tsx +++ b/src/components/template/Page.tsx @@ -44,8 +44,15 @@ export function Page({
{children}
- {actions.map(({ name, onClick, color, loading, ...props }, index) => ( - ))} From 97b0d9cd7fa553b1df99153daee1e2406f0cce7c Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Tue, 12 Sep 2023 23:14:52 -0500 Subject: [PATCH 02/17] home.tsx change --- src/components/home/Home.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx index 06cf7d030..f8f64da1e 100644 --- a/src/components/home/Home.tsx +++ b/src/components/home/Home.tsx @@ -15,7 +15,6 @@ import TemplateModal from '../template/Modal'; * A list of the user's plans */ export default function PlansPage(): JSX.Element { - const [startNew, setStartNew] = useState(false); const [openTemplateModal, setOpenTemplateModal] = useState(false); const userPlanQuery = trpc.plan.getUserPlans.useQuery(undefined, { staleTime: Infinity, @@ -130,7 +129,6 @@ export default function PlansPage(): JSX.Element { onClick={() => { setPlanPage(0); setOpenTemplateModal(true); - setStartNew(true); }} /> From 9cbf967ce40802e0033e5b2be7cfa0ba28dc3c40 Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Thu, 14 Sep 2023 14:21:22 -0500 Subject: [PATCH 03/17] majorsList to useMajors --- src/components/template/CustomPlan.tsx | 2 +- src/components/template/NewPlan.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/template/CustomPlan.tsx b/src/components/template/CustomPlan.tsx index 98dee1f52..42a200ca4 100644 --- a/src/components/template/CustomPlan.tsx +++ b/src/components/template/CustomPlan.tsx @@ -311,7 +311,7 @@ export default function CustomPlan({ onDismiss }: { onDismiss: () => void }) { { name: 'Create Plan', onClick: handleSubmit, - color: file ? 'primary' : 'secondary', + color: 'primary', loading, 'data-testid': 'create-plan-btn', disabled: isDisabled, diff --git a/src/components/template/NewPlan.tsx b/src/components/template/NewPlan.tsx index 29f44e36e..7e3673196 100644 --- a/src/components/template/NewPlan.tsx +++ b/src/components/template/NewPlan.tsx @@ -2,20 +2,21 @@ import { useRouter } from 'next/router'; import { useState } from 'react'; import AutoCompleteMajor from '@/components/AutoCompleteMajor'; + import { trpc } from '@/utils/trpc'; -import majorsList from '@data/majors.json'; + +import useMajors from '@/shared/useMajors'; import { Page } from './Page'; import { ButtonProps } from '../Button'; import useSearch from '../search/search'; -const majors = majorsList as string[]; - export default function CustomPlan({ onDismiss }: { onDismiss: () => void }) { const [name, setName] = useState(''); const [major, setMajor] = useState(null); const [planNameError, setPlanNameError] = useState(false); const [majorError, setMajorError] = useState(false); + const { majors, err } = useMajors(); const setErrors = () => { setPlanNameError(name === ''); setMajorError(major === null); From ea929bb0d3f0e11cd3364cc3aab70e8c5122822e Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Sat, 16 Sep 2023 13:24:43 -0500 Subject: [PATCH 04/17] integration test fix --- cypress/e2e/create-plan.cy.ts | 1 + src/components/home/Home.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/create-plan.cy.ts b/cypress/e2e/create-plan.cy.ts index f92cbf2c0..f82b347d4 100644 --- a/cypress/e2e/create-plan.cy.ts +++ b/cypress/e2e/create-plan.cy.ts @@ -11,6 +11,7 @@ describe('Plan creation flow', () => { cy.task('log', 'Opening custom plan modal...'); cy.dataTestId('add-new-plan-btn').click(); cy.dataTestId('add-custom-plan-btn').click(); + cy.dataTestId('add-transcript-plan-btn').click(); // Modal should be visible cy.dataTestId('create-custom-plan-page').then(($el) => Cypress.dom.isVisible($el)); diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx index f8f64da1e..50c2cc648 100644 --- a/src/components/home/Home.tsx +++ b/src/components/home/Home.tsx @@ -137,7 +137,7 @@ export default function PlansPage(): JSX.Element { { setPlanPage(1); From 7be7ed04b7816aba4abf64fdc6fe3b526eb4fa30 Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Sat, 16 Sep 2023 13:39:13 -0500 Subject: [PATCH 05/17] integration fix 2 --- cypress/e2e/create-plan.cy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/e2e/create-plan.cy.ts b/cypress/e2e/create-plan.cy.ts index f82b347d4..1a940b73c 100644 --- a/cypress/e2e/create-plan.cy.ts +++ b/cypress/e2e/create-plan.cy.ts @@ -12,6 +12,7 @@ describe('Plan creation flow', () => { cy.dataTestId('add-new-plan-btn').click(); cy.dataTestId('add-custom-plan-btn').click(); cy.dataTestId('add-transcript-plan-btn').click(); + cy.dataTestId('add-template-plan-btn').click(); // Modal should be visible cy.dataTestId('create-custom-plan-page').then(($el) => Cypress.dom.isVisible($el)); From 48a9b6881d1ea949b92b8c05f45796a5168e6e1d Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Sat, 16 Sep 2023 18:11:52 -0500 Subject: [PATCH 06/17] tests for creating custom/template plans --- cypress/data/dummytranscript.pdf | Bin 0 -> 8439 bytes cypress/e2e/create-plan.cy.ts | 114 ++++++++++++++++++++++- src/components/home/Home.tsx | 4 +- src/components/template/CustomPlan.tsx | 1 + src/components/template/NewPlan.tsx | 2 +- src/components/template/TemplateView.tsx | 4 + 6 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 cypress/data/dummytranscript.pdf diff --git a/cypress/data/dummytranscript.pdf b/cypress/data/dummytranscript.pdf new file mode 100644 index 0000000000000000000000000000000000000000..479d65ca3b8344585355b08a68c4a8c8a4f3edf6 GIT binary patch literal 8439 zcmb_>WmH|;vgXF!gS)$JT!L-fB^%hdySoHPaDo$D6Ffk0cXxMp3mP=Ro7_G9IVm_O98GM{goWAPc{`a?u!D@K0c^Y~v{|Em={$kE-wje?6^!P?A~LYMnb9|}&se-*$# zA5ho-_Yl1Q9zxpM&duC~f?e9q*v(wh+|<#`oL$k}!P3o&f`^-%UE12k)s2FWpIw=P z6ZqHcR7`9%-0Vd}T-{vEjqTBpy_QdtMr_tNFhdQ^lZPadq&-0#yB^C*LzGChnBx*D z&PUtYUAIW|j)uFnYi^%CX7H-3g|8+k#(tp?Vs3USSOJt^7yxZhl5yc5u=8C$Am@WW z5am0H1xdghcJh#!;d&!sPeM@W=^t*y zqP!a6Oj?co)hT+j^Mn7FD75{<3aR13$z^402c%m<7_Kvbxd!c5uc-Q24@96d3jl>j z+bDhZn^%#W?Tc6^Pe)1m(qaMTPuDk_HUL^;6tK19M_bzkypiq6wfj z2s+X)e2n6@UrboUhxz&|%vk17OrPkZ8Z?F;BymXel_=3*i%bc!Q$)rp*9N}kbWFPG z<&@%%t9j)PthOoaK@r%|Q8f~U`O_39_M~p*=0AaMdhHo|-mlhg00et+Owf>Dk^2`| z{=zW-e*{2IAm3k*(|*OAxv3kwhP#Q|zbr)B(Z!zq&*fhR!^tio;pj!7%keraFDHS{xJ9Vf0M7w-m+IQe#?h`Ae!u{6DDA$@3rS59H+K{kt09AqtLEa@g$+ zy#e7f#WKt;RF_%Fw;#SPkCmYrFnHS5oqWX`_82ocWcztL+F?j*q?ZO~9b4-`UbY`8 z<9{(YxcPtp`y$kIbn;S6E|}sS<`ID7#FknEk7`4g435?4MSxOx`+&ZAfmH<+f9qfRiX^3Tm3Rj@18iz1su3R z)>yO*iKw&!1F?49MJ7$CaBqMSEi9jvfjczY*)z}gP3wtjQnp?1nZ>a35cb+-z{g+) zs;G;{yHshaw$u9e_w}s!SbhmyF?dt`&=9o>9V_Ovs%|cvuN6loSWOkdCepea!&E;{ zn|1n}?Kn(vHPlIoq+hO_-gpXfqh`5|OKY=*k82Q!h2;rnteOQGvrlxc3>FfiQUkL( zcxiGYgW*6?8PV5aH>|&4x!DlIq8gppJW50(2)T<$JL_T4o}(;QPZKUlVMC3udndRE zOxSBmx2(8H^xG~|C^!<8xV)eki_~dIPp9u zC`t1zF^H;I6jLkhBiRLxVrEfsNfr(zCNt7zt~E>^8-E(X90`%#H4P_?xf~+6mfI-`~icdI_?sct!|4Dmm41 zyu)GVrfiu0L9M*JeTE;D)d?tOSX|8TJW0Gyc3qR1)-e^Karj(xFT8*!IrMD3MYcDv zyyAM{%2@bRGrd@x1p?FU;%D&pYbd1U4!-!Dtm%Y%nHp6D*-lxRLJ0(f7x(8QM3*=9 z+74{nP`X^qzhl0rq5~R5#EUyc>Ay6Z_EWk1%6657!xq4ydF)boTTa7ydF}|+{b_;l z^2UBH13&WeTY8Uun}2a6{ck<{`s4f9WJ+T*Q-2>nOVZtKFDDaa!52+LI8k`VihA7d z={Wt*5T=$x?0ZoB`g`4{hF}|a;VEyqg1~P(bK1;?EH&+7<}5RYqJF;X$vlb&;HLDH zixo{^&>@Ims7vW$r8)HZ=Ph*jBX7jAp6pud!g3XUHwm%#<2N&%(-K*y6+3W)d);#P zm&IhxyiZ)LzbsLu_+6%?R-{&LKd?}nqhsLA>tMOZb^z|&5NGVG{MKQ~Yu$6rV}8;a zwkO_nMDB$wBtM;zS0T*J7@!0CsQ?XY>ucZ?YRAumS*AKO#zj9b3KvBTPH;1)E!Jm4 z6%*b=%I^*<_~zeTJN?@H z16xRLGvt3W*De_8xfY^PMYw7v8eKMK2*z9L$xBP~djjVk{lR(H7u(%M`nAL@EaW9~ zB|2|A@7wFd<45Ce{TeCLZLlOTXL@1y6^HMd4Qki+KgrF8wSihe_P$i#Zx|=kxLEDH z!nue^b4pi<47oop-hrtFdV0%KYI#K=>&C zwA7O${4zd`)@n*a)&+7yBFpYP=+%TQ6^Ao9GUHX-MWyz0-8GUu4XUVsbW^~+R=eTk zH|daC+3$HgeBjqE(7Htl?dXpM6^zzHB){V03;L5sh<&=8aR}A@GZcpfsMSLg#pr6C zlTAQlXpm%2Xd@$8=Y31Eyaem5njsx&e^Kv7YEhLHu42QF=jDBfiqJa)D%2AaCvKN} zjAz7j#G>+`qUWFJwxB`O48IQAX4G15XT^52tgqG4$B*Z?>|F`DS2k@C9&mA&^@`H z9(qWJ-?NsU*;n~|cs8vQ#G>s=)4cqRt}MkEoPfFCiW9$lF?On-t@QpwwOU8pgIzCO z;`=;ME1E+u9c+=Hq}wzXX&?%nTHAH}FF5+dp(7h~!_vKKrtHn?%$K4vmEiDQ|DWr= zpJ`3sm_(>2y)lVRN{lWK&11>+Iv#x_76=wXJtu z;m@H&`G$8M3p0c%6Ae!#9HdgO-^MMEHsdy=clX~dcU=w#05<5}t^Ct^{HM_@|Nmq( ziz?Yc@xrmU)Ju~Q-K44NyX2^fX=nH7w}l>KPP~#~L$HoYjfr^1m(ce^q$ zR=pVSRu(IWxrlx_BSs69`_`)4zzwi$>wB7B#v_^Q`*sPC<{TdBh4DmKK9pW0P;;IL zg}lSyK28v(bvAv7004?4{L=ym{Ab}TFDLik5V8DSN8fRUAJ30H?!_tR6C0lK9b&>=Jpt_Hmyr{Oj0}T0Y=dW9(22nOB-jX?-jL}}p!Vk9k4YbRD^)Puudd&X zm6{;kH5c<;x!;tAD0mIBS48erFFwLhygYG`;Kvoi8V7PNVbtzCTz`(+rQh16$J(af z+Ahi$6+nM%g3*w|)F3d0LXwVhjrzgdwF2iC*b}QO$1RSk@SO)Suu^%w&;s}IRJ4Li z_9}8h<%Qa5Ep?u}ymwTF@e3-a_-L)iXMxkaI9c`Fq(!LEF^@Jz4488O(zKpcA9tCE zY*fC94t>N`*Coi}GtO7c86DfR*Z}?Za{IYiganv$>)2?k5Ps#_WT4~#jZ`qwckCR8 z$@;w`EnN|^!5HoRc;~XX$>Cb2&OS)s8_m>AJsq7g+sH$Ja1&FuYNAn;vqYexI&~p` z|EOg%1V473=+yb0k0dlsBIegRGfJv14wH!4ProI9DS;a@U_QgRg~Lio*s@Bg;z=v% zQL-%=#PZFYH+961=0_9dV-%v&lg&@3MUGvHXLlR9pn1-0ZK69AZpc0Y`Xy$a<%AGG7nQR8}`ZR71(!8+(8>w_U ztnY4R@Z!=@3WGnLK{C;Wi7cG>8q>$&@7!)-q}QliZRNDhoGWR8_$QH3X=-Ur7d~SC z2BxW0!~FgCSwybKw?w7{BGB^$XC+10XZY;IGDbZXH@1~*kv}SUsd&FKPJcSbC^j&; zvgwyko5%vcnO`oCfg$)#4fZ+6gOMA<_f;PpBdNGQ?1q;
be=4P6V`WDZ^XyB=S3nMM+S?Mz49u+M3fg3#0L1sjY$;TUhzu6@!gdv8pgz2#2O=GNB!m=`iNiVjXE12`!x=buq99uHkvn{OCi6wJ^;+F&j{E z#QL7uzv2ivG|~iI8!3uBm~Nrp_NK2nm3=>Q(&`=My#;XC;z_UjivzyW6ZB3=7W?qt zsQ(=Ri<6DL9CE7VIy)S{Xw&#MSKjOiO+-=CU8y1|fzy`k^3z+pWc$&gvW zAI6DI{S^(U*^Sam&tFy;eD$F6POu=-0=+aiy}13szZLm@!FW}omIc+O!6y5qvcQ~h zx(k`d4!R6~Rlx4VeJir*KRj}S>~PlBA{a09!$&)m`yvY1XIGKNwX?6J%fk1Jw~g?P zAD2de+OTjPO0!IKv6-*Yh&p=L4c}QVUNL;Wg3J@2PB_Vye?zo|RX-$tz0sU|I+M(b z_E%ASn24*LwxuG8UGrxS8pHwZwl#oO2iX0N05e$^Q*Hjo;nZytZD#1o0*rmch}6LB zUT(`SEq`vryL0WDApY8%AR8v#u%zucywD1$ zg^SdE>bcRqDr*VG{!|g4bSX`(@&`JOES(PX>Dg#y_iAo|RMaXddsz6f6q5zkWBqa#VrMP8r5Q+@4WRCRhVxHQe|sSqsgLF|~ndr=D&WZDc2{>8(0Er`vF6 zTeekf*&|%^5)vfmxH{DM@`T6Ab5F*ewh7}D#4*!D93g5(_N&u_+r3=_{6jDZ+cX$U zbz*_b&jlOMO*~r1CB?>?V=s%D5aMw=m^k#J&z(n&M#CJZ*};)*Ini`IQ&0NoUDwP} zGCfPPPD%q`XD5iMs}-Fq>mu}=jPRQ}%fTk2^?){zXoUm$JtL~rQ)A>)wZ*T#E15_hg1!2KLwd3Xx)p~{MD1N(*=!4A3SVLnS3a3VR{J|dS2Wqze`Ly}P{i1f z*}y}KQu(O|m_tfXPjg?Vjo+s@OZy~}Zsh)C0o*oy9;ROD5d5H0}{-Pl|^d;Kws@Ji0k)^>782ks%1 zh%}6IB`(@E0R*rIt5)L+nV1}B4i7(eyDn~AO-OC@`NHblH&x%L=a$k)Ahr1Z8^g}m ztT1y~l=ZcinUAL5U73r~8_E%uZS+Z489)&UKEDi7gXn^DqTewT$l5sMMtc@`=(|tB zH@U~pmSZe0r?UxG^W+)Lkaovy4y`xcDx9|FhWGw&%Lztos6<$vel;EISJSaUb(O3W zY$d~tIbEd1OOfm|5z2)F3an-fh!C%w8ceBLpkSs^lnk!e_*Shgm^58CN8{$&csg`| z+x_5(kxyl*>=9gumg8xvGQ)6}m&o11Jl20#O^%z_fLZ`q=CW^_D=FQFSU^OZ)u`VF za5?hH;>~42iUX75rbp$cP4=Et4^cM?0D?j-vLpSQ){JfHjOLx!$y3xu!O z@HyGFbqtthUGRq`^UMsIf?HNWS(0h$PXbLuT>44=+KrDMJ zJR5&C{Hu#XrIdBLDA$T=P1oPQ_62u$lx)+KbG+GqQenm5Yf`<^XD}76gws2@p&%Da zo*?rWP=Y5{O6SgsHs)x0n96d$kJ`Fz*_3?{>azB~*xGnO&_-TW`G@Ee_~)6r?&o?m+TRgs~|aeF!Og<^5h%a1ZJ_|3B59EUI&2($N&MsIkB zX?gl05rnTx7X$&v(x9wzznwa-&OpI_qJnv*rJi)mS#bC^9~d=v{MF%$I`?6I#aHs} zWSgcOU`Irr)hw^MU7zFG;vO8JP2}@GssA^k>i?xa2k^iC`9c2IFCoZHWtAh>xQjYH}1%upAFH+}3bUq1`C$gJ?rig@H}*1Jxeh)}G|jt~ zU$1^@>dsbK)#gW^rp&c~7iS7#P=;maa~n$6(i@5hl2njXbQJ@#=fT0fJREa(H4^Bv zzTlmB927X7^-k=N;9%$P`IbPRh0Mil4^rB7)=dq{4vZbOm^1VWx+TRdtMB})T&pAcbh7}R#wi?BW^BxnJrbQh}qDo6W zJ5Rx5A{$S`>s1fiMHke>0naYK2>9a1@1n8pmh7-GKR?MZPC@m%Rwk3NJXxe8_*%n4 zK@R+YdJl~j)q;;b{%prCHS#vys4nQk&bi`*yNGx^@wIaBK`3ttqnhr41UMmtC&tzA ziJ)C!^K@ao8c19;A|cSa7L?j;*QfZFGAg1Q^L91i#Tthq|CJUjDLz8W5Z0rotCV3b z+{Cj*N{rUm5yWo+HLVwYRgKo%*h|iK15F#OwX4rsmDM1f@TX?$g(u8R8*rsq` z7S>dPGc5*Whby%*pf>sJFE_<9pt5*olmnBJ%e)2T?EDSwOQjlW5(51#>)a*;m>Gxo znMIX~4e&@xQqIfL>X8$DpQ2m5&*aQdlf-n~M?B_v>2=YFwKM<@U%5O%hq4@8b!d zmR_Rsf&fUfyRp}#BbZa?*gvfAFtg9Zt!N-0Ek@Jvh1+sP3LNl#q|ep6cu@(WMY_^- zPHE@#$>)LVt{R(MFqx`km{kguEaAG53jc;6Ys80)v|KXkOb_X-6Z!t!`n-4q`OU)K zvhF0*V!&;i_-WNGo;`r@htdK>g1)s^_hfmEp_d;c?gt=_ORHnTiLlH43Jrk&+Fh@ zjMG_M_^Z#`Y-^BQ1;I=G8Ew!0E%c607cI9Dm{R|FU%jR_s>qDA-lR6ySRC|m|LJB^K)`> Oq9N1LN~uVrA^$Hs*h;1V literal 0 HcmV?d00001 diff --git a/cypress/e2e/create-plan.cy.ts b/cypress/e2e/create-plan.cy.ts index 1a940b73c..f7f3db0fe 100644 --- a/cypress/e2e/create-plan.cy.ts +++ b/cypress/e2e/create-plan.cy.ts @@ -7,12 +7,64 @@ describe('Plan creation flow', () => { const planName = "Mr. Bob's Plan"; it('Create blank plan', () => { + // Open add plan dropdown + cy.task('log', 'Opening blank plan modal...'); + cy.dataTestId('add-new-plan-btn').click(); + cy.dataTestId('add-blank-plan-btn').click(); + + // Modal should be visible + cy.dataTestId('create-blank-plan-page').then(($el) => Cypress.dom.isVisible($el)); + + // Fill out plan creation form + cy.task('log', 'Filling out plan creation form...'); + cy.dataTestId('plan-name-input').type(planName); + cy.dataTestId('major-autocomplete').type('Computer'); + cy.getDropdownOptions() + .contains('Computer Science') + .then(($el) => { + cy.wrap($el.get(0).innerText).as('major'); + $el.click(); + }); + + // Create plan without upload transcript + cy.task('log', 'Creating plan...'); + // cy.dataTestId('next-btn').click(); + cy.dataTestId('create-plan-btn').click(); + + // Wait and verify redirect to plan + cy.task('log', 'Verifying redirect...'); + cy.url({ timeout: 20000 }).should('include', '/app/plans/'); + + // Check plan information + cy.task('log', 'Verifying plan information'); + cy.get('@major').then((majorAlias) => { + // Check plan title + cy.dataTestId('plan-title') + .then(($el) => $el.text()) + .should('eq', planName); + + // Check plan major + const major = `${majorAlias}`; // Whack workaround + cy.dataTestId('plan-major') + .then(($el) => $el.text()) + .should('eq', major); + }); + }); +}); + +describe('Plan creation flow', () => { + before('Setup', () => { + cy.resetDbAndLogin(); + cy.visit('/app/home'); + }); + + const planName = "Mr. Bob's Plan"; + + it('Create custom plan', () => { // Open add plan dropdown cy.task('log', 'Opening custom plan modal...'); cy.dataTestId('add-new-plan-btn').click(); cy.dataTestId('add-custom-plan-btn').click(); - cy.dataTestId('add-transcript-plan-btn').click(); - cy.dataTestId('add-template-plan-btn').click(); // Modal should be visible cy.dataTestId('create-custom-plan-page').then(($el) => Cypress.dom.isVisible($el)); @@ -31,6 +83,64 @@ describe('Plan creation flow', () => { // Create plan without upload transcript cy.task('log', 'Creating plan...'); cy.dataTestId('next-btn').click(); + // cy.dataTestId('create-plan-btn').click(); + cy.dataTestId('upload-transcript-btn').click(); + + cy.get('input[type=file]').selectFile('cypress/data/dummytranscript.pdf', { force: true }); + cy.dataTestId('create-plan-btn').click(); + + // Wait and verify redirect to plan + cy.task('log', 'Verifying redirect...'); + cy.url({ timeout: 20000 }).should('include', '/app/plans/'); + + // Check plan information + cy.task('log', 'Verifying plan information'); + cy.get('@major').then((majorAlias) => { + // Check plan title + cy.dataTestId('plan-title') + .then(($el) => $el.text()) + .should('eq', planName); + + // Check plan major + const major = `${majorAlias}`; // Whack workaround + cy.dataTestId('plan-major') + .then(($el) => $el.text()) + .should('eq', major); + }); + }); +}); + +describe('Plan creation flow', () => { + before('Setup', () => { + cy.resetDbAndLogin(); + cy.visit('/app/home'); + }); + + const planName = "Mr. Bob's Plan"; + + it('Create template plan', () => { + // Open add plan dropdown + cy.task('log', 'Opening template plan modal...'); + cy.dataTestId('add-new-plan-btn').click(); + cy.dataTestId('add-template-plan-btn').click(); + + // Modal should be visible + cy.dataTestId('create-template-plan-page').then(($el) => Cypress.dom.isVisible($el)); + + // Fill out plan creation form + cy.task('log', 'Filling out plan creation form...'); + cy.dataTestId('plan-name-input').type(planName); + cy.dataTestId('major-autocomplete').type('Computer'); + cy.getDropdownOptions() + .contains('Computer Science') + .then(($el) => { + cy.wrap($el.get(0).innerText).as('major'); + $el.click(); + }); + + // Create plan without upload transcript + cy.task('log', 'Creating plan...'); + // cy.dataTestId('next-btn').click(); cy.dataTestId('create-plan-btn').click(); // Wait and verify redirect to plan diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx index 50c2cc648..bb2e05022 100644 --- a/src/components/home/Home.tsx +++ b/src/components/home/Home.tsx @@ -124,7 +124,7 @@ export default function PlansPage(): JSX.Element { { setPlanPage(0); @@ -137,7 +137,7 @@ export default function PlansPage(): JSX.Element { { setPlanPage(1); diff --git a/src/components/template/CustomPlan.tsx b/src/components/template/CustomPlan.tsx index 42a200ca4..57babf9a2 100644 --- a/src/components/template/CustomPlan.tsx +++ b/src/components/template/CustomPlan.tsx @@ -323,6 +323,7 @@ export default function CustomPlan({ onDismiss }: { onDismiss: () => void }) { ref={dropRef} className="group flex flex-col items-center justify-center gap-0.5 rounded-md border border-neutral-200 bg-inherit py-10 transition-colors" onClick={() => fileInputRef.current && fileInputRef.current.click()} + data-testid="upload-transcript-btn" > void }) { // TODO(https://nebula-labs.atlassian.net/browse/NP-85): Refactor parseTranscript. return ( void }) { }; return ( void }) { }, color: 'primary', loading, + 'data-testid': 'create-plan-btn', }, ]} >

Plan Name

void }) {

Search degree template

setMajor(value)} From 4f97a5269cb90a651130208ba95d98111237725b Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Sat, 16 Sep 2023 20:07:57 -0500 Subject: [PATCH 07/17] cypress fixes --- cypress/e2e/create-plan.cy.ts | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/cypress/e2e/create-plan.cy.ts b/cypress/e2e/create-plan.cy.ts index f7f3db0fe..bb2f4ba98 100644 --- a/cypress/e2e/create-plan.cy.ts +++ b/cypress/e2e/create-plan.cy.ts @@ -1,5 +1,5 @@ describe('Plan creation flow', () => { - before('Setup', () => { + beforeEach('Setup', () => { cy.resetDbAndLogin(); cy.visit('/app/home'); }); @@ -28,7 +28,6 @@ describe('Plan creation flow', () => { // Create plan without upload transcript cy.task('log', 'Creating plan...'); - // cy.dataTestId('next-btn').click(); cy.dataTestId('create-plan-btn').click(); // Wait and verify redirect to plan @@ -50,15 +49,6 @@ describe('Plan creation flow', () => { .should('eq', major); }); }); -}); - -describe('Plan creation flow', () => { - before('Setup', () => { - cy.resetDbAndLogin(); - cy.visit('/app/home'); - }); - - const planName = "Mr. Bob's Plan"; it('Create custom plan', () => { // Open add plan dropdown @@ -80,10 +70,9 @@ describe('Plan creation flow', () => { $el.click(); }); - // Create plan without upload transcript + // Create plan with uploading transcript cy.task('log', 'Creating plan...'); cy.dataTestId('next-btn').click(); - // cy.dataTestId('create-plan-btn').click(); cy.dataTestId('upload-transcript-btn').click(); cy.get('input[type=file]').selectFile('cypress/data/dummytranscript.pdf', { force: true }); @@ -108,15 +97,6 @@ describe('Plan creation flow', () => { .should('eq', major); }); }); -}); - -describe('Plan creation flow', () => { - before('Setup', () => { - cy.resetDbAndLogin(); - cy.visit('/app/home'); - }); - - const planName = "Mr. Bob's Plan"; it('Create template plan', () => { // Open add plan dropdown @@ -138,9 +118,8 @@ describe('Plan creation flow', () => { $el.click(); }); - // Create plan without upload transcript + // Create template plan without upload transcript cy.task('log', 'Creating plan...'); - // cy.dataTestId('next-btn').click(); cy.dataTestId('create-plan-btn').click(); // Wait and verify redirect to plan From f049a1f478ec46aa37d1777d6fc4ad3f8f33eed2 Mon Sep 17 00:00:00 2001 From: Kamui Date: Mon, 18 Sep 2023 22:10:54 -0500 Subject: [PATCH 08/17] chore: pre-check if validator is running --- .editorconfig | 5 ++ next.config.js | 84 ++++++++++++++++--------- package-lock.json | 153 +++++++++++++++++++++++++++++++++++++++++----- package.json | 1 + validator/api.py | 5 ++ 5 files changed, 205 insertions(+), 43 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..0020fc03a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*] +indent_style = space +indent_size = 2 diff --git a/next.config.js b/next.config.js index 2b1c2ec1d..dc2914127 100644 --- a/next.config.js +++ b/next.config.js @@ -1,38 +1,64 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const { withSentryConfig } = require('@sentry/nextjs'); +const fetch = require('node-fetch'); /* eslint-disable @typescript-eslint/no-var-requires */ -const withBundleAnalyzer = require('@next/bundle-analyzer')({ - enabled: process.env.ANALYZE === 'true', -}); +const checkValidatorAvailability = async () => { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_VALIDATOR}/health`); + if (response.ok) { + return true; + } else { + return false; + } + } catch (error) { + return false; + } +}; -const nextConfig = withBundleAnalyzer({ - modularizeImports: { - '@mui/material': { - transform: '@mui/material/{{member}}', +module.exports = async (phase) => { + if (phase === 'phase-development-server') { + const isValidatorReachable = await checkValidatorAvailability(); + + if (!isValidatorReachable) { + console.error('Start validator server first before running next dev server.'); + process.exit(1); + } + } + + const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', + }); + + const nextConfig = withBundleAnalyzer({ + modularizeImports: { + '@mui/material': { + transform: '@mui/material/{{member}}', + }, + '@mui/icons-material': { + transform: '@mui/icons-material/{{member}}', + }, }, - '@mui/icons-material': { - transform: '@mui/icons-material/{{member}}', + compiler: { + removeConsole: process.env.NODE_ENV === 'production', }, - }, - compiler: { - removeConsole: process.env.NODE_ENV === 'production', - }, - rewrites: async () => { - return [ - { - source: '/', - destination: '/index.html', - }, - ]; - }, -}); + rewrites: async () => { + return [ + { + source: '/', + destination: '/index.html', + }, + ]; + }, + }); -module.exports = withSentryConfig( - nextConfig, - { silent: true }, - // tunnelRoute set to bypass adblockers. - // See: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-tunneling-to-avoid-ad-blockers. - { hideSourcemaps: false, tunnelRoute: '/sentry-tunnel' }, -); + const sentryConfig = withSentryConfig( + nextConfig, + { silent: true }, + // tunnelRoute set to bypass adblockers. + // See: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-tunneling-to-avoid-ad-blockers. + { hideSourcemaps: false, tunnelRoute: '/sentry-tunnel' }, + ); + return sentryConfig; +}; diff --git a/package-lock.json b/package-lock.json index f3b52de22..7857b3006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.6.0", "jest": "^29.6.1", + "node-fetch": "^3.3.2", "postcss": "^8.4.6", "prettier": "^2.3.2", "prettier-plugin-tailwindcss": "^0.2.2", @@ -1572,6 +1573,26 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@motionone/animation": { "version": "10.14.0", "license": "MIT", @@ -3282,6 +3303,25 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/@sentry/cli/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@sentry/cli/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -6447,6 +6487,25 @@ "node-fetch": "2.6.7" } }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "license": "MIT", @@ -6622,6 +6681,15 @@ "node": ">=0.10" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/dayjs": { "version": "1.11.7", "dev": true, @@ -8280,6 +8348,29 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/figures": { "version": "3.2.0", "dev": true, @@ -8561,6 +8652,18 @@ "node": ">= 0.12" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "dev": true, @@ -12029,22 +12132,41 @@ "node": ">= 0.10.5" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { - "version": "2.6.7", - "license": "MIT", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/node-forge": { @@ -15638,7 +15760,8 @@ }, "node_modules/tr46": { "version": "0.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/trough": { "version": "1.0.5", @@ -16426,7 +16549,8 @@ }, "node_modules/webidl-conversions": { "version": "3.0.1", - "license": "BSD-2-Clause" + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { "version": "5.76.1", @@ -16751,7 +16875,8 @@ }, "node_modules/whatwg-url": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index 4a3d775fe..7f607916b 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.6.0", "jest": "^29.6.1", + "node-fetch": "^3.3.2", "postcss": "^8.4.6", "prettier": "^2.3.2", "prettier-plugin-tailwindcss": "^0.2.2", diff --git a/validator/api.py b/validator/api.py index 2c4de1c54..f76aece24 100644 --- a/validator/api.py +++ b/validator/api.py @@ -80,3 +80,8 @@ def test_validate() -> Response: }, 500, ) + + +@app.route("/health") +def health(): + return make_response({"ok": True}, 200) From e40af1fef7b2b9d8b09235dab57808ac39303930 Mon Sep 17 00:00:00 2001 From: Kamui Date: Tue, 19 Sep 2023 11:35:22 -0500 Subject: [PATCH 09/17] chore: remove editorconfig --- .editorconfig | 5 ----- .gitignore | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 0020fc03a..000000000 --- a/.editorconfig +++ /dev/null @@ -1,5 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 diff --git a/.gitignore b/.gitignore index fab54eac8..46a3785b0 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ yarn-error.log* # See docs/ide-config.md for more information. .idea .vs/ +.editorconfig prisma/generated From 34b2c4a1266100de7e0ccc7222248b879f4bb7ee Mon Sep 17 00:00:00 2001 From: Kamui Date: Tue, 19 Sep 2023 11:37:26 -0500 Subject: [PATCH 10/17] fix: add mypy type for health route --- validator/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator/api.py b/validator/api.py index f76aece24..8e5f023bd 100644 --- a/validator/api.py +++ b/validator/api.py @@ -83,5 +83,5 @@ def test_validate() -> Response: @app.route("/health") -def health(): +def health() -> Response: return make_response({"ok": True}, 200) From 9a2cce1be436ab7a302f4ac4aac425758cfab14b Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Wed, 20 Sep 2023 01:52:27 -0500 Subject: [PATCH 11/17] fixes np-36 --- .../planner/Tiles/SemesterCourseItem.tsx | 104 ++++++++++-------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/src/components/planner/Tiles/SemesterCourseItem.tsx b/src/components/planner/Tiles/SemesterCourseItem.tsx index 6cb958c26..a0a44ceaa 100644 --- a/src/components/planner/Tiles/SemesterCourseItem.tsx +++ b/src/components/planner/Tiles/SemesterCourseItem.tsx @@ -72,17 +72,18 @@ export const MemoizedSemesterCourseItem = React.memo( !isValid && !course.prereqOveridden ? 'bg-[#FEFBED]' : 'bg-[#FFFFFF]' } ${ !course.locked || isValid || isValid === undefined - ? course.locked - ? 'bg-neutral-200' + ? course.locked || semesterLocked + ? 'cursor-default bg-neutral-200' : 'bg-inherit' - : 'bg-[#FFFBEB]' + : 'cursor-default bg-neutral-200' } ${semesterLocked || course.locked ? 'text-neutral-400' : 'text-[#1C2A6D]'}`} onClick={() => { // Don't open if user is hovering over course info - setDropdownOpen(true); + if (!course.locked && !semesterLocked) setDropdownOpen(true); }} onMouseEnter={() => { - if (!dropdownOpen) hoverTimer.current = setTimeout(() => setHoverOpen(true), 500); + if (!dropdownOpen && !course.locked && !semesterLocked) + hoverTimer.current = setTimeout(() => setHoverOpen(true), 500); setHoverEllipse(true); }} onMouseLeave={() => { @@ -109,21 +110,25 @@ export const MemoizedSemesterCourseItem = React.memo( >
- e.stopPropagation()} - onCheckedChange={(checked) => { - if (checked && onSelectCourse) { - onSelectCourse(); - } + {course.locked || semesterLocked ? ( + + ) : ( + e.stopPropagation()} + onCheckedChange={(checked) => { + if (checked && onSelectCourse) { + onSelectCourse(); + } - if (!checked && onDeselectCourse) { - onDeselectCourse(); - } - }} - /> + if (!checked && onDeselectCourse) { + onDeselectCourse(); + } + }} + /> + )}
{course.code} @@ -141,39 +146,44 @@ export const MemoizedSemesterCourseItem = React.memo( )} - {course.locked && } {title}
- { - if (hoverOpen) { - setHoverOpen(false); + {!semesterLocked && ( + { + if (hoverOpen) { + setHoverOpen(false); + } + if (!open) setHoverEllipse(false); + setDropdownOpen(open); + }} + locked={course.locked} + onPrereqOverrideChange={() => + onPrereqOverrideChange && onPrereqOverrideChange(!course.prereqOveridden) } - if (!open) setHoverEllipse(false); - setDropdownOpen(open); - }} - locked={course.locked} - onPrereqOverrideChange={() => - onPrereqOverrideChange && onPrereqOverrideChange(!course.prereqOveridden) - } - isValid={isValid} - prereqOverriden={course.prereqOveridden} - semesterLocked={semesterLocked || false} - toggleLock={() => onLockChange && onLockChange(!course.locked)} - changeColor={(color) => onColorChange && onColorChange(color)} - deleteCourse={() => onDeleteCourse && onDeleteCourse()} - > -
setDropdownOpen(true)} + isValid={isValid} + prereqOverriden={course.prereqOveridden} + semesterLocked={semesterLocked || false} + toggleLock={() => onLockChange && onLockChange(!course.locked)} + changeColor={(color) => onColorChange && onColorChange(color)} + deleteCourse={() => onDeleteCourse && onDeleteCourse()} > - -
-
+
setDropdownOpen(true)} + > + {!semesterLocked && ( + + )} +
+
+ )}
From e3d78a621af3df790f8982780718a214ef20f101 Mon Sep 17 00:00:00 2001 From: Saidarsh Date: Thu, 21 Sep 2023 00:18:09 -0500 Subject: [PATCH 12/17] semester lock prereq warning disabled --- src/components/planner/Tiles/SemesterCourseItem.tsx | 4 ++-- src/components/planner/Tiles/SemesterCourseItemDropdown.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/planner/Tiles/SemesterCourseItem.tsx b/src/components/planner/Tiles/SemesterCourseItem.tsx index a0a44ceaa..fd24420f6 100644 --- a/src/components/planner/Tiles/SemesterCourseItem.tsx +++ b/src/components/planner/Tiles/SemesterCourseItem.tsx @@ -132,7 +132,7 @@ export const MemoizedSemesterCourseItem = React.memo(
{course.code} - {!isValid && !course.prereqOveridden && ( + {!isValid && !course.prereqOveridden && !course.locked && ( setPrereqWarnOpen(false)}> - + {!semesterLocked && } )} diff --git a/src/components/planner/Tiles/SemesterCourseItemDropdown.tsx b/src/components/planner/Tiles/SemesterCourseItemDropdown.tsx index 8426ae98e..e9d0ae91b 100644 --- a/src/components/planner/Tiles/SemesterCourseItemDropdown.tsx +++ b/src/components/planner/Tiles/SemesterCourseItemDropdown.tsx @@ -68,7 +68,7 @@ const SemesterCourseItemDropdown: FC = ({ Delete - {!isValid && ( + {!isValid && !locked && ( {prereqOverriden ? 'Show Pre-reqs Warning' : 'Dismiss Pre-reqs Warning'} From 9528a6b1df4f4dbf64b299f35ed359a4baca85d4 Mon Sep 17 00:00:00 2001 From: Kevin Ge Date: Thu, 21 Sep 2023 14:32:40 -0500 Subject: [PATCH 13/17] refactor: properly name sidebar component --- src/components/planner/Planner.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/planner/Planner.tsx b/src/components/planner/Planner.tsx index 92418d2c3..1bc853e1a 100644 --- a/src/components/planner/Planner.tsx +++ b/src/components/planner/Planner.tsx @@ -27,7 +27,7 @@ import { trpc } from '@/utils/trpc'; import PlannerMouseSensor from './PlannerMouseSensor'; import SelectedCoursesToast from './SelectedCoursesToast'; import { useSemestersContext } from './SemesterContext'; -import CourseSelectorContainer from './Sidebar/Sidebar'; +import Sidebar from './Sidebar/Sidebar'; import { SidebarCourseItem } from './Sidebar/SidebarCourseItem'; import { SemesterCourseItem } from './Tiles/SemesterCourseItem'; import DroppableSemesterTile from './Tiles/SemesterTile'; @@ -212,7 +212,7 @@ export default function Planner({
- Date: Sun, 24 Sep 2023 13:40:35 -0500 Subject: [PATCH 14/17] fix: mass delete should not loop through each element --- src/components/planner/SemesterContext.tsx | 92 +++++++++++++--------- src/server/trpc/router/plan.ts | 17 ++++ 2 files changed, 72 insertions(+), 37 deletions(-) diff --git a/src/components/planner/SemesterContext.tsx b/src/components/planner/SemesterContext.tsx index afcf3a03e..04061a2b6 100644 --- a/src/components/planner/SemesterContext.tsx +++ b/src/components/planner/SemesterContext.tsx @@ -1,5 +1,14 @@ import { SemesterType } from '@prisma/client'; -import { createContext, FC, useContext, useEffect, useMemo, useReducer, useState } from 'react'; +import { + createContext, + FC, + useCallback, + useContext, + useEffect, + useMemo, + useReducer, + useState, +} from 'react'; import { toast } from 'react-toastify'; import { trpc } from '@/utils/trpc'; @@ -97,6 +106,7 @@ export type SemestersReducerAction = } | { type: 'removeYear' } | { type: 'removeCourseFromSemester'; semesterId: string; courseId: string } + | { type: 'massDeleteCoursesFromSemester'; courseIds: string[] } | { type: 'moveCourseFromSemesterToSemester'; originSemesterId: string; @@ -166,42 +176,6 @@ export const SemestersContextProvider: FC = ({ }); }; - const handleDeleteAllSelectedCourses = () => { - for (const semester of semesters) { - for (const { id, code } of semester.courses) { - const courseId = id.toString(); - - if (selectedCourseIds.has(courseId)) { - dispatchSemesters({ - type: 'removeCourseFromSemester', - courseId, - semesterId: semester.id.toString(), - }); - addTask({ - func: ({ semesterId, courseName }) => - toast - .promise( - removeCourse.mutateAsync({ planId, semesterId, courseName }), - { - pending: 'Removing course ' + courseName + '...', - success: 'Removed course ' + courseName + '!', - error: 'Error in removing ' + courseName, - }, - { - autoClose: 1000, - position: 'bottom-right', - }, - ) - .catch((err) => console.error(err)), - args: { semesterId: semester.id.toString(), courseName: code }, - }); - } - } - } - - setSelectedCourseIds(new Set()); - }; - const courseIsSelected = (courseId: string): boolean => selectedCourseIds.has(courseId); const handleDeselectAllCourses = () => setSelectedCourseIds(new Set()); @@ -242,6 +216,16 @@ export const SemestersContextProvider: FC = ({ : semester; }); + case 'massDeleteCoursesFromSemester': + return state.map((semester) => { + return { + ...semester, + courses: semester.courses.filter( + (course) => !action.courseIds.includes(course.id.toString()), + ), + }; + }); + case 'moveCourseFromSemesterToSemester': return state.map((semester) => { if (semester.id.toString() === action.destinationSemesterId) { @@ -407,6 +391,13 @@ export const SemestersContextProvider: FC = ({ }, }); + const massDeleteCourses = trpc.plan.massDeleteCourses.useMutation({ + async onSuccess() { + await utils.validator.degreeValidator.invalidate(); + await utils.validator.prereqValidator.invalidate(); + }, + }); + const moveCourse = trpc.plan.moveCourseFromSemester.useMutation({ async onSuccess() { utils.validator.prereqValidator.invalidate(); @@ -427,6 +418,33 @@ export const SemestersContextProvider: FC = ({ const semesterColorChange = trpc.plan.changeSemesterColor.useMutation(); + const handleDeleteAllSelectedCourses = useCallback(() => { + const coursesToDelete = [...selectedCourseIds]; + + dispatchSemesters({ + type: 'massDeleteCoursesFromSemester', + courseIds: coursesToDelete, + }); + setSelectedCourseIds(new Set()); + + addTask({ + func: ({ courseIds }) => + toast.promise( + massDeleteCourses.mutateAsync({ courseIds }), + { + pending: 'Removing selected courses', + success: 'Removed course selected courses!', + error: 'Error in removing selected courses', + }, + { + autoClose: 1000, + position: 'bottom-right', + }, + ), + args: { courseIds: coursesToDelete }, + }); + }, [selectedCourseIds, addTask, massDeleteCourses]); + const handleDeleteAllCoursesFromSemester = (semester: Semester) => { handleDeselectCourses(semester.courses.map((course) => course.id.toString())); diff --git a/src/server/trpc/router/plan.ts b/src/server/trpc/router/plan.ts index 602fc65dd..9097fc3cb 100644 --- a/src/server/trpc/router/plan.ts +++ b/src/server/trpc/router/plan.ts @@ -290,6 +290,23 @@ export const planRouter = router({ return false; } }), + massDeleteCourses: protectedProcedure + .input(z.object({ courseIds: z.array(z.string()) })) + .mutation(async ({ ctx, input }) => { + try { + await ctx.prisma.course.deleteMany({ + where: { + id: { in: input.courseIds }, + }, + }); + } catch (error) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + cause: error, + message: 'Faild to mass delete courses: ' + error, + }); + } + }), // Protected route: route uses session user id deleteAllCoursesFromSemester: protectedProcedure .input(z.object({ semesterId: z.string() })) From 51359353cb397087fe37884c07157a74a7eeaac2 Mon Sep 17 00:00:00 2001 From: Renny Hoang Date: Mon, 25 Sep 2023 12:05:57 -0500 Subject: [PATCH 15/17] Impelement Skeleton UI in Sidebar/Course Item --- package-lock.json | 9 ++++ package.json | 1 + .../planner/Sidebar/AccordionSkeleton.tsx | 16 ++++++ src/components/planner/Sidebar/Sidebar.tsx | 50 ++++++++----------- .../planner/Tiles/SemesterCourseItem.tsx | 19 ++++--- src/components/planner/Toolbar/Toolbar.tsx | 16 +++--- src/server/db/platform_client.ts | 2 +- 7 files changed, 69 insertions(+), 44 deletions(-) create mode 100644 src/components/planner/Sidebar/AccordionSkeleton.tsx diff --git a/package-lock.json b/package-lock.json index 7857b3006..d83ee4254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "prettier-eslint": "^13.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-loading-skeleton": "^3.3.1", "react-toastify": "^9.1.1", "superjson": "^1.11.0", "tss-react": "^3.3.6", @@ -13864,6 +13865,14 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/react-loading-skeleton": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.3.1.tgz", + "integrity": "sha512-NilqqwMh2v9omN7LteiDloEVpFyMIa0VGqF+ukqp0ncVlYu1sKYbYGX9JEl+GtOT9TKsh04zCHAbavnQ2USldA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-reconciler": { "version": "0.23.0", "license": "MIT", diff --git a/package.json b/package.json index 7f607916b..7f97332de 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "prettier-eslint": "^13.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-loading-skeleton": "^3.3.1", "react-toastify": "^9.1.1", "superjson": "^1.11.0", "tss-react": "^3.3.6", diff --git a/src/components/planner/Sidebar/AccordionSkeleton.tsx b/src/components/planner/Sidebar/AccordionSkeleton.tsx new file mode 100644 index 000000000..a845963ba --- /dev/null +++ b/src/components/planner/Sidebar/AccordionSkeleton.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; + +export default function AccordionSkeleton() { + return ( + <> +
+ +
+
+ +
+ + ); +} diff --git a/src/components/planner/Sidebar/Sidebar.tsx b/src/components/planner/Sidebar/Sidebar.tsx index 56a5a840a..bab93fb7c 100644 --- a/src/components/planner/Sidebar/Sidebar.tsx +++ b/src/components/planner/Sidebar/Sidebar.tsx @@ -1,20 +1,23 @@ import * as Dialog from '@radix-ui/react-dialog'; import { useRef, useState, useMemo, memo } from 'react'; +import Skeleton from 'react-loading-skeleton'; import { v4 as uuidv4 } from 'uuid'; +import AccordionSkeleton from './AccordionSkeleton'; +import DraggableCourseList from './DraggableCourseList'; +import { DegreeRequirement } from './types'; +import { Course, DraggableCourse, GetDragIdByCourse } from '../types'; +import useFuse from '../useFuse'; + import Button from '@/components/Button'; import AnalyticsWrapper from '@/components/common/AnalyticsWrapper'; import RequirementsContainer from '@/components/planner/Sidebar/RequirementsContainer'; import SearchBar from '@/components/planner/Sidebar/SearchBar'; -import Spinner from '@/components/Spinner'; import ChevronIcon from '@/icons/ChevronIcon'; import { trpc } from '@/utils/trpc'; import { getSemesterHourFromCourseCode } from '@/utils/utilFunctions'; -import DraggableCourseList from './DraggableCourseList'; -import { DegreeRequirement } from './types'; -import { Course, DraggableCourse, GetDragIdByCourse } from '../types'; -import useFuse from '../useFuse'; +import 'react-loading-skeleton/dist/skeleton.css'; export interface CourseSelectorContainerProps { planId: string; @@ -96,7 +99,11 @@ function CourseSelectorContainer({ taken >= min ? 'text-primary-800' : 'text-yellow-500' }`} > - {taken}/{min} {unit} + {taken != -1 ? ( + taken + '/' + min + ' ' + unit + ) : ( + + )}
); @@ -128,7 +135,7 @@ function CourseSelectorContainer({
- {validationData && !isValidationLoading && ( + {validationData ? ( + ) : ( + )}
@@ -176,30 +185,15 @@ function CourseSelectorContainer({ className="z-[999]" onOpenAutoFocus={(e) => e.preventDefault()} > - {!isLoading ? ( -
- -
- ) : ( -
- Please wait, courses are loading.... -
- )} +
+ +
)} - {isValidationLoading && ( -
- -
- )} - - {!isValidationLoading && error?.data?.code === 'INTERNAL_SERVER_ERROR' && ( + {error?.data?.code === 'INTERNAL_SERVER_ERROR' && (
It seems like a screw has gone loose! @@ -210,8 +204,7 @@ function CourseSelectorContainer({
)} - {!isValidationLoading && - validationData && + {validationData && validationData.validation.requirements.length > 0 && validationData.validation.requirements.map((req: DegreeRequirement, idx: number) => ( ))} + {!validationData && }
Warning: This is an unofficial tool not diff --git a/src/components/planner/Tiles/SemesterCourseItem.tsx b/src/components/planner/Tiles/SemesterCourseItem.tsx index fd24420f6..ff75af7ab 100644 --- a/src/components/planner/Tiles/SemesterCourseItem.tsx +++ b/src/components/planner/Tiles/SemesterCourseItem.tsx @@ -1,12 +1,7 @@ import { UniqueIdentifier, useDraggable } from '@dnd-kit/core'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import React, { ComponentPropsWithoutRef, FC, forwardRef, useState, useRef } from 'react'; - -import Checkbox from '@/components/Checkbox'; -import DotsHorizontalIcon from '@/icons/DotsHorizontalIcon'; -import FilledWarningIcon from '@/icons/FilledWarningIcon'; -import LockIcon from '@/icons/LockIcon'; -import { trpc } from '@/utils/trpc'; +import Skeleton from 'react-loading-skeleton'; import SemesterCourseItemDropdown from './SemesterCourseItemDropdown'; import CourseInfoHoverCard from '../CourseInfoHoverCard'; @@ -16,6 +11,14 @@ import { DragDataFromSemesterTile, DraggableCourse, Semester } from '../types'; import useGetCourseInfo from '../useGetCourseInfo'; import { tagColors } from '../utils'; +import Checkbox from '@/components/Checkbox'; +import DotsHorizontalIcon from '@/icons/DotsHorizontalIcon'; +import FilledWarningIcon from '@/icons/FilledWarningIcon'; +import LockIcon from '@/icons/LockIcon'; +import { trpc } from '@/utils/trpc'; + +import 'react-loading-skeleton/dist/skeleton.css'; + export interface SemesterCourseItemProps extends ComponentPropsWithoutRef<'div'> { course: DraggableCourse; semesterLocked?: boolean; @@ -147,7 +150,9 @@ export const MemoizedSemesterCourseItem = React.memo( )} - {title} + + {title || (course.code[0] == '0' ? '' : )} +
{!semesterLocked && ( = ({ >