API z bazą danych w kilka minut? – poznaj Supabase (2/2)

Pierwszą część naszego poradnika znajdziecie w linku
API z bazą danych w kilka minut? – poznaj Supabase (1/2)

Ostatnim elementem wymagającym zmiany w komponencie Todo jest poprawienie sposobu wyświetlania kategorii. Aktualnie pokazujemy jedynie jej id, a chcielibyśmy, żeby użytkownik miał możliwość zobaczenia jej nazwy.

Krok 6

Wyświetlanie kategorii

Aby to zrobić musimy zmodyfikować naszą funkcję pobierającą zadania, a dokładniej jej metodę select(). Aby odwołać się w niej do danych z innej, powiązanej relacją tabeli oraz pobrać je, należy podać jej nazwę oraz wskazać w nawiasach, jaka dokładnie kolumna nas interesuje. Składnia tego polecenia po zmianach powinna zatem wyglądać następująco:

.select(
    `*,
    Category (
        name
    )
 `)

Jeśli wyświetlisz w konsoli pobraną listę zadań zobaczysz, że teraz nasz obiekt posiada pole Category zawierające obiekt z polem name.

Widok pobranej listy w narzędziach deweloperskich

Poprawmy zatem nasz komponent Todo oraz, jeżeli korzystasz z mojego szablonu lub piszesz w TypeScript’cie, dodajmy pole Category do interfejsu przygotowanego dla naszych zadań (w szablonie wszystkie interfejsy dla obiektów przygotowane są w pliku models.ts)

<p className="todo-date">Category: <span>{todo?.Category.name}</span></p>
export interface ITodo {
    id: number,
    name: string,
    created_at?: string,
    description?: string,
    category_id: number,
    user_id: number,
    Category: {
        name:string
    }
}

Teraz nasze zadanie wygląda tak jak powinno.

Widok zadania w aplikacji
Widok zadania w aplikacji
Krok 7

Dodawanie zadań

Kolejną funkcjonalnością naszej aplikacji jest dodawanie zadań. W komponencie TodoList mamy już przygotowany do tego formularz, ale musimy jeszcze zapewnić jego implementację i powiązanie z naszą bazą.

Aby dodawać rekordy do bazy Supabase zapewnia metodę insert. Możesz zobaczyć sposób jej użycia wchodząc w zakładkę API i wybierając tabelę Todo. Korzystając z tej wiedzy, a także poprzedniej funkcji pobierającej zadania napiszmy funkcję dodająca zadanie do bazy:

const saveTask = async () => {
    try {
        const userId = supabase.auth.user()!.id;
        const { error } = await supabase
            .from('Todo')
            .insert([{
                name: taskName,
                description: taskDescription,
                category_id: taskCategoryId,
                user_id: userId
            }]);

        if (error) {
            throw error;
        }
        alert('Task successfully added')
    }
    catch (error:any) {
        alert(error.message)
    }
 }

Teraz musimy przygotować stany dla poszczególnych pól formularza oraz połączyć je z nimi. Mamy 3 pola, zatem potrzebne są nam 3 stany:

const [taskName,setTaskName] = useState('');

 const [taskCategoryId,setTaskCategoryId] = useState('');

 const [taskDescription,setTaskDescription] = useState('');

Po podłączeniu pól dodajmy jeszcze naszą metodę saveTask, do funkcji obsługującej wysłanie formularza handleSubmit.

const handleSubmit = (e:FormEvent) => {
    e.preventDefault()
    if(taskCategoryId === ''){
        alert('You have to choose some category')
        return;
    }
    saveTask()
 }

Wszystko byłoby idealnie, gdyby nie fakt, że dalej nie mamy możliwości wybrania kategorii dla naszego zadania, gdyż nie pobieramy jeszcze z bazy żadnej z nich. Aby się tym zająć, analogicznie do metody pobierającej zadania napiszmy metodę do pobierania kategorii i dodajmy jej wywołanie w hook’u useEffect.

const getCategories =async () => {
    try {
        const userId = supabase.auth.user()!.id;
        const { data: Categories, error, status } = await supabase
            .from('Category')
            .select(`*`)
            .eq('user_id', userId);

        if (error && status !== 406) {
            throw error;
        }

        if (Categories) {
            setCategories(Categories);
        }
    }
    catch (error:any) {
        alert(error.message)
    }      
 }

Mając już nasze dane, możemy zaktualizować pole select formularza tak, aby wyświetlało ono wszystkie pobrane kategorie.

