Zuka desenhada

O diario de um Munito

"Se o tempo é curto e a atenção mais ainda, peço licença para desperdiçá-los com palavras."

UseKaneo - Minha primeira PR em um projeto Open Source

Desde que conheci o mundo open source, sempre tive vontade de contribuir com algum projeto. Mas sempre esbarrei na seguinte falácia: eu não tenho conhecimento suficiente para isso.

Nesta última semana, por meio de um amigo e ex-companheiro de equipe ~o grande Felipin, o Mordekaiser de Redemption~ conheci o projeto UseKaneo.

O Kaneo é um projeto que visa ajudar na gestão de projetos, oferecendo uma visão simples sobre as tarefas e seus status.


Ok, mas e agora? Como posso ajudar?

Comecei a explorar o repositório e percebi que ele era estruturado como um monorepo, muito parecido com o que eu havia criado recentemente em uma experiência profissional. Por estar em um ambiente mais familiar, me senti um pouco mais confiante, o que reduziu a barreira de entrada.

Foi então que me deparei com a seguinte issue aberta:

Percebi que com poucas mudanças seria possível resolver.


Entendendo o backend

Sempre que vou fazer alguma alteração em um projeto, gosto de entender primeiro como o backend funciona. Isso me ajuda a compreender o sistema como um todo e saber onde e como posso implementar as mudanças necessárias.

No caso do Kaneo, o backend não é muito complexo. Ele utiliza uma ORM diretamente nos services, que são acessados pelos controllers. Não acredito que essa seja a melhor abordagem, mas, para este projeto, ela funciona muito bem.
~Então é a melhor abordagem, senhor!~ ._.

Como a funcionalidade desejada era a de deletar uma task, comecei verificando as operações já existentes para o modelo. Para deletar, basicamente só é necessário o ID da task. Me baseei no GET por ID e voilà, a parte de deleção estava pronta:

import { eq } from "drizzle-orm";
import { HTTPException } from "hono/http-exception";
import db from "../../database";
import { taskTable } from "../../database/schema";

async function deleteTask(taskId: string) {
  const task = await db
    .delete(taskTable)
    .where(eq(taskTable.id, taskId))
    .returning()
    .execute();

  if (!task) {
    throw new HTTPException(404, {
      message: "Task not found",
    });
  }

  return task;
}

export default deleteTask;

Depois disso, foi só incluir a função no controller e puf, já era possível testar:

.delete(
  "/:id",
  zValidator("param", z.object({ id: z.string() })),
  async (c) => {
    const { id } = c.req.valid("param");
    const task = await deleteTask(id);
    return c.json(task);
  },
);

Percebi que não havia uma collection do Postman (ou similar) para testar as rotas. Então, tive a ideia de abrir o frontend do próprio projeto, forçar uma requisição para buscar uma task, e copiar o cURL pelo inspecionador de rede do navegador. Depois, bastou alterar o método HTTP de GET para DELETE.

curl 'http://localhost:1337/task/lijvh18aqxur4m7w8h4dz236' -H 'Accept: */*' -H 'Accept-Encoding: gzip, deflate, br, zstd' -H 'Content-Type: application/json' -H 'Cookie: session=c7pgrjwxabzpu57ely4b5n2mi3rukv4k'

para

curl -X DELETE 'http://localhost:1337/task/lijvh18aqxur4m7w8h4dz236' -H 'Accept: */*' -H 'Accept-Encoding: gzip, deflate, br, zstd' -H 'Content-Type: application/json' -H 'Cookie: session=c7pgrjwxabzpu57ely4b5n2mi3rukv4k'

Também conectei o DBeaver ao SQLite do projeto para observar a deleção diretamente na tabela, além do feedback visual no próprio app.

A parte que menos gosto, frontend

Partindo dessa abordagem quase bottom-up, para o frontend comecei criando a forma de comunicação com o backend. Criei um hook que, quando acionado, enviaria a requisição de deleção. Ademais, adicionei o botão na página, seguindo o padrão de estilização do projeto:

<Button
  className="bg-red-600 text-white hover:bg-red-500 dark:bg-red-500 dark:hover:bg-red-400"
>
  {"Delete Task"}
</Button>

Imagem do Kaneo com um botao em vermelho brilhante

Agora faltava adicionar a função de deleção ao botão:

  const { mutateAsync: deleteTask, isPending: isDeleting } = useDeleteTask();
      <Button
        className="bg-red-600 text-white hover:bg-red-500 dark:bg-red-500 dark:hover:bg-red-400"
      >
        {isDeleting ? "Deleting..." : "Delete Task"}
      </Button>

Pronto, ufa! Acabei a issue, certo? Errado!

Havia esquecido de um detalhe: Depois que apagava, a mesma task continuava sendo exibida, pois a página não era redirecionada.

Foi então que usei o react-router para voltar à página anterior assim que a task fosse deletada com sucesso:

import { useNavigate } from "@tanstack/react-router";

navigate({
  to: "/dashboard/workspace/$workspaceId/project/$projectId/board",
  params: {
    workspaceId: project?.workspaceId ?? "",
    projectId: project?.id ?? "",
  },
});

Agora sim estava pronto. Fui testar e… percebo mais uma coisa. ~AAAAAAAAAAAAAA~ Quando voltava para a página do board, a tarefa ainda aparecia mesmo deletada. Era necessário limpar o estado.

Usei então o react-query, retornando à página anterior, e o handler de deleção ficou assim:

  const handleDeleteTask = async () => {
    if (!task) return;

    try {
      await deleteTask(task.id);
      queryClient.invalidateQueries({
        queryKey: ["tasks", project?.id ?? ""],
      });
      toast.success("Task deleted successfully");
      navigate({
        to: "/dashboard/workspace/$workspaceId/project/$projectId/board",
        params: {
          workspaceId: project?.workspaceId ?? "",
          projectId: project?.id ?? "",
        },
      });
    } catch (error) {
      toast.error(
        error instanceof Error ? error.message : "Failed to delete task",
      );
    }
  };

Enfim…

Ufa! Depois dessa trabalheira, enfim pude abrir o PR. Como segui todas as guidelines da comunidade, não tive problemas e rapidamente a nova feature foi aceita! Me sinto satisfeito em finalmente conseguir fazer esses commits.

É de fato uma realização imensa poder ajudar uma comunidade. Saí da minha zona de conforto mexendo no frontend, aprendi um novo framework, e o fruto desse aprendizado certamente será útil.

Além da parte técnica, outro fator importante foi perder o medo de tentar.

Eu tentei! (e consegui)

#typescript #javascript #open source #kaneo