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

@@ -31,6 +31,22 @@ chrome.runtime.onInstalled.addListener(details => {
}
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
console.log(changeInfo);
if (changeInfo.url === 'https://utdirect.utexas.edu/apps/registrar/course_schedule/utrp_login/') {
console.log('UTDirect detected');
// close the tab, open popup
function openPopupAction() {
chrome.tabs.onActivated.removeListener(openPopupAction);
chrome.action.openPopup();
}
chrome.tabs.onActivated.addListener(openPopupAction);
await chrome.tabs.remove(tabId);
}
});
// initialize the message listener that will listen for messages from the content script
const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
...browserActionHandler,

View File

@@ -1,12 +1,14 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { UserSchedule } from '@shared/types/UserSchedule';
import { generateRandomId } from '@shared/util/random';
import type { Serialized } from 'chrome-extension-toolkit';
/**
* Creates a new schedule with the given name
* @param scheduleName the name of the schedule to create
* @returns undefined if successful, otherwise an error message
*/
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
export default async function createSchedule(scheduleName: string) {
const schedules = await UserScheduleStore.get('schedules');
// get the number of schedules that either have the same name or have the same name with a number appended (e.g. "New Schedule (1)")
// this way we can prevent duplicate schedule names and increment the number if necessary
@@ -22,14 +24,16 @@ export default async function createSchedule(scheduleName: string): Promise<stri
if (count > 0) {
name = `${scheduleName} (${count})`;
}
schedules.push({
const newSchedule: Serialized<UserSchedule> = {
id: generateRandomId(),
name,
courses: [],
hours: 0,
updatedAt: Date.now(),
});
};
schedules.push(newSchedule);
await UserScheduleStore.set('schedules', schedules);
return undefined;
return newSchedule.id;
}

View File

@@ -0,0 +1,78 @@
import { validateLoginStatus } from '@shared/util/checkLoginStatus';
import { getActiveSchedule } from '@views/hooks/useSchedules';
import { courseMigration } from '@views/lib/courseMigration';
import addCourse from './addCourse';
import createSchedule from './createSchedule';
import switchSchedule from './switchSchedule';
/**
* Retrieves the saved courses from the extension's chrome sync storage (old store) and returns an array of course links.
*
* @returns A promise that resolves to an array of course links.
*/
export async function getUTRPv1Courses(): Promise<string[]> {
const { savedCourses } = await chrome.storage.sync.get('savedCourses');
// Check if the savedCourses array is empty
if (!savedCourses || savedCourses.length === 0) {
return [];
}
// Extract the link property from each course object and return it as an array
return savedCourses.map((course: { link: string }) => course.link);
}
/**
* Handles the migration of UTRP v1 courses.
*
* This function checks if the user is logged into the UT course schedule page.
* If the user is not logged in, it logs a message and exits. If the user is
* logged in, it retrieves the UTRP v1 courses, creates a new schedule for the
* migration, switches to the new schedule, and migrates the courses to the
* active schedule.
*
* @returns A promise that resolves when the migration is complete.
*/
async function migrateUTRPv1Courses() {
const loggedInToUT = await validateLoginStatus(
'https://utdirect.utexas.edu/apps/registrar/course_schedule/utrp_login/'
);
if (!loggedInToUT) {
console.warn('Not logged in to UT Registrar.');
return false;
}
const oldCourses = await getUTRPv1Courses();
console.log(oldCourses);
const migratedCourses = await courseMigration(oldCourses);
if (migratedCourses.length > 0) {
console.log(oldCourses, migratedCourses);
const migrateSchedule = await createSchedule('Migrated Schedule');
await switchSchedule(migrateSchedule);
const activeSchedule = getActiveSchedule();
for (const course of migratedCourses) {
// Add the course if it doesn't already exist
if (activeSchedule.courses.every(c => c.uniqueId !== course.uniqueId)) {
// ignore eslint, as we *do* want to spend time on each iteration
// eslint-disable-next-line no-await-in-loop
await addCourse(activeSchedule.id, course);
}
}
// Remove the old courses from storage :>
await chrome.storage.sync.remove('savedCourses');
console.log('Successfully migrated UTRP v1 courses');
} else {
console.warn('No courses successfully found to migrate');
}
return true;
}
export default migrateUTRPv1Courses;

View File

@@ -2,6 +2,8 @@ import type TabInfoMessages from '@shared/messages/TabInfoMessages';
import Calendar from '@views/components/calendar/Calendar';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { MigrationDialog } from '@views/components/common/MigrationDialog';
import SentryProvider from '@views/contexts/SentryContext';
import { MessageListener } from 'chrome-extension-toolkit';
import useKC_DABR_WASM from 'kc-dabr-wasm';
import React, { useEffect } from 'react';
@@ -28,10 +30,13 @@ export default function CalendarMain() {
}, []);
return (
<ExtensionRoot className='h-full w-full'>
<DialogProvider>
<Calendar />
</DialogProvider>
</ExtensionRoot>
<SentryProvider fullInit>
<ExtensionRoot className='h-full w-full'>
<DialogProvider>
<MigrationDialog />
<Calendar />
</DialogProvider>
</ExtensionRoot>
</SentryProvider>
);
}

View File

@@ -1,6 +1,7 @@
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import Settings from '@views/components/settings/Settings';
import SentryProvider from '@views/contexts/SentryContext';
import useKC_DABR_WASM from 'kc-dabr-wasm';
import React from 'react';
@@ -13,10 +14,12 @@ import React from 'react';
export default function SettingsPage() {
useKC_DABR_WASM();
return (
<ExtensionRoot>
<DialogProvider>
<Settings />
</DialogProvider>
</ExtensionRoot>
<SentryProvider fullInit>
<ExtensionRoot>
<DialogProvider>
<Settings />
</DialogProvider>
</ExtensionRoot>
</SentryProvider>
);
}

View File

@@ -1,7 +1,20 @@
import 'uno.css';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { MigrationDialog } from '@views/components/common/MigrationDialog';
import PopupMain from '@views/components/PopupMain';
import SentryProvider from '@views/contexts/SentryContext';
import React from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')!).render(<PopupMain />);
createRoot(document.getElementById('root')!).render(
<SentryProvider fullInit>
<ExtensionRoot>
<DialogProvider>
<MigrationDialog />
<PopupMain />
</DialogProvider>
</ExtensionRoot>
</SentryProvider>
);

View File

@@ -1,5 +1,10 @@
import ReportIssueMain from '@views/components/ReportIssueMain';
import SentryProvider from '@views/contexts/SentryContext';
import React from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')!).render(<ReportIssueMain />);
createRoot(document.getElementById('root')!).render(
<SentryProvider fullInit>
<ReportIssueMain />
</SentryProvider>
);