<select
    name="task-category"
    id="task-category"
    value={taskCategoryId}
    onChange={(e) => setTaskCategoryId(e.target.value)}
 >
    <option value="">--Please choose an option--</option>
    {categories.map((category) =>{
        return (
            <option 
                key={category.id} 
                value={category.id}>
                    {category.name}
            </option>
    )})}
 </select>

Przetestujmy zatem dodanie nowego zadania. Po wysłaniu formularza, w panelu Supabase możemy sprawdzić, że pojawił się nowy rekord w tabeli Todo:

Widok nowego zadania w panelu Supabase

Na koniec, dla lepszego efektu, dodajmy jeszcze czyszczenie pól formularza po jego wysłaniu poprzez ustawienie ich wartości na puste stringi w metodzie saveTask.

Krok 8

Edycja i usuwanie zadań

/*...*/
 alert('Task successfully added');

 setTaskCategoryId('');
 setTaskDescription('');
 setTaskName('');

Sporo pracy za nami, ale została nam jeszcze jedna funkcjonalność. Musimy umożliwić użytkownikowi edycję oraz usuwanie zadań. Zajmijmy się najpierw tym drugim. Dodajmy następującą metodę do naszego komponentu TodoList:

const deleteTask = async (taskId:number) => {
    try {
        const userId = supabase.auth.user()!.id;
        const { error } = await supabase
            .from('Todo')
            .delete()
            .eq('id', taskId)
            .eq('user_id',userId);

        if (error) {
            throw error;
        }

        alert('Task successfully deleted');
    }
    catch (error:any) {
        alert(error.message);
    }
 }

Metoda ta korzysta z udostępnionej przez Supabase metody delete, która pozwala na usuwanie rekordów z bazy danych. Zauważ, że filtr eq został wykorzystany dwa razy. Łączenie filtrów jest możliwe i pozwala na tworzenie znacznie bardziej skomplikowanej logiki podczas wykonywania operacji na bazie danych.

Musimy jeszcze przekazać tę metodę do naszego komponentu Todo, a także dodać obsługę kliknięcia przycisku „Delete”. Tym sposobem nasz komponent Todo wygląda następująco:

interface TodoProps {
    todo: ITodo,
    deleteTask: (taskId: number) => void
}

 const Todo: FunctionComponent<TodoProps> = ({todo,deleteTask}) => {

    return (
        <li className="todo">
            <p className="todo-date">{todo?.created_at}</p>
            <p className="todo-name">{todo?.name}</p>
            <p className="todo-date">Category: <span>{todo?.Category.name}</span></p>
            <p className="todo-desc">{todo?.description}</p>
            <div className="btn-container">
                <button className="btn" 
                    onClick={() => deleteTask(todo.id)}
                >
                    Delete
                </button>
                <button className="btn">Edit</button>
            </div>
        </li>
    );
 }

 export default Todo;

Ostatnią rzeczą jest obsługa edycji naszych zadań. Wykorzystamy do tego zadania posiadany formularz dodawania zadań, jednak aby to zrobić potrzebujemy stanu, który pozwoli określić czy dane przechowywane w jego polach dotyczą zadania już istniejącego, czy może raczej nowego zadania. Stan ten będzie jednocześnie przechowywał informację o id zadania, które edytujemy:

const [editedTaskId,setEditedTaskId] = useState<number | null>(null);

W metodzie handleSubmit wykorzystajmy go do warunkowego wybierania odpowiednio funkcji edycji lub funkcji dodania zadania:

const handleSubmit = (e:FormEvent) => {
    e.preventDefault();

    if(taskCategoryId === ''){
        alert('You have to choose some category');
        return;
    }

    if(editedTaskId)
        editTask(editedTaskId);
    else
        saveTask();
 }

Korzystając dalej z dokumentacji API na Supabase napiszmy odpowiednią metodę, która będzie analogiczna do metody saveTask, ale wykorzystywać będzie metodę update zapewnioną przez Supabase. Metoda ta pozwoli na zapisanie zmian w naszym zadaniu.

const editTask = async (taskId:number) => {
    try {
        const userId = supabase.auth.user()!.id;
        const { error } = await supabase
            .from('Todo')
            .update({
                name: taskName,
                description: taskDescription,
                category_id: taskCategoryId,
            })
            .eq('id', taskId)
            .eq('user_id',userId);

        if (error) {
            throw error;
        }

        alert('Task successfully edited');

        setTaskCategoryId('');
        setTaskDescription('');
        setTaskName('');

        setEditedTaskId(null);
        }
    catch (error:any) {
        alert(error.message);
    }
 }
