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:
- #122 – “Would be love to be able to delete a task”
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>
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)