feat: open an injected course page on course block click in popup main (#146)

* feat: Imports to popupcourseblock.tsx

* changing the blocks to accept parameters for clicking functionality which may or may not open the calendar

* put the click parameter in the div of popupcourseblock

* safely calling for onCourseClick in the event it is an undefined function

* handled other calls of popupcourseblock with empty functions for now, and i think popupmain opens calendar now when the course block is clicked

* feat: Testing out passing params to handleOpenCalendar

* url that takes in params to open calendar with params

* further work on url params; from popup main to handleopencalendar to calendar using urlsearchparams

* feat: small calendar shifting after merge:

* fix: merge handling and then references to new click parameter

* fix: optional params

* feat: split into two functions instead

* fix: changing proper usage of handleOpenCalendarWithCourse

* feat: show course popup when calendar opened

* chore: remove useless commented out code

* feat: close popup on calendar nav, fix build errors, remove useless comments/logs

* chore: chromatic so dumb fr why aren't you chrome

* fix: refactor listeners to build properly

* feat: exit early when not in chrome extension

* fix: function return type

* fix: function return type x2

* fix: generic type for useState

* refactor: extract calendar opening on click functions

* refactor: chrome runtime mock, omit question mark if no query params, rename calendar event

* refactor: move course click event into component directly instead of prop

* refactor: removed useless wrapper functions, made popup course block more accessible

* fix: i dont wanna talk about it

---------

Co-authored-by: Samuel Gunter <sgunter@utexas.edu>
This commit is contained in:
2024-03-16 15:57:50 -05:00
committed by GitHub
parent ed4fbe5651
commit 27094846f7
17 changed files with 203 additions and 38 deletions

View File

@@ -141,6 +141,29 @@ globalThis.chrome = {
},
},
},
runtime: {
id: 'fake-id',
getManifest(): chrome.runtime.Manifest {
return {
manifest_version: 3,
name: 'fake-name',
version: '0.0.0',
};
},
onMessage: {
/**
* Registers an event listener callback to an event.
* @param callback Called when an event occurs. The parameters of this function depend on the type of event.
*/
addListener<T extends Function>(callback: T) {},
/**
* Deregisters an event listener callback from an event.
* @param callback Listener that shall be unregistered.
*/
removeListener<T extends Function>(callback: T) {},
},
},
} as typeof chrome;
export default preview;

View File

@@ -29,7 +29,7 @@
"@hello-pangea/dnd": "^16.5.0",
"@unocss/vite": "^0.58.6",
"@vitejs/plugin-react": "^4.2.1",
"chrome-extension-toolkit": "^0.0.51",
"chrome-extension-toolkit": "^0.0.54",
"clsx": "^2.1.0",
"highcharts": "^11.3.0",
"highcharts-react-official": "^3.2.1",
@@ -120,4 +120,4 @@
"es-module-lexer": "^1.4.1"
}
}
}
}

8
pnpm-lock.yaml generated
View File

@@ -29,8 +29,8 @@ dependencies:
specifier: ^4.2.1
version: 4.2.1(vite@5.1.4)
chrome-extension-toolkit:
specifier: ^0.0.51
version: 0.0.51
specifier: ^0.0.54
version: 0.0.54
clsx:
specifier: ^2.1.0
version: 2.1.0
@@ -6283,8 +6283,8 @@ packages:
optional: true
dev: true
/chrome-extension-toolkit@0.0.51:
resolution: {integrity: sha512-XzOOE2+/aYG43bJOwuJT4oWcn80jBJr5mwGyrSzKKFoqALixT15AsPcfZId/UOoc4pIavu2XcHeJga6ng0m1jQ==}
/chrome-extension-toolkit@0.0.54:
resolution: {integrity: sha512-ux8v/PfWQIvO+EBbF+kDYq2z8Rnp5YZ7GwJxYX7R2a9owIEHJxiCUSJ82tOsiMQINF/31+t6QLG9equKNZUOlA==}
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)

View File

