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
13 changes: 13 additions & 0 deletions src/api/utils/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export namespace UserPreferences {
// instead of lifting them to top-level siblings. Default on.
nestUnderInbox: z.boolean().default(true),
}),
"folder-dnd": z.object({
// Whether folders can be reorganised by drag-and-drop in the sidebar.
// Opt-in, so default off.
enabled: z.boolean().default(false),
}),
} as const;

export type Key = keyof typeof schemas;
Expand Down Expand Up @@ -102,6 +107,14 @@ export class UserPreferencesHandler {
await this.set(userID, "folder-nesting", data);
}

static async getFolderDnd(userID: number) {
return this.get(userID, "folder-dnd");
}

static async setFolderDnd(userID: number, data: z.infer<(typeof UserPreferences.schemas)["folder-dnd"]>) {
await this.set(userID, "folder-dnd", data);
}

static async deleteAllForUser(userID: number): Promise<void> {
await DB.instance().delete(DB.Tables.userPreferences).where(
eq(DB.Tables.userPreferences.user_id, userID)
Expand Down
48 changes: 48 additions & 0 deletions src/api/versions/v1/routes/account/preferences/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,51 @@ router.put('/folder-nesting',
}

);

router.get('/folder-dnd',

APIRouteSpec.authenticated({
summary: "Get folder drag-and-drop preference",
description: "Retrieve whether folders can be reorganised by drag-and-drop in the sidebar for the authenticated user.",
tags: [DOCS_TAGS.ACCOUNT_PREFERENCES],

responses: APIResponseSpec.describeBasic(
APIResponseSpec.success("Folder drag-and-drop preference retrieved successfully", AccountPreferencesModel.FolderDnd.Response),
)
}),

async (c) => {
const authContext = AuthHandler.AuthContext.getAsSession(c);

const preference = await UserPreferencesHandler.getFolderDnd(authContext.user_id);

return APIResponse.success(c, "Folder drag-and-drop preference retrieved successfully", preference);
}

);

router.put('/folder-dnd',

APIRouteSpec.authenticated({
summary: "Update folder drag-and-drop preference",
description: "Set whether folders can be reorganised by drag-and-drop in the sidebar for the authenticated user.",
tags: [DOCS_TAGS.ACCOUNT_PREFERENCES],

responses: APIResponseSpec.describeWithWrongInputs(
APIResponseSpec.successNoData("Folder drag-and-drop preference updated successfully"),
)
}),

validator("json", AccountPreferencesModel.FolderDnd.Body),

async (c) => {
const authContext = AuthHandler.AuthContext.getAsSession(c);

const body = c.req.valid("json");

await UserPreferencesHandler.setFolderDnd(authContext.user_id, body);

return APIResponse.successNoData(c, "Folder drag-and-drop preference updated successfully");
}

);
10 changes: 10 additions & 0 deletions src/api/versions/v1/routes/account/preferences/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ export namespace AccountPreferencesModel.FolderNesting {
export type Body = z.infer<typeof Body>;

}

export namespace AccountPreferencesModel.FolderDnd {

export const Response = UserPreferences.schemas["folder-dnd"];
export type Response = z.infer<typeof Response>;

export const Body = Response;
export type Body = z.infer<typeof Body>;

}
50 changes: 50 additions & 0 deletions tests/api.routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,56 @@ describe("Account Preferences Routes", async () => {
await makeAPIRequest("/v1/account/preferences/folder-nesting", {}, 401);
});

test("GET /v1/account/preferences/folder-dnd defaults to enabled=false with no stored row", async () => {

const dndUser = await seedUser("user", { username: "folderdnduser" }, "DndP@ss1");
const dndSession = await seedSession(dndUser.id).then(s => s.token);

const data = await makeAPIRequest("/v1/account/preferences/folder-dnd", {
authToken: dndSession,
expectedBodySchema: AccountPreferencesModel.FolderDnd.Response
});

expect(data.enabled).toBe(false);

// Default is computed, not persisted.
const dbresult = DB.instance().select().from(DB.Tables.userPreferences).where(
eq(DB.Tables.userPreferences.user_id, dndUser.id)
).all();
expect(dbresult.length).toBe(0);

SessionHandler.inValidateAllSessionsForUser(dndUser.id);
DB.instance().delete(DB.Tables.users).where(eq(DB.Tables.users.id, dndUser.id)).run();
});

test("PUT /v1/account/preferences/folder-dnd persists enabled=true and reads it back", async () => {

await makeAPIRequest("/v1/account/preferences/folder-dnd", {
method: "PUT",
authToken: session_token,
body: { enabled: true }
});

const data = await makeAPIRequest("/v1/account/preferences/folder-dnd", {
authToken: session_token,
expectedBodySchema: AccountPreferencesModel.FolderDnd.Response
});

expect(data.enabled).toBe(true);

const dbresult = DB.instance().select().from(DB.Tables.userPreferences).where(
and(
eq(DB.Tables.userPreferences.user_id, preferencesTestUser.id),
eq(DB.Tables.userPreferences.key, "folder-dnd")
)
).all();
expect(dbresult.length).toBe(1);
});

test("GET /v1/account/preferences/folder-dnd without auth fails", async () => {
await makeAPIRequest("/v1/account/preferences/folder-dnd", {}, 401);
});

});

describe("Mail Account Routes", async () => {
Expand Down
Loading