feat: UTRP v2 migration (#292)

* feat: wip add course by url

* chore: update imports

* feat: add useCourseFromUrl hook

* chore: extract logic into async function

* feat: add checkLoginStatus.ts

* feat: add useCourseMigration hook

* feat: working course migration

* fix: active schedule bug

* feat: refactor logic and add to onUpdate

* feat: update ui style

* feat: add changelog functionality to settings

* chore: update packages

* feat: migration + sentry stuffs

* feat: improve migration flow

* docs: add sentry jsdocs

* chore: fix lint and format

* chore: cleanup + fix race condition

---------

Co-authored-by: Samuel Gunter <sgunter@utexas.edu>
Co-authored-by: Razboy20 <razboy20@gmail.com>
This commit is contained in:
doprz
2024-10-14 21:30:37 -05:00
committed by GitHub
parent e774f316e3
commit d22237d561
23 changed files with 1980 additions and 1865 deletions

View File

@@ -65,7 +65,7 @@ export function Button({
>
{Icon && <Icon className='h-6 w-6' />}
{!isIconOnly && (
<Text variant='h4' className='translate-y-0.08'>
<Text variant='h4' className='inline-flex translate-y-0.08 items-center gap-2'>
{children}
</Text>
)}

View File

@@ -1,4 +1,4 @@
import type { CloseWrapper, DialogInfo, ShowDialogFn } from '@views/contexts/DialogContext';
import type { CloseWrapper, DialogInfo, DialogOptions, ShowDialogFn } from '@views/contexts/DialogContext';
import { DialogContext, useDialog } from '@views/contexts/DialogContext';
import type { ReactNode } from 'react';
import React, { useCallback, useRef, useState } from 'react';
@@ -29,24 +29,27 @@ function unwrapCloseWrapper<T>(obj: T | CloseWrapper<T>, close: () => void): T {
/**
* Hook to show prompt with default stylings.
*/
export function usePrompt(): (info: PromptInfo) => void {
export function usePrompt(): (info: PromptInfo, options?: DialogOptions) => void {
const showDialog = useDialog();
return (info: PromptInfo) => {
showDialog({
...info,
title: (
<Text variant='h2' as='h1' className='text-theme-black'>
{info.title}
</Text>
),
description: (
<Text variant='p' as='p' className='text-ut-black'>
{info.description}
</Text>
),
className: 'max-w-[400px] flex flex-col gap-2.5 p-6.25',
});
return (info, options) => {
showDialog(
{
...info,
title: (
<Text variant='h2' as='h1' className='text-theme-black'>
{info.title}
</Text>
),
description: (
<Text variant='p' as='p' className='text-ut-black'>
{info.description}
</Text>
),
className: 'max-w-[400px] flex flex-col gap-2.5 p-6.25',
},
options
);
};
}
@@ -64,7 +67,7 @@ export default function DialogProvider(props: { children: ReactNode }): JSX.Elem
const [isOpen, setIsOpen] = useState(false);
const showDialog = useCallback<ShowDialogFn>(info => {
const showDialog = useCallback<ShowDialogFn>((info, options) => {
const id = nextId++;
const handleClose = () => {
@@ -89,11 +92,11 @@ export default function DialogProvider(props: { children: ReactNode }): JSX.Elem
const dialogElement = (show: boolean) => (
<Dialog
key={id}
onClose={handleClose}
onClose={(options?.closeOnClickOutside ?? true) ? handleClose : () => {}}
afterLeave={onLeave}
title=<>{infoUnwrapped.title}</>
description=<>{infoUnwrapped.description}</>
appear
appear={!(options?.immediate ?? false)}
show={show}
className={infoUnwrapped.className}
>

View File

@@ -0,0 +1,134 @@
import migrateUTRPv1Courses, { getUTRPv1Courses } from '@background/lib/migrateUTRPv1Courses';
import Text from '@views/components/common/Text/Text';
import { useSentryScope } from '@views/contexts/SentryContext';
import React, { useEffect, useState } from 'react';
import { Button } from './Button';
import { usePrompt } from './DialogProvider/DialogProvider';
import Spinner from './Spinner';
function MigrationButtons({ close }: { close: () => void }): JSX.Element {
const [processState, setProcessState] = useState(0);
const [error, setError] = useState<string | undefined>(undefined);
const [sentryScope] = useSentryScope();
useEffect(() => {
const handleMigration = async () => {
if (processState === 1) {
try {
await chrome.storage.session.set({ pendingMigration: true });
const successful = await migrateUTRPv1Courses();
if (successful) {
await chrome.storage.local.set({ finishedMigration: true });
await chrome.storage.session.remove('pendingMigration');
}
} catch (error) {
console.error(error);
const sentryId = sentryScope.captureException(error);
setError(sentryId);
await chrome.storage.session.remove('pendingMigration');
return;
}
setProcessState(2);
close();
} else {
const { pendingMigration } = await chrome.storage.session.get('pendingMigration');
if (pendingMigration) setProcessState(1);
}
};
handleMigration();
}, [processState]);
return (
<>
{error && (
<Text variant='p' className='text-ut-red'>
An error occurred while migrating your courses. Please try again later in settings. (
{error.substring(0, 8)})
</Text>
)}
<Button
variant='single'
color='ut-black'
onClick={() => {
close();
if (!error) {
chrome.storage.local.set({ finishedMigration: true });
}
}}
>
Cancel
</Button>
<Button
variant='filled'
color='ut-green'
disabled={processState > 0}
onClick={() => {
setProcessState(1);
}}
>
{processState === 1 ? (
<>
Migrating... <Spinner className='ml-2.75 inline-block h-4! w-4! text-current!' />
</>
) : (
'Migrate courses'
)}
</Button>
</>
);
}
export function useMigrationDialog() {
const showDialog = usePrompt();
return async () => {
if ((await getUTRPv1Courses()).length > 0) {
showDialog(
{
title: 'This extension has updated!',
description:
"You may have already began planning your Spring '25 schedule. Click the button below to transfer your saved schedules into a new schedule. (You may be required to login to the UT Registrar)",
buttons: close => <MigrationButtons close={close} />,
},
{
closeOnClickOutside: false,
}
);
} else {
showDialog({
title: 'Already migrated!',
description:
'There are no courses to migrate. If you have any issues, please submit a feedback report by clicking the flag at the top right of the extension popup.',
buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}>
I Understand
</Button>
),
});
}
};
}
export function MigrationDialog(): JSX.Element {
const showMigrationDialog = useMigrationDialog();
useEffect(() => {
const checkMigration = async () => {
// check if migration was already attempted
if ((await chrome.storage.local.get('finishedMigration')).finishedMigration) return;
if ((await getUTRPv1Courses()).length > 0) showMigrationDialog();
};
checkMigration();
}, []);
// (not actually a useless fragment)
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}