refactor/hooks #7

Merged
tcoh merged 2 commits from refactor/hooks into master 2022-08-29 21:02:52 +00:00
24 changed files with 111 additions and 86 deletions

View File

@ -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 (
<article className="content-card">
<h2 className="content-row text-title margin-small">{name}</h2>
<span className="content-row text-meta margin-small">
{expression_count} expressions(s)
{expression_count_loading
? "loading"
: `${expression_count} expressions(s)`}
</span>
<p className="content-row text-details margin-paragraph">{description}</p>
</article>

View File

@ -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";

View File

@ -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());
}

View File

@ -0,0 +1,6 @@
import { useLiveQuery } from "dexie-react-hooks";
import { database } from "../model";
export function useAllExpressions() {
return useLiveQuery(() => database.expressions.toArray());
}

View File

@ -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 })

View File

@ -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]
);
}

View File

@ -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]
);
}

View File

@ -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]
);
}

View File

@ -1,9 +0,0 @@
import { useLiveQuery } from "dexie-react-hooks";
import { database } from "../model";
export function useExpressions() {
return useLiveQuery(
() => database.expressions.toArray(),
[database.expressions]
);
}

View File

@ -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 })

View File

@ -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;
}

View File

@ -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[]);
}

View File

@ -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;
}

View File

@ -14,6 +14,8 @@ export type IndexedExpression = WithId<Expression>;
export type IndexedExpressionSet = WithId<ExpressionSet>;
export type IndexedCategory = WithId<Category>;
export const ItemNotFoundError = Symbol();
class Database extends Dexie {
expressions!: Table<IndexedExpression, number>;
expression_sets!: Table<IndexedExpressionSet, number>;

View File

@ -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;

View File

@ -17,6 +17,8 @@ export function AddExpressionView() {
useState<string | undefined>(undefined);
const [error, setError] = useState<any>(undefined);
// TODO waiting fetch completion elements
return (
<form className="page-with-bottom-navigation">
<section className="padding-small scroll">

View File

@ -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 },
})
}
>

View File

@ -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 <ErrorView message="No valid card selected" />;
if (expression === undefined) return null; // LOADING
if (expression === ItemNotFoundError)
return <ErrorView message="Expression card not found" />;
return (
<div className="page-with-padding content-list scroll">
<ExpressionCard

View File

@ -1,12 +1,12 @@
import { useCallback, useContext } from "react";
import {
useExpressionFilterQueryIds,
useExpressionSetQueryId,
useQueryExpressionIdFilters,
useQueryExpressionSetId,
} from "../../hooks";
import { assignExpressionToSet } from "../../model";
import { AppRouting } from "../../model/routing";
// TODO fix promotion algorithm so it uses a destination expression_set_id
// TODO ensure that the destination expression set exists
export interface DemoteExpressionButtonProps {
expression_id: number;
@ -16,8 +16,8 @@ export function DemoteExpressionButton({
expression_id,
}: DemoteExpressionButtonProps) {
const { route, setRoute } = useContext(AppRouting);
const expression_set_id = useExpressionSetQueryId();
const expression_id_filters = useExpressionFilterQueryIds();
const expression_set_id = useQueryExpressionSetId();
const expression_id_filters = useQueryExpressionIdFilters();
const handleClick = useCallback(() => {
if (expression_set_id === 1) {
setRoute({

View File

@ -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 <ErrorView message="Expression set not found" />;
}
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

View File

@ -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 })

View File

@ -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 <ErrorView message="Expression set not found" />;
}
// Fallback for expression set empty
return (
<ExpressionSetDetailsViewImpl
expression_set={expression_set}

View File

@ -1,6 +1,6 @@
import { useContext } from "react";
import { ExpressionSetCard } from "../../components";
import { useExpressionsInSet } from "../../hooks";
import { useExpressionsByExpressionSetId } from "../../hooks";
import { IndexedExpressionSet } from "../../model";
import { AppPath, AppRouting } from "../../model/routing";
@ -10,7 +10,7 @@ export function ExpressionSetLink({
name,
}: IndexedExpressionSet) {
const { setRoute } = useContext(AppRouting);
const expressions = useExpressionsInSet(id!) || [];
const expressions = useExpressionsByExpressionSetId(id!);
return (
<div
@ -24,7 +24,8 @@ export function ExpressionSetLink({
<ExpressionSetCard
name={name}
description={description}
expression_count={expressions.length}
expression_count={expressions?.length || 0}
expression_count_loading={expressions === undefined}
/>
</div>
);

View File

@ -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 <ErrorView message="No expression sets found" />;