Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/store-permissions.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# WatDesk 1.10.0 — Edge Add-ons permission justifications
# WatDesk 1.10.1 — Edge Add-ons permission justifications

Single purpose: organize a Waterloo student's own coursework, exams and grades from sites they are already signed in to. Academic data stays in local extension storage. A usage count is on by default, can be disabled in setup or Settings, and sends only a random install ID and version to the developer’s Cloudflare Worker. No academic data is sent; there is no remote code.

Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"description": "__MSG_extDescription__",
"version": "1.10.0",
"version": "1.10.1",
"minimum_chrome_version": "120",
"permissions": [
"storage",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "watdesk",
"version": "1.10.0",
"version": "1.10.1",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {personallyDone,personalNote,courseKey} from '../core/organization';
import {SchoolLogin} from './SchoolLogin';
import {Freshness,LocalDataPanel} from './DataTools';
import {HiddenItems} from './HiddenItems';
import {OutlinePrompt} from './OutlinePrompt';
import {CourseReminders,NextClasses} from './CourseReminders';
import {meetingEntries,courseVisible} from '../core/academic';
import {Grades} from './Grades';
Expand Down Expand Up @@ -74,6 +75,7 @@ export function App(){
if(needsOnboarding(state))return <Connections save={settings} state={state} disableMobius={()=>settings({mobiusEnabled:false})} disableCs135={()=>settings({cs135Enabled:false})} error={error} connect={p=>void connect(p)} refresh={p=>void perform('sync',{platform:p})} finish={()=>settings({onboardingComplete:true})} language={()=>settings({locale:state.settings.locale==='en'?'zh-CN':'en'})} full={()=>chrome.tabs.create({url:chrome.runtime.getURL('index.html')})}/>;
return <div className={`shell ${side?'side':''}`}><aside className="sidebar"><a className="brand" href="#overview" onClick={()=>changePage('overview')}><span className="brand-mark">W<span/></span><strong>WatDesk {installedVersion&&<small>v{installedVersion}</small>}</strong></a><div className="school-caption">UNIVERSITY OF WATERLOO</div><nav>{pages.map((p,i)=>{const Icon=pageIcons[i];return <button key={p} className={p===page?'selected':''} onClick={()=>changePage(p)} title={t(p)} aria-label={t(p)} aria-current={p===page?'page':undefined}><Icon size={20} weight={p===page?'fill':'regular'}/><span>{t(p)}</span>{p==='tasks'&&tasks.filter(e=>!done(e)).length>0&&<small>{tasks.filter(e=>!done(e)).length}</small>}</button>})}</nav><div className="sidebar-bottom"><p>{t('privacy')}</p><button className="text-button" onClick={()=>chrome.tabs.create({url:chrome.runtime.getURL('index.html')})}><ArrowSquareOut size={17}/>{t('full')}</button></div></aside>
<main><WorkspaceToolbar zh={state.settings.locale==='zh-CN'} side={side} version={installedVersion} refresh={()=>setRefreshSignal(v=>v+1)} add={()=>setEditor('new')} full={()=>chrome.tabs.create({url:chrome.runtime.getURL('index.html')})} language={()=>settings({locale:state.settings.locale==='en'?'zh-CN':'en'})} theme={()=>settings({theme:document.documentElement.dataset.theme==='dark'?'light':'dark'})}/>
<OutlinePrompt state={state} enable={()=>void connect('outline')}/>
<BackgroundSync state={state} retry={()=>setRefreshSignal(v=>v+1)} details={()=>changePage('settings')}/>
<div ref={pageContent} data-page={page} className={`content ${page==='overview'?'overview-page':''}`}><div className="page-heading"><div><p className="eyebrow">WATDESK</p><h1>{t(page)}</h1><p className="muted">{new Intl.DateTimeFormat(state.settings.locale,{timeZone:zone,weekday:'long',month:'long',day:'numeric',year:'numeric'}).format(new Date())}<span className="timezone">{zone}</span></p></div><button className="icon-button" aria-label={t('export')} title={t('export')} onClick={()=>download(exportIcs(state),'watdesk.ics','text/calendar;charset=utf-8')}><DownloadSimple size={21}/></button></div>
{error&&<div role="alert" className="alert"><Warning size={18}/><span>{error in ({identityChanged:1,restoreError:1,permission:1,invalid:1,error:1})?t(error as TranslationKey):error}</span><button className="icon-button" aria-label={t('cancel')} onClick={()=>setError('')}><X/></button></div>}
Expand Down
17 changes: 17 additions & 0 deletions src/ui/OutlinePrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {useState} from 'react';
import {FileText,X} from '@phosphor-icons/react';
import type {AppState} from '../domain';
const key='watdesk.outlinePromptDismissed';
const dismissed=()=>{try{return localStorage.getItem(key)==='1'}catch{return false}};
// Dates that only appear in a course outline (for example ENGL) are invisible until the user opts in, so say so once.
export function OutlinePrompt({state,enable}:{state:AppState;enable:()=>void}){
const [hidden,setHidden]=useState(dismissed);
const zh=state.settings.locale==='zh-CN';
if(hidden||state.settings.outlineEnabled||!state.settings.onboardingComplete)return null;
return <section className="outline-prompt" role="region" aria-label={zh?'课程大纲':'Course outlines'}>
<FileText size={20} aria-hidden="true"/>
<p><b>{zh?'有些课的作业和考试只写在课程大纲里':'Some courses list deadlines only in the course outline'}</b><span>{zh?'启用后,WatDesk 用你已登录的 outline.uwaterloo.ca 自动读取每门课大纲里写明日期的安排(例如 ENGL)。只读,不猜测未公布的日期。':'WatDesk then reads dated assessments from every course outline using your signed-in outline.uwaterloo.ca session (for example ENGL). Read-only; unpublished dates are never guessed.'}</span></p>
<button className="button primary" onClick={enable}>{zh?'启用':'Enable'}</button>
<button className="icon-button" aria-label={zh?'不再提示':'Dismiss'} onClick={()=>{try{localStorage.setItem(key,'1')}catch{/* The prompt just returns next time. */}setHidden(true)}}><X size={16}/></button>
</section>;
}
1 change: 1 addition & 0 deletions src/ui/styles.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions tests/outline-prompt.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @vitest-environment jsdom
import {act} from 'react';
import {createRoot} from 'react-dom/client';
import {it,expect,vi,beforeEach} from 'vitest';
import {OutlinePrompt} from '../src/ui/OutlinePrompt';
import {emptyState} from '../src/domain';
beforeEach(()=>localStorage.clear());
it('offers the outline source once, hides when enabled, not onboarded or dismissed',async()=>{
(globalThis as any).IS_REACT_ACT_ENVIRONMENT=true;
const s=emptyState('en');s.settings.onboardingComplete=true;
const enable=vi.fn(),host=document.createElement('div');document.body.append(host);const root=createRoot(host);
try{
await act(async()=>root.render(<OutlinePrompt state={s} enable={enable}/>));
expect(host.textContent).toContain('course outline');
await act(async()=>(host.querySelector('.button.primary') as HTMLButtonElement).click());expect(enable).toHaveBeenCalledTimes(1);
await act(async()=>(host.querySelector('.icon-button') as HTMLButtonElement).click());expect(host.innerHTML).toBe('');expect(localStorage.getItem('watdesk.outlinePromptDismissed')).toBe('1');
localStorage.clear();await act(async()=>root.render(<OutlinePrompt state={{...s,settings:{...s.settings,outlineEnabled:true}}} enable={enable}/>));expect(host.innerHTML).toBe('');
await act(async()=>root.render(<OutlinePrompt state={{...s,settings:{...s.settings,onboardingComplete:false}}} enable={enable}/>));expect(host.innerHTML).toBe('');
}finally{root.unmount();host.remove()}
});
Loading