Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions apps/api/src/modules/items/items.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,18 @@ export class ItemsRepository {
id: string,
name: string,
): Promise<RenameResult> {
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) };
}

Expand Down
61 changes: 29 additions & 32 deletions apps/web/app/(drive)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<ApiResult<{ name: string }>>,
): Promise<CreateFolderResult> {
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"
Expand All @@ -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<CreateFolderResult> {
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 é
Expand All @@ -136,23 +147,9 @@ export async function renameItem(
id: string,
name: string,
): Promise<RenameItemResult> {
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. */
Expand Down
2 changes: 1 addition & 1 deletion docs/lld.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<id>`) 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=<id>`, 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.

Expand Down
Loading