@@ -7,6 +7,7 @@ import onInstall from './events/onInstall';
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
import onUpdate from './events/onUpdate';
import browserActionHandler from './handler/browserActionHandler';
import calendarBackgroundHandler from './handler/calendarBackgroundHandler';
import CESHandler from './handler/CESHandler';
import tabManagementHandler from './handler/tabManagementHandler';
import userScheduleHandler from './handler/userScheduleHandler';
@@ -36,6 +37,7 @@ const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
...tabManagementHandler,
...userScheduleHandler,
...CESHandler,
...calendarBackgroundHandler,
});
messageListener.listen();

View File

@@ -0,0 +1,44 @@
import openNewTab from '@background/util/openNewTab';
import { tabs } from '@shared/messages';
import type { CalendarBackgroundMessages } from '@shared/messages/CalendarMessages';
import type { MessageHandler } from 'chrome-extension-toolkit';
const getAllTabInfos = async () => {
const openTabs = await chrome.tabs.query({});
const results = await Promise.allSettled(openTabs.map(tab => tabs.getTabInfo(undefined, tab.id)));
return results
.map((result, index) => ({ result, index }))
.filter(({ result }) => result.status === 'fulfilled')
.map(({ result, index }) => {
if (result.status !== 'fulfilled') throw new Error('Will never happen, typescript dumb');
return {
...result.value,
tab: openTabs[index],
};
});
};
const calendarBackgroundHandler: MessageHandler<CalendarBackgroundMessages> = {
async switchToCalendarTab({ data, sendResponse }) {
const { uniqueId } = data;
const calendarUrl = chrome.runtime.getURL(`calendar.html`);
const allTabs = await getAllTabInfos();
const openCalendarTabInfo = allTabs.find(tab => tab.url.startsWith(calendarUrl));
if (openCalendarTabInfo !== undefined) {
chrome.tabs.update(openCalendarTabInfo.tab.id, { active: true });
if (uniqueId !== undefined) await tabs.openCoursePopup({ uniqueId }, openCalendarTabInfo.tab.id);
sendResponse(openCalendarTabInfo.tab);
} else {
const urlParams = new URLSearchParams();
if (uniqueId !== undefined) urlParams.set('uniqueId', uniqueId.toString());
const url = `${calendarUrl}?${urlParams.toString()}`.replace(/\?$/, '');
const tab = await openNewTab(url);
sendResponse(tab);
}
},
};
export default calendarBackgroundHandler;

View File

@@ -0,0 +1,19 @@
interface CalendarBackgroundMessages {
/**
* Opens the calendar page if it is not already open, focuses the tab, and optionally opens the calendar for a specific course
*
* @param data - The unique id of the course to open the calendar page for (optional)
*/
switchToCalendarTab: (data: { uniqueId?: number }) => chrome.tabs.Tab;
}
interface CalendarTabMessages {
/**
* Opens a popup for a specific course on the calendar page
*
* @param data - The unique id of the course to open on the calendar page
*/
openCoursePopup: (data: { uniqueId: number }) => chrome.tabs.Tab;
}
export type { CalendarBackgroundMessages, CalendarTabMessages };

View File

@@ -0,0 +1,15 @@
type TabInfo = {
url: string;
title: string;
};
interface TabInfoMessages {
/**
* Gets the info for the tab receiving the message
*
* @returns The info for the tab receiving the message
*/
getTabInfo: () => TabInfo;
}
export default TabInfoMessages;

View File

@@ -1,4 +0,0 @@
/**
* This is a type with all the message definitions that can be sent TO specific tabs
*/
export default interface TAB_MESSAGES {}

View File

@@ -1,15 +1,25 @@
import { createMessenger } from 'chrome-extension-toolkit';
import type BrowserActionMessages from './BrowserActionMessages';
import type { CalendarBackgroundMessages, CalendarTabMessages } from './CalendarMessages';
import type CESMessage from './CESMessage';
import type TabInfoMessages from './TabInfoMessages';
import type TabManagementMessages from './TabManagementMessages';
import type TAB_MESSAGES from './TabMessages';
import type { UserScheduleMessages } from './UserScheduleMessages';
/**
* This is a type with all the message definitions that can be sent TO the background script
*/
export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & UserScheduleMessages & CESMessage;
export type BACKGROUND_MESSAGES = BrowserActionMessages &
TabManagementMessages &
UserScheduleMessages &
CESMessage &
CalendarBackgroundMessages;
/**
* This is a type with all the message definitions that can be sent TO specific tabs
*/
export type TAB_MESSAGES = CalendarTabMessages & TabInfoMessages;
/**
* A utility object that can be used to send type-safe messages to the background script
@@ -19,4 +29,4 @@ export const background = createMessenger<BACKGROUND_MESSAGES>('background');
/**
* A utility object that can be used to send type-safe messages to specific tabs
*/
export const tabs = createMessenger<TAB_MESSAGES>('tab');
export const tabs = createMessenger<TAB_MESSAGES>('foreground');

