diff --git a/ts/graphs/BUILD.bazel b/ts/graphs/BUILD.bazel index 84b26bcb7..8911b97ef 100644 --- a/ts/graphs/BUILD.bazel +++ b/ts/graphs/BUILD.bazel @@ -20,6 +20,7 @@ ts_library( "GraphsPage", "lib", "//ts/lib", + "//ts/sveltelib", "@npm//svelte", "@npm//svelte2tsx", ], @@ -32,6 +33,7 @@ ts_library( exclude = ["index.ts"], ), deps = [ + "//ts/sveltelib", "//ts/lib", "//ts/lib:backend_proto", "@npm//@types/d3", @@ -59,6 +61,7 @@ esbuild( output_css = True, visibility = ["//visibility:public"], deps = [ + "//ts/sveltelib", "//ts/lib", "//ts/lib:backend_proto", "//ts/lib:fluent_proto", diff --git a/ts/graphs/Graph.svelte b/ts/graphs/Graph.svelte index 8ee040fd4..03c34128f 100644 --- a/ts/graphs/Graph.svelte +++ b/ts/graphs/Graph.svelte @@ -43,6 +43,10 @@ display: none; } } + + &:focus { + outline: 0; + } } h1 { @@ -57,7 +61,7 @@ } -
+

{title}

