Skip to content

Commit

Permalink
Show & handle more errors (#33)
Browse files Browse the repository at this point in the history
* Show & handle more errors
  • Loading branch information
kubk authored Apr 17, 2024
1 parent 5fb9471 commit 9a5c03a
Show file tree
Hide file tree
Showing 33 changed files with 431 additions and 352 deletions.
20 changes: 20 additions & 0 deletions src/lib/mobx-request/request-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,23 @@ test("request - cache", async () => {
await when(() => request2.result.status === "success");
expect(fn2).toBeCalledTimes(2);
});

test("request - loading - default", async () => {
const fn = () => new Promise((resolve) => setTimeout(resolve, 300));
const request = new RequestStore(fn);
request.execute();
expect(request.result.status).toBe("loading");
await when(() => request.result.status === "success");
request.execute();
expect(request.result.status).toBe("loading");
});

test("request - loading - swr", async () => {
const fn = () => new Promise((resolve) => setTimeout(resolve, 300));
const request = new RequestStore(fn, { staleWhileRevalidate: true });
request.execute();
expect(request.result.status).toBe("loading");
await when(() => request.result.status === "success");
request.execute();
expect(request.result.status).toBe("success");
});
8 changes: 7 additions & 1 deletion src/lib/mobx-request/request-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type ExecuteResult<T> = SuccessResult<T> | ErrorResult;

type Options = {
cacheId?: string;
staleWhileRevalidate?: boolean;
};

const cacheStorage = new Map<string, any>();
Expand Down Expand Up @@ -37,7 +38,12 @@ export class RequestStore<T, Args extends any[] = []> {
return this.result as unknown as ExecuteResult<T>;
}

this.result = { data: null, status: "loading" };
if (
!this.options?.staleWhileRevalidate ||
this.result.status !== "success"
) {
this.result = { data: null, status: "loading" };
}

try {
const data = await this.fetchFn(...args);
Expand Down
8 changes: 6 additions & 2 deletions src/lib/telegram/haptics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import WebApp from "@twa-dev/sdk";

const isIos = WebApp.platform === "ios";

export const hapticNotification = (type: "error" | "success" | "warning") => {
export type HapticNotificationType = "error" | "success" | "warning";

export const hapticNotification = (type: HapticNotificationType) => {
if (!isIos) {
return;
}
WebApp.HapticFeedback.notificationOccurred(type);
};

export const hapticImpact = (type: "light" | "medium" | "heavy") => {
export type HapticImpactType = "light" | "medium" | "heavy";

export const hapticImpact = (type: HapticImpactType) => {
if (!isIos) {
return;
}
Expand Down
29 changes: 19 additions & 10 deletions src/lib/telegram/use-main-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,31 @@ import { autorun } from "mobx";
// Track visible state to avoid flickering
let isVisible = false;

const hide = () => {
if (WebApp.platform !== "ios" && WebApp.platform !== "android") {
WebApp.MainButton.hide();
isVisible = false;
return;
}

// Avoid flickering of the Telegram main button
isVisible = false;
setTimeout(() => {
if (isVisible) {
return;
}
WebApp.MainButton.hide();
isVisible = false;
}, 100);
};

export const useMainButton = (
text: string | (() => string),
onClick: () => void,
condition?: () => boolean,
) => {
const hideMainButton = () => {
// Avoid flickering of the Telegram main button
isVisible = false;
setTimeout(() => {
if (isVisible) {
return;
}
WebApp.MainButton.hide();
isVisible = false;
}, 300);

hide();
WebApp.MainButton.offClick(onClick);
WebApp.MainButton.hideProgress();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ vi.mock("../../../translations/t.ts", () => {
};
});

vi.mock("../../shared/snackbar.tsx", () => {
vi.mock("../../shared/snackbar/snackbar.tsx", () => {
return {
showSnackBar: vi.fn(),
notifyError: vi.fn(),
notifySuccess: vi.fn(),
};
});

Expand Down
4 changes: 2 additions & 2 deletions src/screens/ai-mass-creation/store/ai-mass-creation-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { RequestStore } from "../../../lib/mobx-request/request-store.ts";
import { screenStore } from "../../../store/screen-store.ts";
import { assert } from "../../../lib/typescript/assert.ts";
import { notifySuccess } from "../../shared/snackbar.tsx";
import { notifySuccess } from "../../shared/snackbar/snackbar.tsx";
import { deckListStore } from "../../../store/deck-list-store.ts";
import { showConfirm } from "../../../lib/telegram/show-confirm.ts";

Expand Down Expand Up @@ -228,7 +228,7 @@ export class AiMassCreationStore {
cards: this.massCreationForm.cards.value,
});

if (result.status !== "success") {
if (result.status === "error") {
throw new Error("Failed to add cards");
}

Expand Down
3 changes: 2 additions & 1 deletion src/screens/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import { isRunningWithinTelegram } from "../lib/telegram/is-running-within-teleg
import { FreezeCardsScreenLazy } from "./freeze-cards/freeze-cards-screen-lazy.tsx";
import { AiMassCreationScreen } from "./ai-mass-creation/ai-mass-creation-screen.tsx";
import { AiMassCreationStoreProvider } from "./ai-mass-creation/store/ai-mass-creation-store-provider.tsx";
import { SnackbarProviderWrapper } from "./shared/snackbar.tsx";

import { SnackbarProviderWrapper } from "./shared/snackbar/snackbar-provider-wrapper.tsx";

export const App = observer(() => {
useRestoreFullScreenExpand();
Expand Down
9 changes: 3 additions & 6 deletions src/screens/component-catalog/snackbar-story.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import React from "react";
import {
notifyError,
notifySuccess,
SnackbarProviderWrapper,
} from "../shared/snackbar.tsx";
import { notifyError, notifySuccess } from "../shared/snackbar/snackbar.tsx";
import { SnackbarProviderWrapper } from "../shared/snackbar/snackbar-provider-wrapper.tsx";

export const SnackbarStory = () => {
return (
Expand All @@ -13,7 +10,7 @@ export const SnackbarStory = () => {
Show success snackbar
</button>

<button onClick={() => notifyError("This is a success message")}>
<button onClick={() => notifyError({}, { duration: 10000 })}>
Show error snackbar
</button>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/screens/deck-form/store/deck-form-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ vi.mock("../../../store/user-store.ts", () => {
};
});

vi.mock("../../shared/snackbar/snackbar.tsx", () => {
return {
notifyError: vi.fn(),
notifySuccess: vi.fn(),
};
});

describe("deck form store", () => {
afterEach(() => {
vi.clearAllMocks();
Expand Down
72 changes: 38 additions & 34 deletions src/screens/deck-form/store/deck-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
TextField,
validators,
} from "mobx-form-lite";
import { action, makeAutoObservable } from "mobx";
import { action, makeAutoObservable, runInAction } from "mobx";
import { assert } from "../../../lib/typescript/assert.ts";
import { upsertDeckRequest } from "../../../api/api.ts";
import { screenStore } from "../../../store/screen-store.ts";
Expand All @@ -33,6 +33,8 @@ import {
} from "./card-form-store-interface.ts";
import { UpsertDeckRequest } from "../../../../functions/upsert-deck.ts";
import { UnwrapArray } from "../../../lib/typescript/unwrap-array.ts";
import { RequestStore } from "../../../lib/mobx-request/request-store.ts";
import { notifyError } from "../../shared/snackbar/snackbar.tsx";

export type CardAnswerFormType = {
id: string;
Expand Down Expand Up @@ -171,7 +173,7 @@ export class DeckFormStore implements CardFormStoreInterface {
cardFormIndex?: number;
cardFormType?: "new" | "edit";
form?: DeckFormType;
isSending = false;
upsertDeckRequest = new RequestStore(upsertDeckRequest);
cardInnerScreen = new TextField<CardInnerScreenType>(null);
deckInnerScreen?: "cardList" | "speakingCards";
cardFilter = {
Expand All @@ -184,6 +186,10 @@ export class DeckFormStore implements CardFormStoreInterface {
makeAutoObservable(this, {}, { autoBind: true });
}

get isSending() {
return this.upsertDeckRequest.isLoading;
}

get deckFormScreen() {
if (this.cardFormIndex !== undefined) {
return "cardForm";
Expand Down Expand Up @@ -364,7 +370,7 @@ export class DeckFormStore implements CardFormStoreInterface {

onSaveCard() {
const isEdit = this.cardForm?.id;
this.onDeckSave().then(
this.onDeckSave(
action(() => {
if (isEdit) {
return;
Expand Down Expand Up @@ -474,22 +480,20 @@ export class DeckFormStore implements CardFormStoreInterface {

deckListStore.isFullScreenLoaderVisible = true;

this.onDeckSave()
.then(
action(() => {
this.deckInnerScreen = "cardList";
this.cardFormIndex = undefined;
this.cardFormType = undefined;
}),
)
.finally(
action(() => {
deckListStore.isFullScreenLoaderVisible = false;
}),
);
this.onDeckSave(
action(() => {
this.deckInnerScreen = "cardList";
this.cardFormIndex = undefined;
this.cardFormType = undefined;
}),
).finally(
action(() => {
deckListStore.isFullScreenLoaderVisible = false;
}),
);
}

onDeckSave() {
async onDeckSave(onSuccess?: () => void) {
assert(this.form, "onDeckSave: form is empty");

if (this.form.cards.length === 0) {
Expand All @@ -508,8 +512,6 @@ export class DeckFormStore implements CardFormStoreInterface {
return Promise.reject();
}

this.isSending = true;

// Avoid sending huge collections on every save
// Only new and touched cards are sent to the server
const newCards = this.form.cards.filter((card) => !card.id);
Expand All @@ -518,7 +520,7 @@ export class DeckFormStore implements CardFormStoreInterface {
);
const cardsToSend = newCards.concat(touchedCards).map(cardFormToApi);

return upsertDeckRequest({
const result = await this.upsertDeckRequest.execute({
id: this.form.id,
title: this.form.title.value,
description: this.form.description.value,
Expand All @@ -527,20 +529,22 @@ export class DeckFormStore implements CardFormStoreInterface {
speakField: this.form.speakingCardsField.value,
folderId: this.form.folderId,
cardsToRemoveIds: this.form.cardsToRemoveIds,
})
.then(
action(({ deck, folders, cardsToReview }) => {
this.form = createUpdateForm(deck.id, deck, () => this.cardForm);
deckListStore.replaceDeck(deck, true);
deckListStore.updateFolders(folders);
deckListStore.updateCardsToReview(cardsToReview);
}),
)
.finally(
action(() => {
this.isSending = false;
}),
);
});

if (result.status === "error") {
notifyError({ e: result.error, info: "Error saving deck" });
return;
}

const { deck, folders, cardsToReview } = result.data;

runInAction(() => {
this.form = createUpdateForm(deck.id, deck, () => this.cardForm);
deckListStore.replaceDeck(deck, true);
deckListStore.updateFolders(folders);
deckListStore.updateCardsToReview(cardsToReview);
onSuccess?.();
});
}

quitCardForm() {
Expand Down
33 changes: 18 additions & 15 deletions src/screens/deck-form/store/quick-add-card-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
createAnswerTypeField,
createCardSideField,
} from "./deck-form-store.ts";
import { action, makeAutoObservable } from "mobx";
import { makeAutoObservable } from "mobx";
import {
formTouchAll,
isFormDirty,
Expand All @@ -24,6 +24,8 @@ import {
CardInnerScreenType,
} from "./card-form-store-interface.ts";
import { DeckSpeakFieldEnum } from "../../../../functions/db/deck/decks-with-cards-schema.ts";
import { RequestStore } from "../../../lib/mobx-request/request-store.ts";
import { notifyError, notifySuccess } from "../../shared/snackbar/snackbar.tsx";

export class QuickAddCardFormStore implements CardFormStoreInterface {
cardForm: CardFormType = {
Expand All @@ -34,7 +36,7 @@ export class QuickAddCardFormStore implements CardFormStoreInterface {
options: null,
answers: createAnswerListField([], () => this.cardForm),
};
isSending = false;
addCardRequest = new RequestStore(addCardRequest);
cardInnerScreen = new TextField<CardInnerScreenType>(null);

constructor(
Expand All @@ -46,7 +48,11 @@ export class QuickAddCardFormStore implements CardFormStoreInterface {
makeAutoObservable(this, {}, { autoBind: true });
}

onSaveCard() {
get isSending() {
return this.addCardRequest.isLoading;
}

async onSaveCard() {
if (!isFormValid(this.cardForm)) {
formTouchAll(this.cardForm);
return;
Expand All @@ -55,8 +61,6 @@ export class QuickAddCardFormStore implements CardFormStoreInterface {
const screen = screenStore.screen;
assert(screen.type === "cardQuickAddForm");

this.isSending = true;

const body: AddCardRequest = {
deckId: screen.deckId,
card: {
Expand All @@ -72,16 +76,15 @@ export class QuickAddCardFormStore implements CardFormStoreInterface {
},
};

return addCardRequest(body)
.then(() => {
screenStore.back();
deckListStore.load();
})
.finally(
action(() => {
this.isSending = false;
}),
);
const result = await this.addCardRequest.execute(body);
if (result.status === "error") {
notifyError({ e: result.error, info: "Error adding quick card" });
return;
}

screenStore.back();
deckListStore.load();
notifySuccess(t("card_added"));
}

async onBackCard() {
Expand Down
Loading

0 comments on commit 9a5c03a

Please sign in to comment.