-
Notifications
You must be signed in to change notification settings - Fork 593
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This change stores the rubric in the indexed db and loads the last-active rubric automatically when the page loads. Currently only one rubric is really stored at a time, but the schema can support multiple if we want to evolve this over time. Indexed DB has two tables: 1. Rubrics - where we store the rubrics, with the key set to the rubric name 2. Metadata - internal settings we may want to store, currently just lastActiveRubricName, but I imagine we may want to use this later to preserve preferences like auto-save and auto-run.
- Loading branch information
Showing
17 changed files
with
331 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { useEffect, useRef } from "react"; | ||
import { Input, InputProps } from "react-common/components/controls/Input"; | ||
|
||
export interface DebouncedInputProps extends InputProps { | ||
intervalMs?: number; // Default 500 ms | ||
} | ||
|
||
// This functions like the React Common Input, but debounces onChange calls, | ||
// so if onChange is called multiple times in quick succession, it will only | ||
// be executed once after a pause of the specified `interval` in milliseconds. | ||
export const DebouncedInput: React.FC<DebouncedInputProps> = ({ intervalMs = 500, ...props }) => { | ||
const timerId = useRef<NodeJS.Timeout | undefined>(undefined); | ||
const latestValue = useRef<string>(""); | ||
|
||
const sendChange = () => { | ||
if (props.onChange) { | ||
props.onChange(latestValue.current); | ||
} | ||
}; | ||
|
||
// If the timer is pending and the component unmounts, | ||
// clear the timer and fire the onChange event immediately. | ||
useEffect(() => { | ||
return () => { | ||
if (timerId.current) { | ||
clearTimeout(timerId.current); | ||
sendChange(); | ||
} | ||
}; | ||
}, []); | ||
|
||
const onChangeDebounce = (newValue: string) => { | ||
latestValue.current = newValue; | ||
|
||
if (timerId.current) { | ||
clearTimeout(timerId.current); | ||
} | ||
|
||
timerId.current = setTimeout(sendChange, intervalMs); | ||
}; | ||
|
||
return <Input {...props} onChange={onChangeDebounce} />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { openDB, IDBPDatabase } from "idb"; | ||
import { ErrorCode } from "../types/errorCode"; | ||
import { logError } from "./loggingService"; | ||
import { Rubric } from "../types/rubric"; | ||
|
||
const teacherToolDbName = "makecode-project-insights"; | ||
const dbVersion = 1; | ||
const rubricsStoreName = "rubrics"; | ||
const metadataStoreName = "metadata"; | ||
const metadataKeys = { | ||
lastActiveRubricKey: "lastActiveRubricName", | ||
}; | ||
|
||
type MetadataEntry = { key: string; value: any }; | ||
|
||
class TeacherToolDb { | ||
db: IDBPDatabase | undefined; | ||
|
||
public async initializeAsync() { | ||
if (this.db) return; | ||
this.db = await openDB(teacherToolDbName, dbVersion, { | ||
upgrade(db) { | ||
db.createObjectStore(rubricsStoreName, { keyPath: "name" }); | ||
db.createObjectStore(metadataStoreName, { keyPath: "key" }); | ||
}, | ||
}); | ||
} | ||
|
||
private async getAsync<T>(storeName: string, key: string): Promise<T | undefined> { | ||
if (!this.db) { | ||
throw new Error("IndexedDb not initialized."); | ||
} | ||
|
||
try { | ||
return await this.db.get(storeName, key); | ||
} catch (e) { | ||
// Not recording key, as it could contain user-input with sensitive information. | ||
logError(ErrorCode.unableToGetIndexedDbRecord, e); | ||
} | ||
} | ||
|
||
private async setAsync<T>(storeName: string, value: T): Promise<void> { | ||
if (!this.db) { | ||
throw new Error("IndexedDb not initialized."); | ||
} | ||
|
||
try { | ||
await this.db.put(storeName, value); | ||
} catch (e) { | ||
// Not recording key, as it could contain user-input with sensitive information. | ||
logError(ErrorCode.unableToSetIndexedDbRecord, e); | ||
} | ||
} | ||
|
||
private async deleteAsync(storeName: string, key: string): Promise<void> { | ||
if (!this.db) { | ||
throw new Error("IndexedDb not initialized."); | ||
} | ||
try { | ||
await this.db.delete(storeName, key); | ||
} catch (e) { | ||
// Not recording key, as it could contain user-input with sensitive information. | ||
logError(ErrorCode.unableToDeleteIndexedDbRecord, e); | ||
} | ||
} | ||
|
||
private async getMetadataEntryAsync(key: string): Promise<MetadataEntry | undefined> { | ||
return this.getAsync<MetadataEntry>(metadataStoreName, key); | ||
} | ||
|
||
private async setMetadataEntryAsync(key: string, value: any): Promise<void> { | ||
return this.setAsync<MetadataEntry>(metadataStoreName, { key, value }); | ||
} | ||
|
||
private async deleteMetadataEntryAsync(key: string): Promise<void> { | ||
return this.deleteAsync(metadataStoreName, key); | ||
} | ||
|
||
public async getLastActiveRubricNameAsync(): Promise<string | undefined> { | ||
const metadataEntry = await this.getMetadataEntryAsync(metadataKeys.lastActiveRubricKey); | ||
return metadataEntry?.value; | ||
} | ||
|
||
public saveLastActiveRubricNameAsync(name: string): Promise<void> { | ||
return this.setMetadataEntryAsync(metadataKeys.lastActiveRubricKey, name); | ||
} | ||
|
||
public getRubric(name: string): Promise<Rubric | undefined> { | ||
return this.getAsync<Rubric>(rubricsStoreName, name); | ||
} | ||
|
||
public saveRubric(rubric: Rubric): Promise<void> { | ||
return this.setAsync(rubricsStoreName, rubric); | ||
} | ||
|
||
public deleteRubric(name: string): Promise<void> { | ||
return this.deleteAsync(rubricsStoreName, name); | ||
} | ||
} | ||
|
||
const getDb = (async () => { | ||
const db = new TeacherToolDb(); | ||
await db.initializeAsync(); | ||
return db; | ||
})(); | ||
|
||
export async function getLastActiveRubricAsync(): Promise<Rubric | undefined> { | ||
const db = await getDb; | ||
|
||
let rubric: Rubric | undefined = undefined; | ||
const name = await db.getLastActiveRubricNameAsync(); | ||
if (name) { | ||
rubric = await db.getRubric(name); | ||
} | ||
|
||
return rubric; | ||
} | ||
|
||
export async function saveRubricAsync(rubric: Rubric) { | ||
const db = await getDb; | ||
await db.saveRubric(rubric); | ||
await db.saveLastActiveRubricNameAsync(rubric.name); | ||
} | ||
|
||
export async function deleteRubricAsync(name: string) { | ||
const db = await getDb; | ||
await db.deleteRubric(name); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.