View File

@@ -103,9 +103,9 @@ export const Variants: Story = {
};
export const AllColors: Story = {
render: props => (
render: () => (
<div className='grid grid-flow-col grid-cols-2 grid-rows-9 max-w-2xl w-90vw gap-x-4 gap-y-2'>
{tailwindColorways.map((color, i) => (
{tailwindColorways.map(color => (
<PopupCourseBlock key={color.primaryColor} course={ExampleCourse} colors={color} />
))}
</div>

View File

@@ -1,10 +1,10 @@
import { background } from '@shared/messages';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { tailwindColorways } from '@shared/util/storybook';
import Divider from '@views/components/common/Divider/Divider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import List from '@views/components/common/List/List';
import Text from '@views/components/common/Text/Text';
import { handleOpenCalendar } from '@views/components/injected/CourseCatalogInjectedPopup/HeadingAndActions';
import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from '@views/hooks/useSchedules';
import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString';
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
@@ -34,6 +34,11 @@ export default function PopupMain(): JSX.Element {
await openTabFromContentScript(url);
};
const handleCalendarOpenOnClick = async () => {
await background.switchToCalendarTab({});
window.close();
};
return (
<ExtensionRoot>
<div className='h-screen max-h-full flex flex-col bg-white'>
@@ -50,7 +55,7 @@ export default function PopupMain(): JSX.Element {
</div>
</div>
<div className='flex items-center gap-2.5'>
<button className='bg-ut-burntorange px-2 py-1.25 btn' onClick={handleOpenCalendar}>
<button className='bg-ut-burntorange px-2 py-1.25 btn' onClick={handleCalendarOpenOnClick}>
<CalendarIcon className='size-6 text-white' />
</button>
<button className='bg-transparent px-2 py-1.25 btn' onClick={handleOpenOptions}>

View File

@@ -1,3 +1,4 @@
import type { CalendarTabMessages } from '@shared/messages/CalendarMessages';
import type { Course } from '@shared/types/Course';
import CalendarBottomBar from '@views/components/calendar/CalendarBottomBar/CalendarBottomBar';
import CalendarGrid from '@views/components/calendar/CalendarGrid/CalendarGrid';
@@ -7,6 +8,7 @@ import ImportantLinks from '@views/components/calendar/ImportantLinks';
import Divider from '@views/components/common/Divider/Divider';
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule';
import { MessageListener } from 'chrome-extension-toolkit';
import React, { useEffect, useRef, useState } from 'react';
import styles from './Calendar.module.scss';
@@ -17,11 +19,39 @@ import styles from './Calendar.module.scss';
export default function Calendar(): JSX.Element {
const calendarRef = useRef<HTMLDivElement>(null);
const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
const [course, setCourse] = useState<Course | null>(null);
const [showPopup, setShowPopup] = useState(false);
const [course, setCourse] = useState<Course | null>((): Course | null => {
const urlParams = new URLSearchParams(window.location.search);
const uniqueIdRaw = urlParams.get('uniqueId');
if (uniqueIdRaw === null) return null;
const uniqueId = Number(uniqueIdRaw);
const course = activeSchedule.courses.find(course => course.uniqueId === uniqueId);
if (course === undefined) return null;
urlParams.delete('uniqueId');
const newUrl = `${window.location.pathname}?${urlParams}`.replace(/\?$/, '');
window.history.replaceState({}, '', newUrl);
return course;
});
const [showPopup, setShowPopup] = useState<boolean>(course !== null);
const [sidebarWidth, setSidebarWidth] = useState('20%');
const [scale, setScale] = useState(1);
useEffect(() => {
const listener = new MessageListener<CalendarTabMessages>({
async openCoursePopup({ data, sendResponse }) {
const course = activeSchedule.courses.find(course => course.uniqueId === data.uniqueId);
if (course === undefined) return;
setCourse(course);
setShowPopup(true);
sendResponse(await chrome.tabs.getCurrent());
},
});
listener.listen();
return () => listener.unlisten();
}, [activeSchedule.courses]);
useEffect(() => {
const adjustLayout = () => {
const windowHeight = window.innerHeight;

View File

@@ -1,17 +1,35 @@
// import '@unocss/reset/tailwind-compat.css';
import 'uno.css';
import React from 'react';
import type TabInfoMessages from '@shared/messages/TabInfoMessages';
import { MessageListener } from 'chrome-extension-toolkit';
import React, { useEffect } from 'react';
import styles from './ExtensionRoot.module.scss';
interface Props {
testId?: string;
}
/**
* A wrapper component for the extension elements that adds some basic styling to them
*/
export default function ExtensionRoot(props: React.PropsWithChildren<Props>): JSX.Element {
useEffect(() => {
const tabInfoListener = new MessageListener<TabInfoMessages>({
getTabInfo: ({ sendResponse }) => {
sendResponse({
url: window.location.href,
title: document.title,
});
},
});
tabInfoListener.listen();
return () => tabInfoListener.unlisten();
}, []);
return (
<div className={styles.extensionRoot} data-testid={props.testId}>
{props.children}

View File

@@ -1,3 +1,4 @@
import { background } from '@shared/messages';
import type { Course } from '@shared/types/Course';
import { Status } from '@shared/types/Course';
import type { CourseColors } from '@shared/util/colors';
@@ -34,12 +35,21 @@ export default function PopupCourseBlock({
const fontColor = pickFontColor(colors.primaryColor);
const formattedUniqueId = course.uniqueId.toString().padStart(5, '0');
const handleClick = async () => {
await background.switchToCalendarTab({ uniqueId: course.uniqueId });
window.close();
};
return (
<div
<button
style={{
backgroundColor: colors.primaryColor,
}}
className={clsx('h-full w-full inline-flex items-center justify-center gap-1 rounded pr-3', className)}
className={clsx(
'h-full w-full inline-flex items-center justify-center gap-1 rounded pr-3 cursor-pointer focusable text-left',
className
)}
onClick={handleClick}
>
<div
style={{
@@ -64,6 +74,6 @@ export default function PopupCourseBlock({
<StatusIcon status={course.status} className='h-5 w-5' />
</div>
)}
</div>
</button>
);
}

View File

@@ -29,15 +29,6 @@ interface HeadingAndActionProps {
onClose: () => void;
}
/**
* Opens the calendar in a new tab.
* @returns {Promise<void>} A promise that resolves when the tab is opened.
*/
export const handleOpenCalendar = async (): Promise<void> => {
const url = chrome.runtime.getURL('calendar.html');
openNewTab({ url });
};
/**
* Capitalizes the first letter of a string and converts the rest of the letters to lowercase.
*
@@ -174,7 +165,12 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
</div>
</div>
<div className='my-3 flex flex-wrap items-center gap-x-3.75 gap-y-2.5'>
<Button variant='filled' color='ut-burntorange' icon={CalendarMonth} onClick={handleOpenCalendar} />
<Button
variant='filled'
color='ut-burntorange'
icon={CalendarMonth}
onClick={() => background.switchToCalendarTab({})}
/>
<Divider size='1.75rem' orientation='vertical' />
<Button variant='outline' color='ut-blue' icon={Reviews} onClick={handleOpenRateMyProf}>
RateMyProf

View File

@@ -1,4 +1,4 @@
import type TAB_MESSAGES from '@shared/messages/TabMessages';
import type { TAB_MESSAGES } from '@shared/messages';
import { createUseMessage } from 'chrome-extension-toolkit';
export const useTabMessage = createUseMessage<TAB_MESSAGES>();

View File

@@ -46,8 +46,5 @@ onContextInvalidated(() => {
div.id = 'context-invalidated-container';
document.body.appendChild(div);
render(
<ContextInvalidated fontFamily='monospace' color={colors.white} backgroundColor={colors.burnt_orange} />,
div
);
render(<ContextInvalidated className='bg-ut-burntorange text-white font-mono' />, div);
});