diff --git a/web/src/vocab/term-row.stories.tsx b/web/src/vocab/term-row.stories.tsx new file mode 100644 index 0000000..c13aaa3 --- /dev/null +++ b/web/src/vocab/term-row.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { expect, userEvent } from 'storybook/test' + +import { TermRow } from './term-row' + +const meta = { + component: TermRow, + tags: ['ai-generated'], + args: { + vocabularyId: 'v1', + lang: 'en', + term: { id: 't1', external_uri: null, labels: [{ lang: 'en', label: 'Wood' }] }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Display: Story = { + play: async ({ canvas }) => { + await expect(canvas.getByText('Wood')).toBeVisible() + }, +} + +export const TogglesEdit: Story = { + play: async ({ canvas }) => { + await userEvent.click(canvas.getByRole('button', { name: 'Edit' })) + await expect(canvas.getByRole('button', { name: 'Save' })).toBeVisible() + }, +} + +export const CancelsEdit: Story = { + play: async ({ canvas }) => { + await userEvent.click(canvas.getByRole('button', { name: 'Edit' })) + await userEvent.click(canvas.getByRole('button', { name: 'Cancel' })) + await expect(canvas.getByText('Wood')).toBeVisible() + }, +} diff --git a/web/src/vocab/term-row.tsx b/web/src/vocab/term-row.tsx new file mode 100644 index 0000000..64b8af3 --- /dev/null +++ b/web/src/vocab/term-row.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import type { components } from "../api/schema"; +import { useUpdateTerm, useDeleteTerm } from "../api/queries"; +import { LabelEditor } from "../components/label-editor"; +import { DeleteConfirmDialog } from "../components/delete-confirm-dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { labelText } from "../lib/labels"; + +type TermView = components["schemas"]["TermView"]; +type LabelInput = components["schemas"]["LabelInput"]; + +export function TermRow({ vocabularyId, term, lang }: { vocabularyId: string; term: TermView; lang: string }) { + const { t } = useTranslation(); + + const updateTerm = useUpdateTerm(); + const deleteTerm = useDeleteTerm(); + + const [editing, setEditing] = useState(false); + const [labels, setLabels] = useState(term.labels as LabelInput[]); + const [uri, setUri] = useState(term.external_uri ?? ""); + + if (editing) { + return ( +
  • + +
    + + setUri(e.target.value)} /> +
    +
    + + +
    +
  • + ); + } + + return ( +
  • + {labelText(term.labels, lang)} + + deleteTerm.mutateAsync({ vocabularyId, termId: term.id })} + /> +
  • + ); +} diff --git a/web/src/vocab/vocabulary-list.tsx b/web/src/vocab/vocabulary-list.tsx index 41e07d5..ffc582f 100644 --- a/web/src/vocab/vocabulary-list.tsx +++ b/web/src/vocab/vocabulary-list.tsx @@ -2,7 +2,8 @@ import { useState, type FormEvent } from "react"; import { NavLink } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { useVocabularies, useCreateVocabulary } from "../api/queries"; +import { useVocabularies, useCreateVocabulary, useRenameVocabulary, useDeleteVocabulary } from "../api/queries"; +import { DeleteConfirmDialog } from "../components/delete-confirm-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -13,8 +14,12 @@ export function VocabularyList() { const { data, isLoading, isError } = useVocabularies(); const create = useCreateVocabulary(); + const renameVocabulary = useRenameVocabulary(); + const deleteVocabulary = useDeleteVocabulary(); const [key, setKey] = useState(""); + const [editingId, setEditingId] = useState(null); + const [draftKey, setDraftKey] = useState(""); const onCreate = (event: FormEvent) => { event.preventDefault(); @@ -56,15 +61,59 @@ export function VocabularyList() {
  • {t("vocab.empty")}
  • )} {data?.map((v) => ( -
  • - - `block border-b px-3 py-2 text-sm ${isActive ? "bg-indigo-50" : "hover:bg-neutral-50"}` - } - > - {v.key} - +
  • + {editingId === v.id ? ( +
    { + e.preventDefault(); + renameVocabulary.mutate( + { id: v.id, key: draftKey.trim() }, + { onSuccess: () => setEditingId(null) }, + ); + }} + > + setDraftKey(e.target.value)} + /> + + + {renameVocabulary.isError && ( +

    + {t("form.rejected")} +

    + )} +
    + ) : ( + <> + + `block flex-1 px-3 py-2 text-sm ${isActive ? "bg-indigo-50" : "hover:bg-neutral-50"}` + } + > + {v.key} + + + deleteVocabulary.mutateAsync(v.id)} + /> + + )}
  • ))} diff --git a/web/src/vocab/vocabulary-terms.tsx b/web/src/vocab/vocabulary-terms.tsx index c4c75e4..992d9a8 100644 --- a/web/src/vocab/vocabulary-terms.tsx +++ b/web/src/vocab/vocabulary-terms.tsx @@ -5,10 +5,10 @@ import { useTranslation } from "react-i18next"; import type { components } from "../api/schema"; import { useTerms, useAddTerm } from "../api/queries"; import { LabelEditor } from "../components/label-editor"; +import { TermRow } from "./term-row"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { labelText } from "../lib/labels"; type LabelInput = components["schemas"]["LabelInput"]; @@ -63,9 +63,7 @@ export function VocabularyTerms() {
  • {t("vocab.noTerms")}
  • )} {terms?.map((term) => ( -
  • - {labelText(term.labels, lang)} -
  • + ))}