diff --git a/src/components/ExpressionSetCard/ExpressionSetCard.tsx b/src/components/ExpressionSetCard/ExpressionSetCard.tsx index c2d48c6..a7e82fa 100644 --- a/src/components/ExpressionSetCard/ExpressionSetCard.tsx +++ b/src/components/ExpressionSetCard/ExpressionSetCard.tsx @@ -2,18 +2,22 @@ export interface ExpressionSetCardProps { name: string; description: string; expression_count: number; + expression_count_loading?: boolean; } export function ExpressionSetCard({ name, description, expression_count, + expression_count_loading, }: ExpressionSetCardProps) { return (

{name}

- {expression_count} expressions(s) + {expression_count_loading + ? "loading" + : `${expression_count} expressions(s)`}

{description}

diff --git a/src/hooks/index.ts b/src/hooks/index.ts index ba7648a..e435ba3 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,9 +1,9 @@ -export * from "./useExpressionCategories"; +export * from "./useAllExpressions"; +export * from "./useAllExpressionSets"; +export * from "./useCategoriesByExpressionId"; export * from "./useExpressionById"; -export * from "./useExpressionFilterQueryIds"; -export * from "./useExpressionQueryId"; -export * from "./useExpressions"; -export * from "./useExpressionSet"; -export * from "./useExpressionSetQueryId"; -export * from "./useExpressionSets"; -export * from "./useExpressionsInSet"; +export * from "./useExpressionsByExpressionSetId"; +export * from "./useExpressionSetById"; +export * from "./useQueryExpressionId"; +export * from "./useQueryExpressionIdFilters"; +export * from "./useQueryExpressionSetId"; diff --git a/src/hooks/useExpressionSets.ts b/src/hooks/useAllExpressionSets.ts similarity index 79% rename from src/hooks/useExpressionSets.ts rename to src/hooks/useAllExpressionSets.ts index 280a4fc..f706c56 100644 --- a/src/hooks/useExpressionSets.ts +++ b/src/hooks/useAllExpressionSets.ts @@ -1,6 +1,6 @@ import { useLiveQuery } from "dexie-react-hooks"; import { database } from "../model"; -export function useExpressionSets() { +export function useAllExpressionSets() { return useLiveQuery(() => database.expression_sets.toArray()); } diff --git a/src/hooks/useAllExpressions.ts b/src/hooks/useAllExpressions.ts new file mode 100644 index 0000000..ebecaa6 --- /dev/null +++ b/src/hooks/useAllExpressions.ts @@ -0,0 +1,6 @@ +import { useLiveQuery } from "dexie-react-hooks"; +import { database } from "../model"; + +export function useAllExpressions() { + return useLiveQuery(() => database.expressions.toArray()); +} diff --git a/src/hooks/useExpressionCategories.ts b/src/hooks/useCategoriesByExpressionId.ts similarity index 61% rename from src/hooks/useExpressionCategories.ts rename to src/hooks/useCategoriesByExpressionId.ts index 96052a6..31501a3 100644 --- a/src/hooks/useExpressionCategories.ts +++ b/src/hooks/useCategoriesByExpressionId.ts @@ -1,7 +1,10 @@ import { useLiveQuery } from "dexie-react-hooks"; import { database } from "../model"; -export function useExpressionCategories(expression_id: number) { +// TODO there may be a case here for reporting errors where relations exist +// but some referenced item does not exist in its table, maybe the +// return value should be { entries[], errors[] } +export function useCategoriesByExpressionId(expression_id: number) { return useLiveQuery(() => { return database.expression_to_category .where({ expression_id }) diff --git a/src/hooks/useExpressionById.ts b/src/hooks/useExpressionById.ts index 3af7ccc..08d371b 100644 --- a/src/hooks/useExpressionById.ts +++ b/src/hooks/useExpressionById.ts @@ -1,9 +1,16 @@ import { useLiveQuery } from "dexie-react-hooks"; -import { database, IndexedExpression } from "../model"; +import { database, ItemNotFoundError } from "../model"; -export function useExpressionById(id?: number): IndexedExpression | undefined { - return useLiveQuery(() => { - if (id === undefined) return undefined; - return database.expressions.where({ id }).first(); - }, [id]); +export function useExpressionById(expression_id: number) { + return useLiveQuery( + () => + database.expressions + .where({ id: expression_id }) + .toArray() + .then((result) => { + if (result.length === 0) return ItemNotFoundError; + return result[0]; + }), + [expression_id] + ); } diff --git a/src/hooks/useExpressionSet.ts b/src/hooks/useExpressionSet.ts deleted file mode 100644 index 442600f..0000000 --- a/src/hooks/useExpressionSet.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useLiveQuery } from "dexie-react-hooks"; -import { database } from "../model"; - -export function useExpressionSet(id: number) { - return useLiveQuery( - () => database.expression_sets.where({ id }).first(), - [id] - ); -} diff --git a/src/hooks/useExpressionSetById.ts b/src/hooks/useExpressionSetById.ts new file mode 100644 index 0000000..fe0d827 --- /dev/null +++ b/src/hooks/useExpressionSetById.ts @@ -0,0 +1,16 @@ +import { useLiveQuery } from "dexie-react-hooks"; +import { database, ItemNotFoundError } from "../model"; + +export function useExpressionSetById(expression_set_id: number) { + return useLiveQuery( + () => + database.expression_sets + .where({ id: expression_set_id }) + .toArray() + .then((result) => { + if (result.length === 0) return ItemNotFoundError; + return result[0]; + }), + [expression_set_id] + ); +} diff --git a/src/hooks/useExpressions.ts b/src/hooks/useExpressions.ts deleted file mode 100644 index 162dfea..0000000 --- a/src/hooks/useExpressions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useLiveQuery } from "dexie-react-hooks"; -import { database } from "../model"; - -export function useExpressions() { - return useLiveQuery( - () => database.expressions.toArray(), - [database.expressions] - ); -} diff --git a/src/hooks/useExpressionsInSet.ts b/src/hooks/useExpressionsByExpressionSetId.ts similarity index 61% rename from src/hooks/useExpressionsInSet.ts rename to src/hooks/useExpressionsByExpressionSetId.ts index c4d77a6..1628270 100644 --- a/src/hooks/useExpressionsInSet.ts +++ b/src/hooks/useExpressionsByExpressionSetId.ts @@ -1,7 +1,10 @@ import { useLiveQuery } from "dexie-react-hooks"; import { database } from "../model"; -export function useExpressionsInSet(expression_set_id: number) { +// TODO there may be a case here for reporting errors where relations exist +// but some referenced item does not exist in its table, maybe the +// return value should be { entries[], errors[] } +export function useExpressionsByExpressionSetId(expression_set_id: number) { return useLiveQuery(() => { return database.expression_to_expression_set .where({ expression_set_id }) diff --git a/src/hooks/useExpressionQueryId.ts b/src/hooks/useQueryExpressionId.ts similarity index 55% rename from src/hooks/useExpressionQueryId.ts rename to src/hooks/useQueryExpressionId.ts index 2476b20..3f15022 100644 --- a/src/hooks/useExpressionQueryId.ts +++ b/src/hooks/useQueryExpressionId.ts @@ -1,7 +1,7 @@ import { useContext } from "react"; import { AppRouting } from "../model/routing"; -export function useExpressionQueryId(): number | undefined { +export function useQueryExpressionId() { const { route } = useContext(AppRouting); - return route.options?.expression_card_id; + return route.options?.expression_id || 0; } diff --git a/src/hooks/useExpressionFilterQueryIds.ts b/src/hooks/useQueryExpressionIdFilters.ts similarity index 53% rename from src/hooks/useExpressionFilterQueryIds.ts rename to src/hooks/useQueryExpressionIdFilters.ts index d93442e..173462f 100644 --- a/src/hooks/useExpressionFilterQueryIds.ts +++ b/src/hooks/useQueryExpressionIdFilters.ts @@ -1,7 +1,7 @@ import { useContext } from "react"; import { AppRouting } from "../model/routing"; -export function useExpressionFilterQueryIds(): number[] { +export function useQueryExpressionIdFilters() { const { route } = useContext(AppRouting); - return route.options?.expression_id_filters || []; + return route.options?.expression_id_filters || ([] as number[]); } diff --git a/src/hooks/useExpressionSetQueryId.ts b/src/hooks/useQueryExpressionSetId.ts similarity index 77% rename from src/hooks/useExpressionSetQueryId.ts rename to src/hooks/useQueryExpressionSetId.ts index 859eec3..b8fec3f 100644 --- a/src/hooks/useExpressionSetQueryId.ts +++ b/src/hooks/useQueryExpressionSetId.ts @@ -1,7 +1,7 @@ import { useContext } from "react"; import { AppRouting } from "../model/routing"; -export function useExpressionSetQueryId(): number { +export function useQueryExpressionSetId() { const { route } = useContext(AppRouting); return route.options?.expression_set_id || 0; } diff --git a/src/model/database.ts b/src/model/database.ts index f04a13d..ec7b7e2 100644 --- a/src/model/database.ts +++ b/src/model/database.ts @@ -14,6 +14,8 @@ export type IndexedExpression = WithId; export type IndexedExpressionSet = WithId; export type IndexedCategory = WithId; +export const ItemNotFoundError = Symbol(); + class Database extends Dexie { expressions!: Table; expression_sets!: Table; diff --git a/src/model/routing.ts b/src/model/routing.ts index adef827..78a110f 100644 --- a/src/model/routing.ts +++ b/src/model/routing.ts @@ -14,7 +14,7 @@ export enum AppPath { export interface RouteOptions { // Used in cards view - expression_card_id?: number; + expression_id?: number; // Used in practice view expression_set_id?: number; diff --git a/src/views/AddExpressionView/AddExpressionView.tsx b/src/views/AddExpressionView/AddExpressionView.tsx index 4c9f1ad..780196b 100644 --- a/src/views/AddExpressionView/AddExpressionView.tsx +++ b/src/views/AddExpressionView/AddExpressionView.tsx @@ -17,6 +17,8 @@ export function AddExpressionView() { useState(undefined); const [error, setError] = useState(undefined); + // TODO waiting fetch completion elements + return (
diff --git a/src/views/ExpressionCardListView/ExpressionCardListView.tsx b/src/views/ExpressionCardListView/ExpressionCardListView.tsx index ba5324e..e2e0603 100644 --- a/src/views/ExpressionCardListView/ExpressionCardListView.tsx +++ b/src/views/ExpressionCardListView/ExpressionCardListView.tsx @@ -1,13 +1,13 @@ import { useContext, useMemo } from "react"; import { ExpressionCard } from "../../components"; -import { useExpressions } from "../../hooks"; +import { useAllExpressions } from "../../hooks"; import { IndexedExpression } from "../../model"; import { AppPath, AppRouting } from "../../model/routing"; import { ErrorView } from "../ErrorView"; export function ExpressionCardListView() { const { setRoute } = useContext(AppRouting); - const expressions = useExpressions(); + const expressions = useAllExpressions(); const expression_list = useMemo( () => (expressions || []).concat().sort(sort_function), [expressions] @@ -27,7 +27,7 @@ export function ExpressionCardListView() { onClick={() => setRoute({ path: AppPath.CardView, - options: { expression_card_id: id }, + options: { expression_id: id }, }) } > diff --git a/src/views/ExpressionCardView/ExpressionCardView.tsx b/src/views/ExpressionCardView/ExpressionCardView.tsx index e250b99..2bc545a 100644 --- a/src/views/ExpressionCardView/ExpressionCardView.tsx +++ b/src/views/ExpressionCardView/ExpressionCardView.tsx @@ -1,12 +1,17 @@ import { ExpressionCard } from "../../components"; import { ExpressionDescription } from "../../components/ExpressionDescription"; -import { useExpressionById, useExpressionQueryId } from "../../hooks"; +import { useExpressionById, useQueryExpressionId } from "../../hooks"; +import { ItemNotFoundError } from "../../model"; import { ErrorView } from "../ErrorView"; export function ExpressionCardView() { - const expression_id = useExpressionQueryId(); + const expression_id = useQueryExpressionId(); const expression = useExpressionById(expression_id); - if (!expression) return ; + + if (expression === undefined) return null; // LOADING + if (expression === ItemNotFoundError) + return ; + return (
{ if (expression_set_id === 1) { setRoute({ diff --git a/src/views/ExpressionPracticeView/ExpressionPracticeView.tsx b/src/views/ExpressionPracticeView/ExpressionPracticeView.tsx index 09929e7..9a90839 100644 --- a/src/views/ExpressionPracticeView/ExpressionPracticeView.tsx +++ b/src/views/ExpressionPracticeView/ExpressionPracticeView.tsx @@ -1,9 +1,9 @@ import { useMemo } from "react"; import { - useExpressionCategories, - useExpressionFilterQueryIds, - useExpressionSetQueryId, - useExpressionsInSet, + useCategoriesByExpressionId, + useQueryExpressionIdFilters, + useQueryExpressionSetId, + useExpressionsByExpressionSetId, } from "../../hooks"; import { IndexedExpression } from "../../model"; import { sample } from "../../util"; @@ -11,18 +11,13 @@ import { ErrorView } from "../ErrorView"; import { ExpressionPracticeCardView } from "./ExpressionPracticeCardView"; export function ExpressionPracticeView() { - const expression_set_id = useExpressionSetQueryId(); - const filter_ids = useExpressionFilterQueryIds(); - const expressions = useExpressionsInSet(expression_set_id); + const expression_set_id = useQueryExpressionSetId(); + const filter_ids = useQueryExpressionIdFilters(); + const expressions = useExpressionsByExpressionSetId(expression_set_id); + // TODO handle errors - // Fallback rendering for content not yet fetched - if (!expressions) return null; - if (!filter_ids) return null; + if (!expressions) return null; // LOADING - // Fallback views for expression set content not found - if (!expression_set_id) { - return ; - } const filtered_expressions = expressions.filter( (expression) => !filter_ids.includes(expression.id!) ); @@ -45,7 +40,7 @@ function ExpressionPracticeViewImpl({ select_from_expressions: expressions, }: ExpressionPracticeViewImplProps) { const expression = useMemo(() => sample(expressions), [expressions]); - const categories = useExpressionCategories(expression.id!); + const categories = useCategoriesByExpressionId(expression.id!); if (!categories) return null; // Loading // Delegate internal interaction state to next component so it resets diff --git a/src/views/ExpressionPracticeView/PromoteExpressionButton.tsx b/src/views/ExpressionPracticeView/PromoteExpressionButton.tsx index 83df99e..a065ca5 100644 --- a/src/views/ExpressionPracticeView/PromoteExpressionButton.tsx +++ b/src/views/ExpressionPracticeView/PromoteExpressionButton.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { useExpressionSetQueryId } from "../../hooks"; +import { useQueryExpressionSetId } from "../../hooks"; import { assignExpressionToSet, database, removeExpression } from "../../model"; export interface PromoteExpressionButtonProps { @@ -12,7 +12,7 @@ export interface PromoteExpressionButtonProps { export function PromoteExpressionButton({ expression_id, }: PromoteExpressionButtonProps) { - const expression_set_id = useExpressionSetQueryId(); + const expression_set_id = useQueryExpressionSetId(); const handleClick = useCallback(async () => { const existing_next_level = await database.expression_sets .where({ id: expression_set_id + 1 }) diff --git a/src/views/ExpressionSetDetailsView/ExpressionSetDetailsView.tsx b/src/views/ExpressionSetDetailsView/ExpressionSetDetailsView.tsx index 183c8d6..ab6c218 100644 --- a/src/views/ExpressionSetDetailsView/ExpressionSetDetailsView.tsx +++ b/src/views/ExpressionSetDetailsView/ExpressionSetDetailsView.tsx @@ -1,26 +1,24 @@ import { useContext } from "react"; import { ExpressionSetInfo } from "../../components"; import { - useExpressionSet, - useExpressionSetQueryId, - useExpressionsInSet, + useExpressionSetById, + useQueryExpressionSetId, + useExpressionsByExpressionSetId, } from "../../hooks"; -import { IndexedExpressionSet } from "../../model"; +import { IndexedExpressionSet, ItemNotFoundError } from "../../model"; import { AppPath, AppRouting } from "../../model/routing"; import { ErrorView } from "../ErrorView"; export function ExpressionSetDetailsView() { - const expression_set_id = useExpressionSetQueryId(); - const expression_set = useExpressionSet(expression_set_id); - const expressions = useExpressionsInSet(expression_set_id); + const expression_set_id = useQueryExpressionSetId(); + const expression_set = useExpressionSetById(expression_set_id); + const expressions = useExpressionsByExpressionSetId(expression_set_id); - // Fallback for expression set not found - if (!expressions) return null; - if (!expression_set) { + if (!expression_set || !expressions) return null; // LOADING + if (expression_set === ItemNotFoundError) { return ; } - // Fallback for expression set empty return (
); diff --git a/src/views/ExpressionSetListView/ExpressionSetListView.tsx b/src/views/ExpressionSetListView/ExpressionSetListView.tsx index bf935eb..38e3214 100644 --- a/src/views/ExpressionSetListView/ExpressionSetListView.tsx +++ b/src/views/ExpressionSetListView/ExpressionSetListView.tsx @@ -1,9 +1,10 @@ -import { useExpressionSets } from "../../hooks"; +import { useAllExpressionSets } from "../../hooks"; import { ErrorView } from "../ErrorView"; import { ExpressionSetLink } from "./ExpressionSetLink"; export function ExpressionSetListView() { - const expression_sets = useExpressionSets(); + const expression_sets = useAllExpressionSets(); + if (!expression_sets) return null; // LOADING if (!expression_sets.length) { return ;