{#if subtitle} diff --git a/ts/graphs/GraphsPage.svelte b/ts/graphs/GraphsPage.svelte index 2c52979e6..6d2488ed4 100644 --- a/ts/graphs/GraphsPage.svelte +++ b/ts/graphs/GraphsPage.svelte @@ -2,82 +2,49 @@ import "../sass/core.css"; import type { SvelteComponent } from "svelte/internal"; + import { writable } from "svelte/store"; import type { I18n } from "anki/i18n"; - import type { PreferenceStore } from "./preferences"; - import type pb from "anki/backend_proto"; - import { getGraphData, RevlogRange, daysToRevlogRange } from "./graph-helpers"; - import { getPreferences } from "./preferences"; import { bridgeCommand } from "anki/bridgecommand"; + import WithGraphData from "./WithGraphData.svelte"; + export let i18n: I18n; export let nightMode: boolean; export let graphs: SvelteComponent[]; - export let search: string; - export let days: number; + export let initialSearch: string; + export let initialDays: number; export let controller: SvelteComponent | null; - let active = false; - let sourceData: pb.BackendProto.GraphsOut | null = null; - let preferences: PreferenceStore | null = null; - let revlogRange: RevlogRange; + const search = writable(initialSearch); + const days = writable(initialDays); - const preferencesPromise = getPreferences(); - - const refreshWith = async (searchNew: string, days: number) => { - search = searchNew; - - active = true; - try { - [sourceData, preferences] = await Promise.all([ - getGraphData(search, days), - preferencesPromise, - ]); - revlogRange = daysToRevlogRange(days); - } catch (e) { - sourceData = null; - alert(e); - } - active = false; - }; - - const refresh = (event: CustomEvent) => { - refreshWith(event.detail.search, event.detail.days); - }; - - refreshWith(search, days); - - const browserSearch = (event: CustomEvent) => { - const query = `${search} ${event.detail.query}`; - bridgeCommand(`browserSearch:${query}`); - }; + function browserSearch(event: CustomEvent) { + bridgeCommand(`browserSearch: ${$search} ${event.detail.query}`); + } -
- {#if controller} - - {/if} +
+ + {#if controller} + + {/if} - {#if sourceData} -
+ {#if sourceData && preferences && revlogRange} {#each graphs as graph} {/each} -
- {/if} + {/if} +
diff --git a/ts/graphs/RangeBox.svelte b/ts/graphs/RangeBox.svelte index 553cca1c0..7e816eaf8 100644 --- a/ts/graphs/RangeBox.svelte +++ b/ts/graphs/RangeBox.svelte @@ -1,5 +1,5 @@ + + diff --git a/ts/graphs/index.ts b/ts/graphs/index.ts index 4e261cfd6..68e877e66 100644 --- a/ts/graphs/index.ts +++ b/ts/graphs/index.ts @@ -40,8 +40,8 @@ export function graphs( i18n, graphs, nightMode, - search, - days, + initialSearch: search, + initialDays: days, controller, }, }); diff --git a/ts/svelte/svelte.bzl b/ts/svelte/svelte.bzl index 90de9bbe0..894a1097d 100644 --- a/ts/svelte/svelte.bzl +++ b/ts/svelte/svelte.bzl @@ -67,6 +67,7 @@ def svelte_check(name = "svelte_check", srcs = []): ], data = [ "//ts:tsconfig.json", + "//ts/sveltelib", "//ts/lib", "//ts/lib:backend_proto", "@npm//sass", diff --git a/ts/sveltelib/BUILD.bazel b/ts/sveltelib/BUILD.bazel new file mode 100644 index 000000000..60cbfd715 --- /dev/null +++ b/ts/sveltelib/BUILD.bazel @@ -0,0 +1,32 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("//ts:prettier.bzl", "prettier_test") +load("//ts:eslint.bzl", "eslint_test") + +# Anki Library +################ + +ts_library( + name = "sveltelib", + srcs = glob(["**/*.ts"]), + data = [], + module_name = "sveltelib", + tsconfig = "//:tsconfig.json", + visibility = ["//visibility:public"], + deps = [ + "@npm//svelte", + "@npm//tslib", + ], +) + +# Tests +################ + +prettier_test( + name = "format_check", + srcs = glob(["*.ts"]), +) + +eslint_test( + name = "eslint", + srcs = glob(["*.ts"]), +) diff --git a/ts/sveltelib/async.ts b/ts/sveltelib/async.ts new file mode 100644 index 000000000..c6af59cc8 --- /dev/null +++ b/ts/sveltelib/async.ts @@ -0,0 +1,27 @@ +import { Readable, readable } from "svelte/store"; + +interface AsyncData { + value: Readable; + error: Readable; + loading: Readable; +} + +function useAsync(asyncFunction: () => Promise): AsyncData { + const promise = asyncFunction(); + + const value = readable(null, (set: (value: T) => void) => { + promise.then((value: T) => set(value)); + }); + + const error = readable(null, (set: (value: E) => void) => { + promise.catch((value: E) => set(value)); + }); + + const loading = readable(true, (set: (value: boolean) => void) => { + promise.finally(() => set(false)); + }); + + return { value, error, loading }; +} + +export default useAsync; diff --git a/ts/sveltelib/asyncReactive.ts b/ts/sveltelib/asyncReactive.ts new file mode 100644 index 000000000..0b0aced12 --- /dev/null +++ b/ts/sveltelib/asyncReactive.ts @@ -0,0 +1,49 @@ +import { Readable, derived } from "svelte/store"; + +interface AsyncReativeData { + value: Readable; + error: Readable; + loading: Readable; +} + +function useAsyncReactive( + asyncFunction: () => Promise, + dependencies: [Readable, ...Readable[]] +): AsyncReativeData { + const promise = derived( + dependencies, + (_, set: (value: Promise | null) => void): void => set(asyncFunction()), + // initialize with null to avoid duplicate fetch on init + null + ); + + const value = derived( + promise, + ($promise, set: (value: T) => void): void => { + $promise?.then((value: T) => set(value)); + }, + null + ); + + const error = derived( + promise, + ($promise, set: (error: E | null) => void): (() => void) => { + $promise?.catch((error: E) => set(error)); + return (): void => set(null); + }, + null + ); + + const loading = derived( + promise, + ($promise, set: (value: boolean) => void): (() => void) => { + $promise?.finally(() => set(false)); + return (): void => set(true); + }, + true + ); + + return { value, error, loading }; +} + +export default useAsyncReactive; diff --git a/ts/tsconfig.json b/ts/tsconfig.json index 883e2130b..eb5ce06d4 100644 --- a/ts/tsconfig.json +++ b/ts/tsconfig.json @@ -3,10 +3,11 @@ "compilerOptions": { "target": "es6", "module": "es6", - "lib": ["es2017", "es2019.array", "dom", "dom.iterable"], + "lib": ["es2017", "es2019.array", "es2018.promise", "dom", "dom.iterable"], "baseUrl": ".", "paths": { - "anki/*": ["../bazel-bin/ts/lib/*"] + "anki/*": ["../bazel-bin/ts/lib/*"], + "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"] }, "importsNotUsedAsValues": "error", "outDir": "dist",