Translate the older FileNode rights, so Rename and Delete work again
Deleting a file did nothing on the live 0.15.5 server, with no error: the menu items are gated on myRights.mayDelete and myRights.mayRename, and 0.16 was the release that split rights up. Before it a node carried mayRead, mayWrite and mayShare, with the one mayWrite covering everything the newer release names separately — so both items sat permanently disabled. Widen mayWrite into the four rights the newer shape names, alongside the nodeType normalisation, and the UI can keep reading the 0.16 vocabulary.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { client } from "@/jmap/client";
|
||||
import { directoryCreate, fileCreate, fileNodeProps, supportsNodeType, withNodeType } from "../filenode";
|
||||
import { directoryCreate, fileCreate, fileNodeProps, supportsNodeType, normalizeFileNodes } from "../filenode";
|
||||
import type { FileNode, JmapSession } from "@/jmap/types";
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ describe("on Stalwart 0.16 and newer", () => {
|
||||
it("leaves what the server reported alone", () => {
|
||||
client.session = session(NEW_SERVER);
|
||||
const nodes = [{ id: "1", name: "x", nodeType: "directory" }] as Partial<FileNode>[];
|
||||
expect(withNodeType(nodes)).toEqual(nodes);
|
||||
expect(normalizeFileNodes(nodes)).toEqual(nodes);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("on Stalwart before 0.16", () => {
|
||||
|
||||
it("works out nodeType from the file properties, so folders stay folders", () => {
|
||||
client.session = session(OLD_SERVER);
|
||||
const out = withNodeType([
|
||||
const out = normalizeFileNodes([
|
||||
{ id: "1", name: "Documents", blobId: null, size: null, type: null },
|
||||
{ id: "2", name: "notes.txt", blobId: "b1", size: 11, type: "text/plain" },
|
||||
{ id: "3", name: "empty.txt", blobId: "b2", size: 0, type: null },
|
||||
@@ -72,7 +72,7 @@ describe("on Stalwart before 0.16", () => {
|
||||
|
||||
it("does not overwrite a nodeType that did come back", () => {
|
||||
client.session = session(OLD_SERVER);
|
||||
const out = withNodeType([{ id: "1", name: "x", nodeType: "symlink", blobId: "b1" }] as Partial<FileNode>[]);
|
||||
const out = normalizeFileNodes([{ id: "1", name: "x", nodeType: "symlink", blobId: "b1" }] as Partial<FileNode>[]);
|
||||
expect(out[0]!.nodeType).toBe("symlink");
|
||||
});
|
||||
});
|
||||
@@ -81,3 +81,39 @@ it("assumes the older shape when there is no session yet", () => {
|
||||
client.session = null;
|
||||
expect(supportsNodeType()).toBe(false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Rights were split up in 0.16. Before that a node carried mayRead / mayWrite /
|
||||
* mayShare, with mayWrite covering everything the newer release names
|
||||
* separately — so Rename and Delete sat permanently greyed out, doing nothing
|
||||
* and saying nothing.
|
||||
*/
|
||||
describe("rights on a pre-0.16 server", () => {
|
||||
const oldRights = (mayWrite: boolean) => ({ mayRead: true, mayWrite, mayShare: false });
|
||||
|
||||
it("widens mayWrite into the rights the UI gates on", () => {
|
||||
client.session = session(OLD_SERVER);
|
||||
const [node] = normalizeFileNodes([{ id: "1", name: "x", myRights: oldRights(true) }] as unknown as Partial<FileNode>[]);
|
||||
expect(node!.myRights).toMatchObject({ mayRead: true, mayAddChildren: true, mayRename: true, mayDelete: true, mayModifyContent: true, mayShare: false });
|
||||
});
|
||||
|
||||
it("does not hand out rights the server withheld", () => {
|
||||
client.session = session(OLD_SERVER);
|
||||
const [node] = normalizeFileNodes([{ id: "1", name: "x", myRights: oldRights(false) }] as unknown as Partial<FileNode>[]);
|
||||
expect(node!.myRights).toMatchObject({ mayRename: false, mayDelete: false, mayModifyContent: false });
|
||||
});
|
||||
|
||||
it("leaves rights that already use the newer names untouched", () => {
|
||||
client.session = session(OLD_SERVER);
|
||||
const newer = { mayRead: true, mayAddChildren: true, mayRename: true, mayDelete: false, mayModifyContent: true, mayShare: true };
|
||||
const [node] = normalizeFileNodes([{ id: "1", name: "x", myRights: newer }] as unknown as Partial<FileNode>[]);
|
||||
expect(node!.myRights).toEqual(newer);
|
||||
});
|
||||
|
||||
it("copes with a node that reported no rights at all", () => {
|
||||
client.session = session(OLD_SERVER);
|
||||
const [node] = normalizeFileNodes([{ id: "1", name: "x" }] as Partial<FileNode>[]);
|
||||
expect(node!.myRights).toBeUndefined();
|
||||
expect(node!.nodeType).toBe("directory");
|
||||
});
|
||||
});
|
||||
|
||||
+22
-4
@@ -43,12 +43,30 @@ export function fileCreate(parentId: Id | null, name: string, blobId: Id, type:
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill in `nodeType` where the server does not report it, so everything
|
||||
* downstream — icons, sorting, "is this a folder" — can rely on it.
|
||||
* Fill in what an older server does not report, so everything downstream —
|
||||
* icons, sorting, "may I delete this" — can read the 0.16 shape.
|
||||
*
|
||||
* Rights were split up in 0.16. Before that a node carried `mayRead`,
|
||||
* `mayWrite` and `mayShare`, with the one `mayWrite` covering everything the
|
||||
* newer release names separately. Without translating it, the Rename and
|
||||
* Delete menu items sit permanently greyed out: no error, just nothing.
|
||||
*/
|
||||
export function withNodeType<T extends Partial<FileNode>>(nodes: T[]): T[] {
|
||||
export function normalizeFileNodes<T extends Partial<FileNode>>(nodes: T[]): T[] {
|
||||
if (supportsNodeType()) return nodes;
|
||||
return nodes.map((n) => (n.nodeType ? n : { ...n, nodeType: isFile(n) ? "file" : "directory" }));
|
||||
return nodes.map((n) => ({
|
||||
...n,
|
||||
nodeType: n.nodeType ?? (isFile(n) ? "file" : "directory"),
|
||||
myRights: widenRights(n.myRights),
|
||||
}));
|
||||
}
|
||||
|
||||
type Rights = FileNode["myRights"];
|
||||
|
||||
function widenRights(rights: Rights | undefined): Rights | undefined {
|
||||
if (!rights) return rights;
|
||||
const r = rights as Rights & { mayWrite?: boolean };
|
||||
if (r.mayDelete !== undefined || r.mayWrite === undefined) return rights; // already the newer shape
|
||||
return { ...r, mayAddChildren: r.mayWrite, mayRename: r.mayWrite, mayDelete: r.mayWrite, mayModifyContent: r.mayWrite };
|
||||
}
|
||||
|
||||
function isFile(n: Partial<FileNode>): boolean {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import { CAP, client, setErrorMessage } from "@/jmap/client";
|
||||
import type { FileNode, GetResponse, QueryResponse, SetResponse } from "@/jmap/types";
|
||||
import { directoryCreate, fileCreate, supportsNodeType, withNodeType } from "@/lib/filenode";
|
||||
import { directoryCreate, fileCreate, supportsNodeType, normalizeFileNodes } from "@/lib/filenode";
|
||||
import { useSession } from "@/store/session";
|
||||
import { toast } from "@/ui/toast";
|
||||
|
||||
@@ -22,14 +22,14 @@ async function ensureFolder(accountId: string): Promise<string> {
|
||||
["FileNode/query", { accountId, filter: { isTopLevel: true, nodeType: "directory", name: FOLDER }, limit: 5 }, "q"],
|
||||
["FileNode/get", { accountId, "#ids": { resultOf: "q", name: "FileNode/query", path: "/ids" }, properties: folderProps() }, "g"],
|
||||
]);
|
||||
list = withNodeType((res.get("g")?.[0] as unknown as GetResponse<FileNode>).list);
|
||||
list = normalizeFileNodes((res.get("g")?.[0] as unknown as GetResponse<FileNode>).list);
|
||||
} catch {
|
||||
// Older servers: no filter support — scan everything.
|
||||
const res = await client.chain([
|
||||
["FileNode/query", { accountId, limit: 1000 }, "q"],
|
||||
["FileNode/get", { accountId, "#ids": { resultOf: "q", name: "FileNode/query", path: "/ids" }, properties: folderProps() }, "g"],
|
||||
]);
|
||||
list = withNodeType((res.get("g")?.[0] as unknown as GetResponse<FileNode>).list);
|
||||
list = normalizeFileNodes((res.get("g")?.[0] as unknown as GetResponse<FileNode>).list);
|
||||
}
|
||||
const existing = list.find((n) => n.name === FOLDER && n.nodeType === "directory" && !n.parentId);
|
||||
if (existing) return existing.id;
|
||||
|
||||
Reference in New Issue
Block a user