Więcej o metodzie delete możesz przeczytać tutaj.
Teraz potrzebujemy jeszcze możliwości wskazania edytowanego zadania poprzez kliknięcie przycisku Edit danego zadania, a także możliwości umieszczenia danych w polach formularza. Napiszmy metodę handleEdit, którą umieścimy w komponencie TodoList.
const handleEdit = (taskId:number) => {
    setEditedTaskId(taskId);
    const editedTask = todos.find((todo) => todo.id == taskId)!
    setTaskCategoryId(editedTask.category_id.toString())
    setTaskDescription(editedTask.description ? editedTask.description : '')
    setTaskName(editedTask.name)
 }

Przekażmy tę metodę do komponentu Todo i wywołajmy ją w przypadku kliknięcia w przycisk Edit:

<button className="btn" onClick={() => handleEdit(todo.id)}>Edit</button>

Możesz teraz już przetestować edytowanie zadań.

Krok 9

Ostatnie szlify

Wszystko działa już jak należy, ale możliwe, że zwróciłeś uwagę na fakt, że po edycji/usunięciu zadania, lista zadań nie aktualizuje się. Musimy poprawić ten fakt tak, aby wszystko działo się dynamicznie. Możesz pomyśleć, że można po prostu wywołać zapytanie pobierające listę zadań za każdym razem, gdy następuje jakaś zmiana.

To rozwiązanie dałoby poprawny rezultat, jednak w przypadku, gdybyśmy mieli miliony rekordów, byłoby to bardzo nieoptymalne. Dlatego też lepiej jest pracować bezpośrednio na liście, którą już pobraliśmy przy pierwszym renderowaniu komponentu. Poprawmy zatem nasze metody editTask, deleteTask oraz saveTask tak, aby aktualizowały one zmienną stanu todos w przypadku poprawnego wykonania.

  • editTask
setTodos(todos.map(todo =>
    todo.id === taskId ?
    {...todo,
        name: taskName,
        category_id: parseInt(taskCategoryId),
        description: taskDescription,
        Category: {
            name: categories.find(category => 
                category.id === parseInt(taskCategoryId))?.name!
    }} :
    todo))
  • deleteTask
setTodos(todos.filter((todo) => todo.id !== taskId))
  • saveTask
const newTodo:ITodo = {...Todo[0], 
    Category: {
        name: categories.find(category => 
            category.id === parseInt(taskCategoryId))?.name!
    }
 }

 setTodos([...todos,newTodo])

Mamy już w pełni funkcjonalną, dynamiczną aplikację. Skoro zbliżamy się do końca tego artykułu, dobrze byłoby móc wylogować się z aplikacji po zakończonej pracy. Dodajmy zatem metodę, która to umożliwi. W tym przypadku również mamy praktycznie gotowe rozwiązanie dla tego problemu, dzięki metodzie supabase.auth.signOut() zapewnianej przez Supabase.

const logOut = async () => {
    try{
      const { error } = await supabase.auth.signOut();

      if(error)
        throw error;
    }
    catch(error){
      alert('This action is unavailable right now');
    }
 }

Dodajmy tę metodę do obsługi kliknięcia przycisku z nagłówka naszej aplikacji oraz zapewnijmy, że przycisk będzie widoczny tylko dla zalogowanych użytkowników:

{session && <button className='btn' onClick={logOut}>Log out</button>}
Krok 10

Trochę samodzielnej pracy

Możliwe, że zauważyłeś, że w projekcie nie ma komponentu umożliwiającego dodawanie nowych typów zadań z poziomu aplikacji. Jest to zabieg celowy (wcale nie chodzi o moje lenistwo, przysięgam), żeby dać Ci możliwość sprawdzenia swoich sił w samodzielnej pracy z Supabase.

Skorzystaj z wiedzy zawartej w poprzednich częściach tego poradnika, a także z automatycznej dokumentacji z panelu Supabase i spróbuj przygotować tę funkcjonalność samodzielnie :). Jeżeli chcesz, możesz dodać nowy formularz w komponencie z listą zadań, albo stworzyć zupełnie nowy komponent, specjalnie dla tej funkcjonalności – możliwości jest mnóstwo!

Podsumowanie

Mam nadzieję, że udało mi się przybliżyć Ci podstawy pracy z Supabase. Oczywiście wszystko, co udało mi się zawrzeć w tym artykule, to dopiero wierzchołek góry lodowej, a możliwości tego serwisu są przeogromne. Jeszcze raz zachęcam Cię do zgłębienia dokumentacji Supabase i dalszej pracy nad poznawaniem coraz bardziej zaawansowanych sposobów użycia tego narzędzia.
Adam Lipiński

Za dnia początkujący front-end developer, w nocy zaś student (czasami na odwrót). Fan testowania nowych technologii oraz CSSowych sztuczek.

Zostaw komentarz

jedenaście + szesnaście =

Top