From fd24f46603ad28da5d32f79c8d5454674b6b02ac Mon Sep 17 00:00:00 2001 From: Egor Gorbachev <7gorbachevm@gmail.com> Date: Thu, 11 Jan 2024 13:58:35 +0700 Subject: [PATCH] Allow to remove card (#40) --- functions/db/card/delete-cards-in-ids.ts | 19 ++++++++ functions/upsert-deck.ts | 7 ++- src/screens/deck-form/card-form-view.tsx | 36 ++++++++++++---- src/screens/deck-form/card-form.tsx | 5 +-- .../deck-form/store/deck-form-store.ts | 43 +++++++++++++++++-- src/screens/deck-review/deck-preview.tsx | 11 ++--- src/screens/folder-review/folder-preview.tsx | 11 ++--- src/translations/t.ts | 16 +++++-- src/ui/button-grid.tsx | 19 ++++++++ 9 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 functions/db/card/delete-cards-in-ids.ts create mode 100644 src/ui/button-grid.tsx diff --git a/functions/db/card/delete-cards-in-ids.ts b/functions/db/card/delete-cards-in-ids.ts new file mode 100644 index 00000000..399a38b4 --- /dev/null +++ b/functions/db/card/delete-cards-in-ids.ts @@ -0,0 +1,19 @@ +import { EnvSafe } from "../../env/env-schema.ts"; +import { getDatabase } from "../get-database.ts"; +import { DatabaseException } from "../database-exception.ts"; + +export const deleteCardsInIds = async ( + env: EnvSafe, + cardsToRemoveIds: number[], +) => { + const db = getDatabase(env); + + const deleteCardsResult = await db + .from("deck_card") + .delete() + .in("id", cardsToRemoveIds); + + if (deleteCardsResult.error) { + throw new DatabaseException(deleteCardsResult.error); + } +}; diff --git a/functions/upsert-deck.ts b/functions/upsert-deck.ts index 36ddd59d..0fb778e4 100644 --- a/functions/upsert-deck.ts +++ b/functions/upsert-deck.ts @@ -25,6 +25,7 @@ import { CardToReviewDbType, getCardsToReviewDb, } from "./db/deck/get-cards-to-review-db.ts"; +import { deleteCardsInIds } from "./db/card/delete-cards-in-ids.ts"; const requestSchema = z.object({ id: z.number().nullable().optional(), @@ -33,6 +34,7 @@ const requestSchema = z.object({ speakLocale: z.string().nullable().optional(), speakField: z.string().nullable().optional(), folderId: z.number().nullable().optional(), + cardsToRemoveIds: z.array(z.number()).optional(), cards: z.array( z.object({ front: z.string(), @@ -95,7 +97,6 @@ export const onRequestPost = handleError(async ({ request, env }) => { throw new DatabaseException(upsertDeckResult.error); } - // Supabase returns an array as a result of upsert, that's why it gets validated against an array here const upsertedDeck = deckSchema.parse(upsertDeckResult.data); const updateCardsResult = await db.from("deck_card").upsert( @@ -129,6 +130,10 @@ export const onRequestPost = handleError(async ({ request, env }) => { throw new DatabaseException(createCardsResult.error); } + if (input.data.id && input.data.cardsToRemoveIds?.length) { + await deleteCardsInIds(envSafe, input.data.cardsToRemoveIds); + } + // If create deck if (!input.data.id) { await addDeckToMineDb(envSafe, { diff --git a/src/screens/deck-form/card-form-view.tsx b/src/screens/deck-form/card-form-view.tsx index 1094026b..28b04dbe 100644 --- a/src/screens/deck-form/card-form-view.tsx +++ b/src/screens/deck-form/card-form-view.tsx @@ -6,19 +6,22 @@ import { CardFormType } from "./store/deck-form-store.ts"; import { HintTransparent } from "../../ui/hint-transparent.tsx"; import { t } from "../../translations/t.ts"; import { Screen } from "../shared/screen.tsx"; -import { CenteredUnstyledButton } from "../../ui/centered-unstyled-button.tsx"; import { isFormValid } from "../../lib/mobx-form/form-has-error.ts"; +import { css } from "@emotion/css"; +import { ButtonSideAligned } from "../../ui/button-side-aligned.tsx"; +import { ButtonGrid } from "../../ui/button-grid.tsx"; type Props = { cardForm: CardFormType; onPreviewClick: () => void; + onDeleteClick?: () => void; }; export const CardFormView = observer((props: Props) => { - const { cardForm, onPreviewClick } = props; + const { cardForm, onPreviewClick, onDeleteClick } = props; return ( - + - {isFormValid(cardForm) && ( - - {t("card_preview")} - - )} +
+ + {isFormValid(cardForm) && ( + + {t("card_preview")} + + )} + {onDeleteClick && cardForm.id && ( + + {t("delete")} + + )} + +
); }); diff --git a/src/screens/deck-form/card-form.tsx b/src/screens/deck-form/card-form.tsx index 754ea1da..0a03b3f6 100644 --- a/src/screens/deck-form/card-form.tsx +++ b/src/screens/deck-form/card-form.tsx @@ -30,9 +30,8 @@ export const CardForm = observer(() => { return ( { - deckFormStore.isCardPreviewSelected.setTrue(); - }} + onPreviewClick={deckFormStore.isCardPreviewSelected.setTrue} + onDeleteClick={deckFormStore.markCardAsRemoved} /> ); }); diff --git a/src/screens/deck-form/store/deck-form-store.ts b/src/screens/deck-form/store/deck-form-store.ts index f137e79f..b9164f82 100644 --- a/src/screens/deck-form/store/deck-form-store.ts +++ b/src/screens/deck-form/store/deck-form-store.ts @@ -36,6 +36,7 @@ type DeckFormType = { speakingCardsLocale: TextField; speakingCardsField: TextField; folderId?: number; + cardsToRemoveIds: number[]; }; export const createDeckTitleField = (value: string) => { @@ -62,6 +63,7 @@ const createUpdateForm = ( back: createCardSideField(card.back), example: new TextField(card.example || ""), })), + cardsToRemoveIds: [], }; }; @@ -127,6 +129,7 @@ export class DeckFormStore { speakingCardsLocale: new TextField(null), speakingCardsField: new TextField(null), folderId: screen.folder?.id ?? undefined, + cardsToRemoveIds: [], }; } } @@ -278,7 +281,7 @@ export class DeckFormStore { return; } - this.onDeckSave()?.finally( + this.onDeckSave().finally( action(() => { this.cardFormIndex = undefined; this.cardFormType = undefined; @@ -312,16 +315,49 @@ export class DeckFormStore { } } + async markCardAsRemoved() { + const result = await showConfirm(t("deck_form_remove_card_confirm")); + if (!result) { + return; + } + + const selectedCard = this.cardForm; + if (!selectedCard) { + return; + } + assert(this.form, "markCardAsRemoved: form is empty"); + if (!selectedCard.id) { + return; + } + this.form.cardsToRemoveIds.push(selectedCard.id); + + deckListStore.isFullScreenLoaderVisible = true; + + this.onDeckSave() + .then( + action(() => { + this.isCardList = true; + this.cardFormIndex = undefined; + this.cardFormType = undefined; + }), + ) + .finally( + action(() => { + deckListStore.isFullScreenLoaderVisible = false; + }), + ); + } + onDeckSave() { assert(this.form, "onDeckSave: form is empty"); if (this.form.cards.length === 0) { showAlert(t("deck_form_no_cards_alert")); - return; + return Promise.reject(); } if (!isFormValid(this.form)) { - return; + return Promise.reject(); } this.isSending = true; @@ -341,6 +377,7 @@ export class DeckFormStore { speakLocale: this.form.speakingCardsLocale.value, speakField: this.form.speakingCardsField.value, folderId: this.form.folderId, + cardsToRemoveIds: this.form.cardsToRemoveIds, }) .then( action(({ deck, folders, cardsToReview }) => { diff --git a/src/screens/deck-review/deck-preview.tsx b/src/screens/deck-review/deck-preview.tsx index 4a3ff83b..1e839a8d 100644 --- a/src/screens/deck-review/deck-preview.tsx +++ b/src/screens/deck-review/deck-preview.tsx @@ -14,6 +14,7 @@ import { useTelegramProgress } from "../../lib/telegram/use-telegram-progress.ts import { duplicateDeckRequest } from "../../api/api.ts"; import { t } from "../../translations/t.ts"; import { userStore } from "../../store/user-store.ts"; +import { ButtonGrid } from "../../ui/button-grid.tsx"; export const DeckPreview = observer(() => { const reviewStore = useReviewStore(); @@ -122,13 +123,7 @@ export const DeckPreview = observer(() => { )} -
+ {deckListStore.canEditDeck ? ( { {t("delete")} ) : null} -
+ {deck.cardsToReview.length === 0 && ( {t("no_cards_to_review_in_deck")} diff --git a/src/screens/folder-review/folder-preview.tsx b/src/screens/folder-review/folder-preview.tsx index fe8e32ab..d58c1ac7 100644 --- a/src/screens/folder-review/folder-preview.tsx +++ b/src/screens/folder-review/folder-preview.tsx @@ -16,6 +16,7 @@ import { ListHeader } from "../../ui/list-header.tsx"; import { assert } from "../../lib/typescript/assert.ts"; import { userStore } from "../../store/user-store.ts"; import { DeckRowWithCardsToReview } from "../shared/deck-row-with-cards-to-review/deck-row-with-cards-to-review.tsx"; +import { ButtonGrid } from "../../ui/button-grid.tsx"; export const FolderPreview = observer(() => { const reviewStore = useReviewStore(); @@ -126,13 +127,7 @@ export const FolderPreview = observer(() => { )} -
+ {deckListStore.canEditFolder ? ( { > {t("delete")} -
+
{ + const { children } = props; + return ( +
+ {children} +
+ ); +};