From e2eb19add4b07142e5e078ed9f5884ca949de0a2 Mon Sep 17 00:00:00 2001 From: Luiz Carlos Date: Sun, 18 Oct 2026 13:00:21 -0300 Subject: [PATCH] refactor: simplify rename and document the page reset (LUI-180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O renomear devolve a linha na mesma consulta (updateManyAndReturn), e createFolder e renameItem passam a dividir o corpo comum. O LLD registra que o diálogo de renomear descarta as páginas acrescentadas por "Carregar mais". Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01ANcDtWf5vRMzv1r42Ax9ce --- .../api/src/modules/items/items.repository.ts | 9 +-- apps/web/app/(drive)/actions.ts | 61 +++++++++---------- docs/lld.md | 2 +- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/apps/api/src/modules/items/items.repository.ts b/apps/api/src/modules/items/items.repository.ts index 5ba2670..d2b9f50 100644 --- a/apps/api/src/modules/items/items.repository.ts +++ b/apps/api/src/modules/items/items.repository.ts @@ -196,21 +196,18 @@ export class ItemsRepository { id: string, name: string, ): Promise { - const { count } = await this.prisma.item.updateMany({ + const [row] = await this.prisma.item.updateManyAndReturn({ where: { id, ownerId, status: 'active', parentId: { not: null } }, data: { name }, + select: ITEM_FIELDS, }); - if (count === 0) { + if (!row) { const root = await this.prisma.item.findFirst({ where: { id, ownerId, status: 'active', parentId: null }, select: { id: true }, }); return { status: root ? 'root' : 'not_found' }; } - const row = await this.prisma.item.findUniqueOrThrow({ - where: { id }, - select: ITEM_FIELDS, - }); return { status: 'renamed', item: toItem(row) }; } diff --git a/apps/web/app/(drive)/actions.ts b/apps/web/app/(drive)/actions.ts index f05d743..8d45ff2 100644 --- a/apps/web/app/(drive)/actions.ts +++ b/apps/web/app/(drive)/actions.ts @@ -6,6 +6,7 @@ import { toFileRow } from "@/components/files/format"; import type { LoadMoreResult } from "@/components/files/load-more"; import * as authApi from "@/lib/api/auth"; import { errorMessage } from "@/lib/api/error-messages"; +import type { ApiResult } from "@/lib/api/types"; import { completeFailureReason } from "@/lib/uploads/complete-failure"; import type { CancelUploadResult, @@ -94,25 +95,24 @@ const INVALID_NAME = "O nome não pode ter “/” nem caracteres de controle, nem ser “.” ou “..”."; /** - * Cria uma pasta dentro de `parentId`. É uma Server Action, um endpoint público: o nome é - * conferido aqui, e a Sessão, pela própria chamada à API. + * O trecho comum de "Nova pasta" e "Renomear": confere o nome e o id aqui, chama a API e traduz o + * erro dela em português. */ -export async function createFolder( - parentId: string, +async function runNameAction( + id: string, name: string, + emptyMessage: string, + call: (id: string, name: string) => Promise>, ): Promise { - const parsedName = itemNameSchema("Informe um nome para a pasta.").safeParse(name); - const parsedParent = itemIdSchema.safeParse(parentId); + const parsedName = itemNameSchema(emptyMessage).safeParse(name); + const parsedId = itemIdSchema.safeParse(id); if (!parsedName.success) { return { ok: false, message: parsedName.error.issues[0].message }; } - if (!parsedParent.success) { + if (!parsedId.success) { return { ok: false, message: errorMessage("validation_error") }; } - const result = await makeFolder({ - name: parsedName.data, - parentId: parsedParent.data, - }); + const result = await call(parsedId.data, parsedName.data); if (!result.ok) { const message = result.error.code === "validation_error" @@ -123,10 +123,21 @@ export async function createFolder( return { ok: true, name: result.data.name }; } -/** O que o "Renomear" devolve ao diálogo: o nome novo, como a API o gravou, ou o erro, em português. */ -export type RenameItemResult = - | { ok: true; name: string } - | { ok: false; message: string }; +/** + * Cria uma pasta dentro de `parentId`. É uma Server Action, um endpoint público: o nome é + * conferido aqui, e a Sessão, pela própria chamada à API. + */ +export async function createFolder( + parentId: string, + name: string, +): Promise { + return runNameAction(parentId, name, "Informe um nome para a pasta.", (id, n) => + makeFolder({ name: n, parentId: id }), + ); +} + +/** O que o "Renomear" devolve ao diálogo: o mesmo formato do "Nova pasta". */ +export type RenameItemResult = CreateFolderResult; /** * Renomeia um Item (pasta ou arquivo). É uma Server Action, um endpoint público: o nome é @@ -136,23 +147,9 @@ export async function renameItem( id: string, name: string, ): Promise { - const parsedName = itemNameSchema("Informe um nome.").safeParse(name); - const parsedId = itemIdSchema.safeParse(id); - if (!parsedName.success) { - return { ok: false, message: parsedName.error.issues[0].message }; - } - if (!parsedId.success) { - return { ok: false, message: errorMessage("validation_error") }; - } - const result = await changeName(parsedId.data, { name: parsedName.data }); - if (!result.ok) { - const message = - result.error.code === "validation_error" - ? INVALID_NAME - : errorMessage(result.error.code); - return { ok: false, message }; - } - return { ok: true, name: result.data.name }; + return runNameAction(id, name, "Informe um nome.", (itemId, n) => + changeName(itemId, { name: n }), + ); } /** A Server Action é um endpoint público: o que vem do navegador é conferido antes de ir à API. */ diff --git a/docs/lld.md b/docs/lld.md index 531df42..ee46db2 100644 --- a/docs/lld.md +++ b/docs/lld.md @@ -684,7 +684,7 @@ Esta seção descreve o que a web apresenta e protege: as rotas e telas, a Sess - **Linha da lista hoje:** ícone do tipo, nome, tipo, tamanho e data de modificação, sem miniatura nem seleção. Toda linha tem o botão "Mais ações": o menu do arquivo tem "Baixar" e "Renomear", e o da pasta, só "Renomear" ("Mover para…" e "Excluir" entram com o mover e a lixeira). "Renomear" abre um diálogo com o nome atual todo selecionado, a extensão inclusive, e o erro de nome inválido aparece nele, sem fechá-lo. O duplo clique não faz nada até a pré-visualização existir. O tipo é "Pasta" ou um rótulo tirado do `mimeType`, com os rótulos do Figma. O tamanho e a data saem formatados em português, a data no fuso `America/Sao_Paulo`, porque o Server Component roda em UTC. - **Layout do `(drive)`:** mostra a navegação (início, lixeira, busca, planos), o indicador de uso da cota (`usedBytes` e `quotaBytes` de `GET /me`) e o painel de envios (seção 7.4). Uma conta somente leitura (`readOnly`) tem o envio desligado, com o motivo e o caminho para `/planos`. - **Estado na URL:** a pasta atual (`/pasta/[id]`), a busca (`q`) e o arquivo aberto na pré-visualização (`?ver=`) ficam na URL. -- **Paginação das listas:** o Server Component mostra a primeira página, e um botão "Carregar mais", no fim da lista, busca a seguinte por Server Action e a acrescenta no cliente. O cursor não vai para a URL. Depois de um upload, o `router.refresh()` atualiza a primeira página, e as páginas acrescentadas ficam como estavam. O diálogo de compartilhar e o menu de cada item são estado de cliente. +- **Paginação das listas:** o Server Component mostra a primeira página, e um botão "Carregar mais", no fim da lista, busca a seguinte por Server Action e a acrescenta no cliente. O cursor não vai para a URL. Depois de um upload, o `router.refresh()` atualiza a primeira página, e as páginas acrescentadas ficam como estavam. Depois de renomear, o diálogo descarta as páginas acrescentadas e volta à primeira página do servidor, porque o `router.refresh()` não atualiza uma linha já acrescentada. O diálogo de compartilhar e o menu de cada item são estado de cliente. - **Pré-visualização:** abre sobre a lista, a partir de `?ver=`, e usa a rota `/arquivos/[id]/visualizar` como origem da imagem, do vídeo, do áudio, do PDF e do texto. - **Estrutura de pastas:** a árvore da seção 1 mostra a organização do código. As rotas desta tabela entram nos grupos `(auth)`, `(drive)` e `s/[token]` dela.