style: satisfy housekeeping biome gate
This commit is contained in:
1 parent
d1160eb65a
commit
08358b8aa4
62 files changed
+2024
-773
No files matched your search
@@ -7,17 +7,30 @@ import { ActionError } from "@/lib/safe-action-shared";
|
||||
import { createAd, deleteAd } from "./admin-ads";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({
|
||||
execute: vi.fn(async () => ({ ok: true, data: { before: null, after: { id: "1" }, output: { id: "1" } }, correlationId: "legacy" })),
|
||||
execute: vi.fn(async () => ({
|
||||
ok: true,
|
||||
data: { before: null, after: { id: "1" }, output: { id: "1" } },
|
||||
correlationId: "legacy",
|
||||
})),
|
||||
}));
|
||||
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
|
||||
contentMutationService: { execute },
|
||||
createContentMutationInvocation: (actor, correlationId) => ({ expectedActorId: actor.id, correlationId, legacy: true }),
|
||||
createContentMutationInvocation: (actor, correlationId) => ({
|
||||
expectedActorId: actor.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||
vi.mock("@/lib/logger", () => ({ logger: { error: vi.fn() } }));
|
||||
vi.mock("@/lib/safe-action", () => ({ adminAction: (_options, handler) => handler }));
|
||||
vi.mock("@/lib/safe-action-shared", () => ({ ActionError: class ActionError extends Error {}, actionOk: () => "ok" }));
|
||||
vi.mock("@/lib/safe-action", () => ({
|
||||
adminAction: (_options, handler) => handler,
|
||||
}));
|
||||
vi.mock("@/lib/safe-action-shared", () => ({
|
||||
ActionError: class ActionError extends Error {},
|
||||
actionOk: () => "ok",
|
||||
}));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||
|
||||
@@ -27,13 +40,21 @@ const fakeForm = (data) => ({ get: (key) => data[key] ?? null });
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue(staff);
|
||||
execute.mockResolvedValue({ ok: true, data: { before: null, after: { id: "1" }, output: { id: "1" } }, correlationId: "legacy" });
|
||||
execute.mockResolvedValue({
|
||||
ok: true,
|
||||
data: { before: null, after: { id: "1" }, output: { id: "1" } },
|
||||
correlationId: "legacy",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Content advertisement legacy wrappers", () => {
|
||||
it("delegates creation and preserves redirect", async () => {
|
||||
await createAd(fakeForm({ image: "https://example.com/ad.png" }));
|
||||
expect(execute).toHaveBeenCalledWith(expect.objectContaining({ expectedActorId: 1, legacy: true }), "ad.change", { action: "create", image: "https://example.com/ad.png" });
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ expectedActorId: 1, legacy: true }),
|
||||
"ad.change",
|
||||
{ action: "create", image: "https://example.com/ad.png" },
|
||||
);
|
||||
expect(redirect).toHaveBeenCalledWith("/admin/ads");
|
||||
});
|
||||
|
||||
@@ -43,20 +64,52 @@ describe("Content advertisement legacy wrappers", () => {
|
||||
});
|
||||
|
||||
it("logs a redacted service failure", async () => {
|
||||
execute.mockResolvedValue({ ok: false, error: { code: "DEPENDENCY_UNAVAILABLE", messageKey: "errors.housekeeping.dependencyUnavailable" }, correlationId: "legacy" });
|
||||
execute.mockResolvedValue({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "DEPENDENCY_UNAVAILABLE",
|
||||
messageKey: "errors.housekeeping.dependencyUnavailable",
|
||||
},
|
||||
correlationId: "legacy",
|
||||
});
|
||||
await createAd(fakeForm({ image: "x" }));
|
||||
expect(logger.error).toHaveBeenCalledWith("Action failed: createAd", expect.objectContaining({ error: "errors.housekeeping.dependencyUnavailable" }));
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Action failed: createAd",
|
||||
expect.objectContaining({
|
||||
error: "errors.housekeeping.dependencyUnavailable",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("delegates deletion and preserves action result", async () => {
|
||||
const handler = deleteAd as unknown as (ctx: unknown) => Promise<string>;
|
||||
await expect(handler({ data: { id: 99n }, session: { user: { id: "1" } }, requestId: "delete" })).resolves.toBe("ok");
|
||||
expect(execute).toHaveBeenCalledWith(expect.objectContaining({ correlationId: "delete" }), "ad.change", { action: "delete", id: "99" });
|
||||
await expect(
|
||||
handler({
|
||||
data: { id: 99n },
|
||||
session: { user: { id: "1" } },
|
||||
requestId: "delete",
|
||||
}),
|
||||
).resolves.toBe("ok");
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ correlationId: "delete" }),
|
||||
"ad.change",
|
||||
{ action: "delete", id: "99" },
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves not-found ActionError", async () => {
|
||||
execute.mockResolvedValue({ ok: false, error: { code: "NOT_FOUND", messageKey: "errors.housekeeping.notFound" }, correlationId: "legacy" });
|
||||
execute.mockResolvedValue({
|
||||
ok: false,
|
||||
error: { code: "NOT_FOUND", messageKey: "errors.housekeeping.notFound" },
|
||||
correlationId: "legacy",
|
||||
});
|
||||
const handler = deleteAd as unknown as (ctx: unknown) => Promise<string>;
|
||||
await expect(handler({ data: { id: 999n }, session: { user: { id: "1" } }, requestId: "missing" })).rejects.toThrow(ActionError);
|
||||
await expect(
|
||||
handler({
|
||||
data: { id: 999n },
|
||||
session: { user: { id: "1" } },
|
||||
requestId: "missing",
|
||||
}),
|
||||
).rejects.toThrow(ActionError);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,10 @@ import { ActionError, actionOk } from "@/lib/safe-action-shared";
|
||||
|
||||
export async function createAd(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const image = String(formData.get("image") ?? "").normalize("NFC").trim().slice(0, 255);
|
||||
const image = String(formData.get("image") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim()
|
||||
.slice(0, 255);
|
||||
if (!image) return;
|
||||
const result = await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
@@ -24,7 +27,10 @@ export async function createAd(formData: FormData): Promise<void> {
|
||||
{ action: "create", image },
|
||||
);
|
||||
if (!result.ok) {
|
||||
logger.error("Action failed: createAd", { action: "createAd", error: result.error.messageKey });
|
||||
logger.error("Action failed: createAd", {
|
||||
action: "createAd",
|
||||
error: result.error.messageKey,
|
||||
});
|
||||
revalidatePath("/admin/ads");
|
||||
return;
|
||||
}
|
||||
@@ -35,7 +41,10 @@ export async function updateAd(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const raw = String(formData.get("id") ?? "").normalize("NFC");
|
||||
if (!/^\d+$/u.test(raw)) return;
|
||||
const image = String(formData.get("image") ?? "").normalize("NFC").trim().slice(0, 255);
|
||||
const image = String(formData.get("image") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim()
|
||||
.slice(0, 255);
|
||||
if (!image) return;
|
||||
const result = await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
@@ -43,7 +52,11 @@ export async function updateAd(formData: FormData): Promise<void> {
|
||||
{ action: "update", id: raw, image },
|
||||
);
|
||||
if (!result.ok) {
|
||||
logger.error("Action failed: updateAd", { action: "updateAd", id: Number(raw), error: result.error.messageKey });
|
||||
logger.error("Action failed: updateAd", {
|
||||
action: "updateAd",
|
||||
id: Number(raw),
|
||||
error: result.error.messageKey,
|
||||
});
|
||||
revalidatePath("/admin/ads/" + raw);
|
||||
return;
|
||||
}
|
||||
@@ -51,14 +64,20 @@ export async function updateAd(formData: FormData): Promise<void> {
|
||||
}
|
||||
|
||||
const deleteAdInput = z.object({
|
||||
id: z.union([z.string(), z.number(), z.bigint()]).transform((value) => BigInt(String(value))),
|
||||
id: z
|
||||
.union([z.string(), z.number(), z.bigint()])
|
||||
.transform((value) => BigInt(String(value))),
|
||||
});
|
||||
|
||||
export const deleteAd = adminAction(
|
||||
{ permission: PERMS.PAGES_EDIT, schema: deleteAdInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"ad.change",
|
||||
{ action: "delete", id: ctx.data.id.toString() },
|
||||
);
|
||||
|
||||
@@ -12,10 +12,18 @@ import { PERMS } from "@/lib/permissions";
|
||||
|
||||
function articleInput(formData: FormData) {
|
||||
return {
|
||||
title: String(formData.get("title") ?? "").normalize("NFC").trim(),
|
||||
shortStory: String(formData.get("shortStory") ?? "").normalize("NFC").trim(),
|
||||
fullStory: String(formData.get("fullStory") ?? "").normalize("NFC").trim(),
|
||||
image: String(formData.get("image") ?? "").normalize("NFC").trim(),
|
||||
title: String(formData.get("title") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim(),
|
||||
shortStory: String(formData.get("shortStory") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim(),
|
||||
fullStory: String(formData.get("fullStory") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim(),
|
||||
image: String(formData.get("image") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim(),
|
||||
slug: String(formData.get("slug") ?? "").trim(),
|
||||
};
|
||||
}
|
||||
@@ -30,7 +38,9 @@ export async function createArticle(formData: FormData): Promise<void> {
|
||||
{ action: "create", ...input },
|
||||
);
|
||||
if (!result.ok) {
|
||||
redirect("/admin/articles/new?error=Database error while creating article. Please try again.");
|
||||
redirect(
|
||||
"/admin/articles/new?error=Database error while creating article. Please try again.",
|
||||
);
|
||||
}
|
||||
redirect("/admin/articles");
|
||||
}
|
||||
|
||||
@@ -51,9 +51,7 @@ describe("legacy admin ban wrappers", () => {
|
||||
});
|
||||
|
||||
it("returns early when userId is invalid", async () => {
|
||||
await createBan(
|
||||
fakeForm({ userId: "0", hours: "1", type: "account" }),
|
||||
);
|
||||
await createBan(fakeForm({ userId: "0", hours: "1", type: "account" }));
|
||||
expect(execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -11,10 +11,18 @@ import { PERMS } from "@/lib/permissions";
|
||||
|
||||
function templateInput(formData: FormData) {
|
||||
return {
|
||||
name: String(formData.get("name") ?? "").normalize("NFC").trim().slice(0, 255),
|
||||
subject: String(formData.get("subject") ?? "").normalize("NFC").trim().slice(0, 255),
|
||||
name: String(formData.get("name") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim()
|
||||
.slice(0, 255),
|
||||
subject: String(formData.get("subject") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim()
|
||||
.slice(0, 255),
|
||||
body: String(formData.get("body") ?? "").normalize("NFC"),
|
||||
variables: String(formData.get("variables") ?? "").normalize("NFC").trim(),
|
||||
variables: String(formData.get("variables") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim(),
|
||||
isActive: formData.get("isActive") != null,
|
||||
};
|
||||
}
|
||||
@@ -23,7 +31,11 @@ export async function createEmailTemplate(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const input = templateInput(formData);
|
||||
if (!input.name || !input.subject || !input.body) return;
|
||||
const result = await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "email-template.change", { action: "create", ...input });
|
||||
const result = await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"email-template.change",
|
||||
{ action: "create", ...input },
|
||||
);
|
||||
if (!result.ok) throw new Error("Email template creation failed");
|
||||
revalidatePath("/admin/email-templates");
|
||||
}
|
||||
@@ -34,16 +46,26 @@ export async function updateEmailTemplate(formData: FormData): Promise<void> {
|
||||
if (!/^\d+$/u.test(id)) return;
|
||||
const input = templateInput(formData);
|
||||
if (!input.subject || !input.body) return;
|
||||
const result = await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "email-template.change", { action: "update", id, ...input });
|
||||
const result = await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"email-template.change",
|
||||
{ action: "update", id, ...input },
|
||||
);
|
||||
if (!result.ok) throw new Error("Email template update failed");
|
||||
revalidatePath("/admin/email-templates");
|
||||
}
|
||||
|
||||
export async function deleteEmailTemplate(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const id = String(formData.get("id") ?? "").normalize("NFC").trim();
|
||||
const id = String(formData.get("id") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!/^[1-9]\d*$/u.test(id)) return;
|
||||
const result = await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "email-template.change", { action: "delete", id });
|
||||
const result = await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"email-template.change",
|
||||
{ action: "delete", id },
|
||||
);
|
||||
if (!result.ok) throw new Error("Email template deletion failed");
|
||||
revalidatePath("/admin/email-templates");
|
||||
}
|
||||
@@ -47,10 +47,7 @@ function revalidateHelpCenterTicketPaths(ticketId: bigint) {
|
||||
|
||||
async function execute(
|
||||
staff: { readonly id: number },
|
||||
operation:
|
||||
| "help-ticket.reply"
|
||||
| "help-ticket.status"
|
||||
| "help-ticket.unban",
|
||||
operation: "help-ticket.reply" | "help-ticket.status" | "help-ticket.unban",
|
||||
input: unknown,
|
||||
) {
|
||||
return peopleMutationService.execute(
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { createHelpQuestion, deleteHelpQuestion, updateHelpQuestion } from "./admin-help";
|
||||
import {
|
||||
createHelpQuestion,
|
||||
deleteHelpQuestion,
|
||||
updateHelpQuestion,
|
||||
} from "./admin-help";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
|
||||
contentMutationService: { execute },
|
||||
createContentMutationInvocation: (actor: { id: number }, correlationId: string) => ({ expectedActorId: actor.id, correlationId, legacy: true }),
|
||||
createContentMutationInvocation: (
|
||||
actor: { id: number },
|
||||
correlationId: string,
|
||||
) => ({ expectedActorId: actor.id, correlationId, legacy: true }),
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||
@@ -14,28 +21,50 @@ vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||
|
||||
const staff = { id: 1, rank: 7, username: "admin" };
|
||||
const fakeForm = (data: Record<string, string | null>) => ({ get: (key: string) => key in data ? data[key] : null });
|
||||
const fakeForm = (data: Record<string, string | null>) => ({
|
||||
get: (key: string) => (key in data ? data[key] : null),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||
execute.mockResolvedValue({ ok: true, data: { before: null, after: { id: "5" } }, correlationId: "legacy" });
|
||||
execute.mockResolvedValue({
|
||||
ok: true,
|
||||
data: { before: null, after: { id: "5" } },
|
||||
correlationId: "legacy",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Content help legacy wrappers", () => {
|
||||
it("delegates create and redirects", async () => {
|
||||
await createHelpQuestion(fakeForm({ name: "FAQ", content: "<p>Answer</p>" }) as FormData);
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "help-question.change", expect.objectContaining({ action: "create", name: "FAQ" }));
|
||||
await createHelpQuestion(
|
||||
fakeForm({ name: "FAQ", content: "<p>Answer</p>" }) as FormData,
|
||||
);
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
"help-question.change",
|
||||
expect.objectContaining({ action: "create", name: "FAQ" }),
|
||||
);
|
||||
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||
});
|
||||
it("delegates update and redirects", async () => {
|
||||
await updateHelpQuestion(fakeForm({ id: "42", name: "Updated", content: "New" }) as FormData);
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "help-question.change", expect.objectContaining({ action: "update", id: "42" }));
|
||||
await updateHelpQuestion(
|
||||
fakeForm({ id: "42", name: "Updated", content: "New" }) as FormData,
|
||||
);
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
"help-question.change",
|
||||
expect.objectContaining({ action: "update", id: "42" }),
|
||||
);
|
||||
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||
});
|
||||
it("delegates delete and redirects", async () => {
|
||||
await deleteHelpQuestion(fakeForm({ id: "42" }) as FormData);
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "help-question.change", { action: "delete", id: "42" });
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
"help-question.change",
|
||||
{ action: "delete", id: "42" },
|
||||
);
|
||||
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||
});
|
||||
});
|
||||
@@ -15,12 +15,16 @@ function helpInput(formData: FormData) {
|
||||
return {
|
||||
name: sanitizeField(formData.get("name")),
|
||||
content: canonicalize(String(formData.get("content") ?? "")),
|
||||
position: Number(formData.get("position")) > 0 ? Math.floor(Number(formData.get("position"))) : 1,
|
||||
position:
|
||||
Number(formData.get("position")) > 0
|
||||
? Math.floor(Number(formData.get("position")))
|
||||
: 1,
|
||||
imageUrl: sanitizeField(formData.get("imageUrl")),
|
||||
buttonText: sanitizeField(formData.get("buttonText")),
|
||||
buttonUrl: sanitizeField(formData.get("buttonUrl")),
|
||||
buttonColor: sanitizeField(formData.get("buttonColor"), 16) || "#eeb425",
|
||||
buttonBorderColor: sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15",
|
||||
buttonBorderColor:
|
||||
sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15",
|
||||
smallBox: formData.get("smallBox") != null,
|
||||
};
|
||||
}
|
||||
@@ -29,10 +33,16 @@ export async function createHelpQuestion(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const input = helpInput(formData);
|
||||
if (!input.name || !input.content) return;
|
||||
const result = await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "help-question.change", { action: "create", ...input });
|
||||
const result = await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"help-question.change",
|
||||
{ action: "create", ...input },
|
||||
);
|
||||
if (!result.ok) {
|
||||
revalidatePath("/admin/help-questions");
|
||||
redirect("/admin/help-questions/new?error=Unique name collision or database error. Please try again.");
|
||||
redirect(
|
||||
"/admin/help-questions/new?error=Unique name collision or database error. Please try again.",
|
||||
);
|
||||
}
|
||||
revalidatePath("/admin/help-questions");
|
||||
redirect("/admin/help-questions");
|
||||
@@ -44,7 +54,11 @@ export async function updateHelpQuestion(formData: FormData): Promise<void> {
|
||||
if (!/^[1-9]\d*$/u.test(id)) return;
|
||||
const input = helpInput(formData);
|
||||
if (!input.name || !input.content) return;
|
||||
const result = await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "help-question.change", { action: "update", id, ...input });
|
||||
const result = await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"help-question.change",
|
||||
{ action: "update", id, ...input },
|
||||
);
|
||||
if (!result.ok) {
|
||||
revalidatePath("/admin/help-questions/" + id);
|
||||
return;
|
||||
@@ -56,6 +70,10 @@ export async function deleteHelpQuestion(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const id = String(formData.get("id") ?? "").trim();
|
||||
if (!/^[1-9]\d*$/u.test(id)) return;
|
||||
await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "help-question.change", { action: "delete", id });
|
||||
await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"help-question.change",
|
||||
{ action: "delete", id },
|
||||
);
|
||||
redirect("/admin/help-questions");
|
||||
}
|
||||
@@ -5,20 +5,42 @@ import { requirePermission } from "@/lib/admin/guard";
|
||||
import { deleteMedia, uploadMedia, uploadMediaAndReturn } from "./admin-media";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({ contentMutationService: { execute }, createContentMutationInvocation: (actor, correlationId) => ({ expectedActorId: actor.id, correlationId, legacy: true }) }));
|
||||
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
|
||||
contentMutationService: { execute },
|
||||
createContentMutationInvocation: (actor, correlationId) => ({
|
||||
expectedActorId: actor.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue({ id: 1, rank: 7, username: "admin" });
|
||||
execute.mockResolvedValue({ ok: true, data: { before: null, after: { name: "photo.png" }, output: { url: "/api/media/photo.png" } }, correlationId: "legacy" });
|
||||
vi.mocked(requirePermission).mockResolvedValue({
|
||||
id: 1,
|
||||
rank: 7,
|
||||
username: "admin",
|
||||
});
|
||||
execute.mockResolvedValue({
|
||||
ok: true,
|
||||
data: {
|
||||
before: null,
|
||||
after: { name: "photo.png" },
|
||||
output: { url: "/api/media/photo.png" },
|
||||
},
|
||||
correlationId: "legacy",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Content media legacy wrappers", () => {
|
||||
it("retains no-file validation", async () => {
|
||||
expect(await uploadMedia(new FormData())).toEqual({ ok: false, error: "No file provided" });
|
||||
expect(await uploadMedia(new FormData())).toEqual({
|
||||
ok: false,
|
||||
error: "No file provided",
|
||||
});
|
||||
expect(await uploadMediaAndReturn(new FormData())).toBe("");
|
||||
expect(execute).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -28,11 +50,15 @@ describe("Content media legacy wrappers", () => {
|
||||
form.set("file", file);
|
||||
expect(await uploadMedia(form)).toEqual({ ok: true });
|
||||
expect(await uploadMediaAndReturn(form)).toBe("/api/media/photo.png");
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "media.upload", { file });
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "media.upload", {
|
||||
file,
|
||||
});
|
||||
});
|
||||
it("delegates deletion and preserves revalidation", async () => {
|
||||
await deleteMedia("photo.png");
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "media.delete", { filename: "photo.png" });
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "media.delete", {
|
||||
filename: "photo.png",
|
||||
});
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/api/media");
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/media");
|
||||
});
|
||||
|
||||
@@ -20,11 +20,14 @@ function mediaFile(formData: FormData): File | null {
|
||||
function validateMediaFile(file: File | null): string | null {
|
||||
if (!file || file.size === 0) return "No file provided";
|
||||
if (file.size > MAX_SIZE) return "File too large (max 5MB)";
|
||||
if (!ALLOWED.includes(file.type)) return "Invalid file type. Allowed: PNG, JPEG, GIF, WebP";
|
||||
if (!ALLOWED.includes(file.type))
|
||||
return "Invalid file type. Allowed: PNG, JPEG, GIF, WebP";
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function uploadMedia(formData: FormData): Promise<{ ok: boolean; error?: string }> {
|
||||
export async function uploadMedia(
|
||||
formData: FormData,
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const file = mediaFile(formData);
|
||||
const error = validateMediaFile(file);
|
||||
@@ -51,7 +54,9 @@ export async function deleteMedia(name: string): Promise<void> {
|
||||
revalidatePath("/admin/media");
|
||||
}
|
||||
|
||||
export async function uploadMediaAndReturn(formData: FormData): Promise<string> {
|
||||
export async function uploadMediaAndReturn(
|
||||
formData: FormData,
|
||||
): Promise<string> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const file = mediaFile(formData);
|
||||
if (validateMediaFile(file)) return "";
|
||||
@@ -63,5 +68,7 @@ export async function uploadMediaAndReturn(formData: FormData): Promise<string>
|
||||
if (!result.ok) return "";
|
||||
revalidatePath("/api/media");
|
||||
revalidatePath("/admin/media");
|
||||
return typeof result.data.output?.url === "string" ? result.data.output.url : "";
|
||||
return typeof result.data.output?.url === "string"
|
||||
? result.data.output.url
|
||||
: "";
|
||||
}
|
||||
@@ -22,7 +22,11 @@ export const saveAdminNavConfig = adminAction(
|
||||
},
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"navigation.update",
|
||||
ctx.data,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,11 @@ export async function deletePhoto(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const id = Number(formData.get("id"));
|
||||
if (!(id > 0)) return;
|
||||
await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "photo.delete", { id });
|
||||
await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"photo.delete",
|
||||
{ id },
|
||||
);
|
||||
revalidatePath("/admin/photos");
|
||||
revalidatePath("/photos");
|
||||
}
|
||||
@@ -5,7 +5,14 @@ import { requirePermission } from "@/lib/admin/guard";
|
||||
import { createTag, deleteTag, updateTag } from "./admin-tags";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({ contentMutationService: { execute }, createContentMutationInvocation: (actor, correlationId) => ({ expectedActorId: actor.id, correlationId, legacy: true }) }));
|
||||
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
|
||||
contentMutationService: { execute },
|
||||
createContentMutationInvocation: (actor, correlationId) => ({
|
||||
expectedActorId: actor.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
@@ -13,32 +20,61 @@ vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
const form = (data) => ({ get: (key) => data[key] ?? null });
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requirePermission).mockResolvedValue({ id: 1, rank: 7, username: "admin" });
|
||||
execute.mockResolvedValue({ ok: true, data: { before: null, after: { id: "1" } }, correlationId: "legacy" });
|
||||
vi.mocked(requirePermission).mockResolvedValue({
|
||||
id: 1,
|
||||
rank: 7,
|
||||
username: "admin",
|
||||
});
|
||||
execute.mockResolvedValue({
|
||||
ok: true,
|
||||
data: { before: null, after: { id: "1" } },
|
||||
correlationId: "legacy",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Content tag legacy wrappers", () => {
|
||||
it("delegates create with normalized values", async () => {
|
||||
await createTag(form({ name: "News", backgroundColor: "#ff0000" }));
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "tag.change", { action: "create", name: "News", backgroundColor: "#ff0000" });
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "tag.change", {
|
||||
action: "create",
|
||||
name: "News",
|
||||
backgroundColor: "#ff0000",
|
||||
});
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/tags");
|
||||
});
|
||||
it("uses the legacy default color", async () => {
|
||||
await createTag(form({ name: "Test" }));
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "tag.change", expect.objectContaining({ backgroundColor: "#888888" }));
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
"tag.change",
|
||||
expect.objectContaining({ backgroundColor: "#888888" }),
|
||||
);
|
||||
});
|
||||
it("returns early for an empty name", async () => {
|
||||
await createTag(form({ name: "" }));
|
||||
expect(execute).not.toHaveBeenCalled();
|
||||
});
|
||||
it("revalidates after a fail-soft dependency result", async () => {
|
||||
execute.mockResolvedValue({ ok: false, error: { code: "DEPENDENCY_UNAVAILABLE", messageKey: "errors.housekeeping.dependencyUnavailable" }, correlationId: "legacy" });
|
||||
execute.mockResolvedValue({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "DEPENDENCY_UNAVAILABLE",
|
||||
messageKey: "errors.housekeeping.dependencyUnavailable",
|
||||
},
|
||||
correlationId: "legacy",
|
||||
});
|
||||
await createTag(form({ name: "News" }));
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/tags");
|
||||
});
|
||||
it("delegates update", async () => {
|
||||
await updateTag(form({ id: "42", name: "Updated", backgroundColor: "#00ff00" }));
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "tag.change", expect.objectContaining({ action: "update", id: "42" }));
|
||||
await updateTag(
|
||||
form({ id: "42", name: "Updated", backgroundColor: "#00ff00" }),
|
||||
);
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
"tag.change",
|
||||
expect.objectContaining({ action: "update", id: "42" }),
|
||||
);
|
||||
});
|
||||
it("rejects invalid update id or name", async () => {
|
||||
await updateTag(form({ id: "", name: "Test" }));
|
||||
@@ -47,7 +83,10 @@ describe("Content tag legacy wrappers", () => {
|
||||
});
|
||||
it("delegates delete", async () => {
|
||||
await deleteTag(form({ id: "42" }));
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "tag.change", { action: "delete", id: "42" });
|
||||
expect(execute).toHaveBeenCalledWith(expect.anything(), "tag.change", {
|
||||
action: "delete",
|
||||
id: "42",
|
||||
});
|
||||
});
|
||||
it("rejects invalid delete id", async () => {
|
||||
await deleteTag(form({ id: "" }));
|
||||
|
||||
@@ -11,8 +11,13 @@ import { PERMS } from "@/lib/permissions";
|
||||
|
||||
function tagInput(formData: FormData) {
|
||||
return {
|
||||
name: String(formData.get("name") ?? "").trim().slice(0, 255),
|
||||
backgroundColor: String(formData.get("backgroundColor") ?? "").trim().slice(0, 10) || "#888888",
|
||||
name: String(formData.get("name") ?? "")
|
||||
.trim()
|
||||
.slice(0, 255),
|
||||
backgroundColor:
|
||||
String(formData.get("backgroundColor") ?? "")
|
||||
.trim()
|
||||
.slice(0, 10) || "#888888",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +25,11 @@ export async function createTag(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const input = tagInput(formData);
|
||||
if (!input.name) return;
|
||||
await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "tag.change", { action: "create", ...input });
|
||||
await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"tag.change",
|
||||
{ action: "create", ...input },
|
||||
);
|
||||
revalidatePath("/admin/tags");
|
||||
}
|
||||
|
||||
@@ -30,7 +39,11 @@ export async function updateTag(formData: FormData): Promise<void> {
|
||||
if (!/^[1-9]\d*$/u.test(id)) return;
|
||||
const input = tagInput(formData);
|
||||
if (!input.name) return;
|
||||
await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "tag.change", { action: "update", id, ...input });
|
||||
await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"tag.change",
|
||||
{ action: "update", id, ...input },
|
||||
);
|
||||
revalidatePath("/admin/tags");
|
||||
}
|
||||
|
||||
@@ -38,6 +51,10 @@ export async function deleteTag(formData: FormData): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const id = String(formData.get("id") ?? "").trim();
|
||||
if (!/^[1-9]\d*$/u.test(id)) return;
|
||||
await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "tag.change", { action: "delete", id });
|
||||
await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"tag.change",
|
||||
{ action: "delete", id },
|
||||
);
|
||||
revalidatePath("/admin/tags");
|
||||
}
|
||||
+64
-15
@@ -11,16 +11,34 @@ import { requirePermission } from "@/lib/admin/guard";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
|
||||
function formValues(formData: FormData): Record<string, string> {
|
||||
return Object.fromEntries(Array.from(formData.entries(), ([key, value]) => [key, typeof value === "string" ? value : value.name]));
|
||||
return Object.fromEntries(
|
||||
Array.from(formData.entries(), ([key, value]) => [
|
||||
key,
|
||||
typeof value === "string" ? value : value.name,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
async function executeTheme(operation: "theme.update" | "theme.apply-preset" | "theme.custom-change" | "theme.apply-custom", input: Record<string, unknown>) {
|
||||
async function executeTheme(
|
||||
operation:
|
||||
| "theme.update"
|
||||
| "theme.apply-preset"
|
||||
| "theme.custom-change"
|
||||
| "theme.apply-custom",
|
||||
input: Record<string, unknown>,
|
||||
) {
|
||||
const staff = await requirePermission(PERMS.SETTINGS_EDIT);
|
||||
return contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), operation, input);
|
||||
return contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
operation,
|
||||
input,
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveTheme(formData: FormData): Promise<void> {
|
||||
const result = await executeTheme("theme.update", { values: formValues(formData) });
|
||||
const result = await executeTheme("theme.update", {
|
||||
values: formValues(formData),
|
||||
});
|
||||
if (result.ok) revalidatePath("/", "layout");
|
||||
redirect("/admin/theme?saved=1");
|
||||
}
|
||||
@@ -29,39 +47,70 @@ export async function applyPreset(formData: FormData): Promise<void> {
|
||||
const name = String(formData.get("preset") ?? "").normalize("NFC");
|
||||
const result = await executeTheme("theme.apply-preset", { preset: name });
|
||||
if (result.ok) revalidatePath("/", "layout");
|
||||
redirect(result.ok ? "/admin/theme?preset=" + encodeURIComponent(name) : "/admin/theme");
|
||||
redirect(
|
||||
result.ok
|
||||
? "/admin/theme?preset=" + encodeURIComponent(name)
|
||||
: "/admin/theme",
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveCustomTheme(formData: FormData): Promise<void> {
|
||||
const name = String(formData.get("name") ?? "").normalize("NFC").trim();
|
||||
const name = String(formData.get("name") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!name) redirect("/admin/theme");
|
||||
const result = await executeTheme("theme.custom-change", { action: "create", name });
|
||||
const result = await executeTheme("theme.custom-change", {
|
||||
action: "create",
|
||||
name,
|
||||
});
|
||||
if (result.ok) revalidatePath("/admin/theme");
|
||||
redirect("/admin/theme?savedTheme=1");
|
||||
}
|
||||
|
||||
export async function applyCustomTheme(formData: FormData): Promise<void> {
|
||||
const id = String(formData.get("id") ?? "").normalize("NFC").trim();
|
||||
const id = String(formData.get("id") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!id) redirect("/admin/theme");
|
||||
const result = await executeTheme("theme.apply-custom", { id });
|
||||
if (result.ok) revalidatePath("/", "layout");
|
||||
const name = result.ok && typeof result.data.output?.name === "string" ? result.data.output.name : "";
|
||||
redirect(result.ok ? "/admin/theme?theme=" + encodeURIComponent(name) : "/admin/theme");
|
||||
const name =
|
||||
result.ok && typeof result.data.output?.name === "string"
|
||||
? result.data.output.name
|
||||
: "";
|
||||
redirect(
|
||||
result.ok
|
||||
? "/admin/theme?theme=" + encodeURIComponent(name)
|
||||
: "/admin/theme",
|
||||
);
|
||||
}
|
||||
|
||||
export async function renameCustomTheme(formData: FormData): Promise<void> {
|
||||
const id = String(formData.get("id") ?? "").normalize("NFC").trim();
|
||||
const name = String(formData.get("name") ?? "").normalize("NFC").trim();
|
||||
const id = String(formData.get("id") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
const name = String(formData.get("name") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!id || !name) redirect("/admin/theme");
|
||||
const result = await executeTheme("theme.custom-change", { action: "rename", id, name });
|
||||
const result = await executeTheme("theme.custom-change", {
|
||||
action: "rename",
|
||||
id,
|
||||
name,
|
||||
});
|
||||
if (result.ok) revalidatePath("/admin/theme");
|
||||
redirect("/admin/theme?renamed=1");
|
||||
}
|
||||
|
||||
export async function deleteCustomTheme(formData: FormData): Promise<void> {
|
||||
const id = String(formData.get("id") ?? "").normalize("NFC").trim();
|
||||
const id = String(formData.get("id") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!id) redirect("/admin/theme");
|
||||
const result = await executeTheme("theme.custom-change", { action: "delete", id });
|
||||
const result = await executeTheme("theme.custom-change", {
|
||||
action: "delete",
|
||||
id,
|
||||
});
|
||||
if (result.ok) revalidatePath("/admin/theme");
|
||||
redirect("/admin/theme?deletedTheme=1");
|
||||
}
|
||||
@@ -12,10 +12,17 @@ import { PERMS } from "@/lib/permissions";
|
||||
function boxInput(formData: FormData) {
|
||||
const position = Number(formData.get("position"));
|
||||
return {
|
||||
title: String(formData.get("title") ?? "").normalize("NFC").trim().slice(0, 255),
|
||||
icon: String(formData.get("icon") ?? "").normalize("NFC").trim().slice(0, 255),
|
||||
title: String(formData.get("title") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim()
|
||||
.slice(0, 255),
|
||||
icon: String(formData.get("icon") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim()
|
||||
.slice(0, 255),
|
||||
content: String(formData.get("content") ?? "").normalize("NFC"),
|
||||
position: Number.isFinite(position) && position >= 0 ? Math.floor(position) : 0,
|
||||
position:
|
||||
Number.isFinite(position) && position >= 0 ? Math.floor(position) : 0,
|
||||
isActive: String(formData.get("isActive") ?? "").normalize("NFC") === "1",
|
||||
};
|
||||
}
|
||||
@@ -25,13 +32,23 @@ function refreshBoxes(): void {
|
||||
revalidatePath("/", "layout");
|
||||
}
|
||||
|
||||
async function executeBox(formData: FormData, action: "create" | "update" | "delete" | "toggle"): Promise<boolean> {
|
||||
async function executeBox(
|
||||
formData: FormData,
|
||||
action: "create" | "update" | "delete" | "toggle",
|
||||
): Promise<boolean> {
|
||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
||||
const id = String(formData.get("id") ?? "").normalize("NFC").trim();
|
||||
const id = String(formData.get("id") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (action !== "create" && !/^\d+$/u.test(id)) return false;
|
||||
const input = boxInput(formData);
|
||||
if ((action === "create" || action === "update") && !input.title) return false;
|
||||
const result = await contentMutationService.execute(createContentMutationInvocation(staff, createCorrelationId()), "writeable-box.change", { action, ...(id ? { id } : {}), ...input, next: formData.get("next") });
|
||||
if ((action === "create" || action === "update") && !input.title)
|
||||
return false;
|
||||
const result = await contentMutationService.execute(
|
||||
createContentMutationInvocation(staff, createCorrelationId()),
|
||||
"writeable-box.change",
|
||||
{ action, ...(id ? { id } : {}), ...input, next: formData.get("next") },
|
||||
);
|
||||
return result.ok;
|
||||
}
|
||||
|
||||
|
||||
+16
-3
@@ -18,9 +18,20 @@ const bannerSchema = z.object({
|
||||
endDate: z.string().max(50).nullable().optional(),
|
||||
});
|
||||
|
||||
async function runBanner(ctx: { data: Record<string, unknown>; requestId: unknown; session: { user: { id: number } } }, action: "create" | "update" | "delete") {
|
||||
async function runBanner(
|
||||
ctx: {
|
||||
data: Record<string, unknown>;
|
||||
requestId: unknown;
|
||||
session: { user: { id: number } };
|
||||
},
|
||||
action: "create" | "update" | "delete",
|
||||
) {
|
||||
return contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"banner.change",
|
||||
{ action, ...ctx.data },
|
||||
);
|
||||
@@ -35,7 +46,9 @@ export const createBanner = adminAction(
|
||||
},
|
||||
);
|
||||
|
||||
const updateBannerInput = bannerSchema.partial().extend({ id: z.coerce.number().int().positive() });
|
||||
const updateBannerInput = bannerSchema
|
||||
.partial()
|
||||
.extend({ id: z.coerce.number().int().positive() });
|
||||
|
||||
export const updateBanner = adminAction(
|
||||
{ permission: PERMS.BANNERS_EDIT, schema: updateBannerInput },
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
// @ts-nocheck
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn(async () => ({ ok: true, data: { before: null, after: { keys: ["key1", "key2"] } }, correlationId: "emulator" })) }));
|
||||
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({ contentMutationService: { execute } }));
|
||||
vi.mock("@/lib/permissions", () => ({ PERMS: { SETTINGS_EDIT: "settings.edit" } }));
|
||||
vi.mock("@/lib/safe-action", () => ({ adminAction: (_options, handler) => handler }));
|
||||
const { execute } = vi.hoisted(() => ({
|
||||
execute: vi.fn(async () => ({
|
||||
ok: true,
|
||||
data: { before: null, after: { keys: ["key1", "key2"] } },
|
||||
correlationId: "emulator",
|
||||
})),
|
||||
}));
|
||||
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
|
||||
contentMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/permissions", () => ({
|
||||
PERMS: { SETTINGS_EDIT: "settings.edit" },
|
||||
}));
|
||||
vi.mock("@/lib/safe-action", () => ({
|
||||
adminAction: (_options, handler) => handler,
|
||||
}));
|
||||
vi.mock("@/lib/safe-action-shared", () => ({ actionOk: () => "ok" }));
|
||||
|
||||
describe("saveEmulatorSettings", () => {
|
||||
it("delegates the third translation store and preserves result shape", async () => {
|
||||
const handler = (await import("./emulator")).saveEmulatorSettings as unknown as (ctx: unknown) => Promise<string>;
|
||||
const result = await handler({ data: { settings: { key1: "val1", key2: "val2" } }, session: { user: { id: "1" } }, requestId: "emulator" });
|
||||
expect(execute).toHaveBeenCalledWith({ correlationId: "emulator", expectedActorId: 1, legacy: true }, "translation.emulator.save", { settings: { key1: "val1", key2: "val2" } });
|
||||
const handler = (await import("./emulator"))
|
||||
.saveEmulatorSettings as unknown as (ctx: unknown) => Promise<string>;
|
||||
const result = await handler({
|
||||
data: { settings: { key1: "val1", key2: "val2" } },
|
||||
session: { user: { id: "1" } },
|
||||
requestId: "emulator",
|
||||
});
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
{ correlationId: "emulator", expectedActorId: 1, legacy: true },
|
||||
"translation.emulator.save",
|
||||
{ settings: { key1: "val1", key2: "val2" } },
|
||||
);
|
||||
expect(result).toBe("ok");
|
||||
});
|
||||
});
|
||||
@@ -6,13 +6,19 @@ import { PERMS } from "@/lib/permissions";
|
||||
import { adminAction } from "@/lib/safe-action";
|
||||
import { actionOk } from "@/lib/safe-action-shared";
|
||||
|
||||
const saveEmulatorSettingsSchema = z.object({ settings: z.record(z.string(), z.string()) });
|
||||
const saveEmulatorSettingsSchema = z.object({
|
||||
settings: z.record(z.string(), z.string()),
|
||||
});
|
||||
|
||||
export const saveEmulatorSettings = adminAction(
|
||||
{ permission: PERMS.SETTINGS_EDIT, schema: saveEmulatorSettingsSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"translation.emulator.save",
|
||||
ctx.data,
|
||||
);
|
||||
|
||||
+45
-9
@@ -28,7 +28,11 @@ export const createEventType = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: eventTypeSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event-type.change",
|
||||
{ action: "create", ...ctx.data },
|
||||
);
|
||||
@@ -45,7 +49,11 @@ export const updateEventType = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: updateEventTypeInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event-type.change",
|
||||
{ action: "update", ...ctx.data },
|
||||
);
|
||||
@@ -62,7 +70,11 @@ export const deleteEventType = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: deleteEventTypeInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event-type.change",
|
||||
{ action: "delete", ...ctx.data },
|
||||
);
|
||||
@@ -77,7 +89,11 @@ export const createEvent = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: createEventSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event.change",
|
||||
{ action: "create", ...ctx.data },
|
||||
);
|
||||
@@ -94,7 +110,11 @@ export const updateEvent = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: updateEventInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event.change",
|
||||
{ action: "update", ...ctx.data },
|
||||
);
|
||||
@@ -111,7 +131,11 @@ export const deleteEvent = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: deleteEventInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event.change",
|
||||
{ action: "delete", ...ctx.data },
|
||||
);
|
||||
@@ -126,7 +150,11 @@ export const addEventPrize = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: eventPrizeSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event-prize.change",
|
||||
{ action: "create", ...ctx.data },
|
||||
);
|
||||
@@ -141,7 +169,11 @@ export const deleteEventPrize = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: deletePrizeInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event-prize.change",
|
||||
{ action: "delete", ...ctx.data },
|
||||
);
|
||||
@@ -156,7 +188,11 @@ export const addEventWinner = adminAction(
|
||||
{ permission: PERMS.EVENTS_EDIT, schema: eventWinnerSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"event-winner.add",
|
||||
ctx.data,
|
||||
);
|
||||
|
||||
@@ -11,12 +11,15 @@ function wrapper(
|
||||
options: {
|
||||
permission: string | readonly string[];
|
||||
schema?: {
|
||||
safeParse(value: unknown):
|
||||
| { success: true; data: unknown }
|
||||
| { success: false; error: unknown };
|
||||
safeParse(
|
||||
value: unknown,
|
||||
): { success: true; data: unknown } | { success: false; error: unknown };
|
||||
};
|
||||
},
|
||||
handler: (context: { data: unknown; session: { user: typeof staff } }) => unknown,
|
||||
handler: (context: {
|
||||
data: unknown;
|
||||
session: { user: typeof staff };
|
||||
}) => unknown,
|
||||
) {
|
||||
registrations.push(options);
|
||||
return async (data: unknown) => {
|
||||
@@ -174,9 +177,10 @@ describe("legacy People support and moderation wrappers", () => {
|
||||
await call(replyHelpCenterTicket, { ticketId, content: " Handled " });
|
||||
await call(closeHelpCenterTicket, { ticketId });
|
||||
await call(reopenHelpCenterTicket, { ticketId });
|
||||
await expect(
|
||||
call(liftBanFromHelpTicket, { ticketId }),
|
||||
).resolves.toEqual({ ok: true, data: { removed: 2, userId: 7 } });
|
||||
await expect(call(liftBanFromHelpTicket, { ticketId })).resolves.toEqual({
|
||||
ok: true,
|
||||
data: { removed: 2, userId: 7 },
|
||||
});
|
||||
|
||||
expect(execute.mock.calls.map((entry) => entry[2])).toEqual([
|
||||
{ ticketId: "9007199254740993", content: "Handled" },
|
||||
|
||||
+30
-6
@@ -26,7 +26,11 @@ export const createPoll = adminAction(
|
||||
{ permission: PERMS.POLLS_EDIT, schema: createPollSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"poll.change",
|
||||
{ action: "create", ...ctx.data },
|
||||
);
|
||||
@@ -43,7 +47,11 @@ export const updatePoll = adminAction(
|
||||
{ permission: PERMS.POLLS_EDIT, schema: updatePollInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"poll.change",
|
||||
{ action: "update", ...ctx.data },
|
||||
);
|
||||
@@ -60,7 +68,11 @@ export const deletePoll = adminAction(
|
||||
{ permission: PERMS.POLLS_EDIT, schema: deletePollInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"poll.change",
|
||||
{ action: "delete", ...ctx.data },
|
||||
);
|
||||
@@ -75,7 +87,11 @@ export const addPollQuestion = adminAction(
|
||||
{ permission: PERMS.POLLS_EDIT, schema: pollQuestionSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"poll-question.change",
|
||||
{ action: "create", ...ctx.data },
|
||||
);
|
||||
@@ -92,7 +108,11 @@ export const updatePollQuestion = adminAction(
|
||||
{ permission: PERMS.POLLS_EDIT, schema: updateQuestionInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"poll-question.change",
|
||||
{ action: "update", ...ctx.data },
|
||||
);
|
||||
@@ -109,7 +129,11 @@ export const deletePollQuestion = adminAction(
|
||||
{ permission: PERMS.POLLS_EDIT, schema: deleteQuestionInput },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"poll-question.change",
|
||||
{ action: "delete", ...ctx.data },
|
||||
);
|
||||
|
||||
+50
-11
@@ -24,23 +24,62 @@ const updatePrefixSchema = z.object({
|
||||
});
|
||||
const deletePrefixSchema = z.object({ id: z.coerce.number().int().positive() });
|
||||
const addBlacklistWordSchema = z.object({ word: z.string().min(1).max(100) });
|
||||
const removeBlacklistWordSchema = z.object({ id: z.coerce.number().int().positive() });
|
||||
const updatePrefixSettingsSchema = z.object({ settings: z.record(z.string(), z.string()) });
|
||||
const removeBlacklistWordSchema = z.object({
|
||||
id: z.coerce.number().int().positive(),
|
||||
});
|
||||
const updatePrefixSettingsSchema = z.object({
|
||||
settings: z.record(z.string(), z.string()),
|
||||
});
|
||||
|
||||
async function run(ctx: { data: Record<string, unknown>; requestId: unknown; session: { user: { id: number } } }, operation: "prefix.change" | "prefix-blacklist.change" | "prefix-settings.update", input: Record<string, unknown>) {
|
||||
async function run(
|
||||
ctx: {
|
||||
data: Record<string, unknown>;
|
||||
requestId: unknown;
|
||||
session: { user: { id: number } };
|
||||
},
|
||||
operation:
|
||||
| "prefix.change"
|
||||
| "prefix-blacklist.change"
|
||||
| "prefix-settings.update",
|
||||
input: Record<string, unknown>,
|
||||
) {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
operation,
|
||||
input,
|
||||
);
|
||||
if (!result.ok && result.error.code === "NOT_FOUND") throw new ActionError("User not found");
|
||||
if (!result.ok && result.error.code === "NOT_FOUND")
|
||||
throw new ActionError("User not found");
|
||||
if (!result.ok) throw new Error(result.error.messageKey);
|
||||
return actionOk();
|
||||
}
|
||||
|
||||
export const createPrefix = adminAction({ permission: PERMS.PREFIXES_EDIT, schema: createPrefixSchema }, (ctx) => run(ctx, "prefix.change", { action: "create", ...ctx.data }));
|
||||
export const updatePrefix = adminAction({ permission: PERMS.PREFIXES_EDIT, schema: updatePrefixSchema }, (ctx) => run(ctx, "prefix.change", { action: "update", ...ctx.data }));
|
||||
export const deletePrefix = adminAction({ permission: PERMS.PREFIXES_EDIT, schema: deletePrefixSchema }, (ctx) => run(ctx, "prefix.change", { action: "delete", ...ctx.data }));
|
||||
export const addBlacklistWord = adminAction({ permission: PERMS.PREFIXES_EDIT, schema: addBlacklistWordSchema }, (ctx) => run(ctx, "prefix-blacklist.change", { action: "add", ...ctx.data }));
|
||||
export const removeBlacklistWord = adminAction({ permission: PERMS.PREFIXES_EDIT, schema: removeBlacklistWordSchema }, (ctx) => run(ctx, "prefix-blacklist.change", { action: "remove", ...ctx.data }));
|
||||
export const updatePrefixSettings = adminAction({ permission: PERMS.PREFIXES_EDIT, schema: updatePrefixSettingsSchema }, (ctx) => run(ctx, "prefix-settings.update", ctx.data));
|
||||
export const createPrefix = adminAction(
|
||||
{ permission: PERMS.PREFIXES_EDIT, schema: createPrefixSchema },
|
||||
(ctx) => run(ctx, "prefix.change", { action: "create", ...ctx.data }),
|
||||
);
|
||||
export const updatePrefix = adminAction(
|
||||
{ permission: PERMS.PREFIXES_EDIT, schema: updatePrefixSchema },
|
||||
(ctx) => run(ctx, "prefix.change", { action: "update", ...ctx.data }),
|
||||
);
|
||||
export const deletePrefix = adminAction(
|
||||
{ permission: PERMS.PREFIXES_EDIT, schema: deletePrefixSchema },
|
||||
(ctx) => run(ctx, "prefix.change", { action: "delete", ...ctx.data }),
|
||||
);
|
||||
export const addBlacklistWord = adminAction(
|
||||
{ permission: PERMS.PREFIXES_EDIT, schema: addBlacklistWordSchema },
|
||||
(ctx) => run(ctx, "prefix-blacklist.change", { action: "add", ...ctx.data }),
|
||||
);
|
||||
export const removeBlacklistWord = adminAction(
|
||||
{ permission: PERMS.PREFIXES_EDIT, schema: removeBlacklistWordSchema },
|
||||
(ctx) =>
|
||||
run(ctx, "prefix-blacklist.change", { action: "remove", ...ctx.data }),
|
||||
);
|
||||
export const updatePrefixSettings = adminAction(
|
||||
{ permission: PERMS.PREFIXES_EDIT, schema: updatePrefixSettingsSchema },
|
||||
(ctx) => run(ctx, "prefix-settings.update", ctx.data),
|
||||
);
|
||||
@@ -17,10 +17,7 @@ const templateSchema = z.object({
|
||||
sortOrder: z.coerce.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
async function execute(
|
||||
staff: { readonly id: number },
|
||||
input: unknown,
|
||||
) {
|
||||
async function execute(staff: { readonly id: number }, input: unknown) {
|
||||
const result = await peopleMutationService.execute(
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"ticket-template.change",
|
||||
|
||||
@@ -31,7 +31,9 @@ async function execute(
|
||||
);
|
||||
if (!result.ok) {
|
||||
throw new ActionError(
|
||||
result.error.code === "NOT_FOUND" ? "Ticket not found" : "Ticket update failed",
|
||||
result.error.code === "NOT_FOUND"
|
||||
? "Ticket not found"
|
||||
: "Ticket update failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,36 @@ import { adminAction } from "@/lib/safe-action";
|
||||
import { ActionError, actionOk } from "@/lib/safe-action-shared";
|
||||
|
||||
const saveTranslationsSchema = z.object({
|
||||
locale: z.enum(["en", "it", "nl", "de", "fr", "es", "pt", "pl", "sv", "tr", "ro", "hu", "cs", "sk", "da", "no", "el", "bg", "hr", "sr", "uk", "ru"]),
|
||||
locale: z.enum([
|
||||
"en",
|
||||
"it",
|
||||
"nl",
|
||||
"de",
|
||||
"fr",
|
||||
"es",
|
||||
"pt",
|
||||
"pl",
|
||||
"sv",
|
||||
"tr",
|
||||
"ro",
|
||||
"hu",
|
||||
"cs",
|
||||
"sk",
|
||||
"da",
|
||||
"no",
|
||||
"el",
|
||||
"bg",
|
||||
"hr",
|
||||
"sr",
|
||||
"uk",
|
||||
"ru",
|
||||
]),
|
||||
data: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
const saveClientTranslationsSchema = z.object({
|
||||
fileId: z.enum(CLIENT_TRANSLATION_FILES.map((file) => file.id) as [string, ...string[]]),
|
||||
fileId: z.enum(
|
||||
CLIENT_TRANSLATION_FILES.map((file) => file.id) as [string, ...string[]],
|
||||
),
|
||||
data: z.record(z.string(), z.string()),
|
||||
});
|
||||
|
||||
@@ -20,7 +45,11 @@ export const saveTranslations = adminAction(
|
||||
{ permission: PERMS.SETTINGS_EDIT, schema: saveTranslationsSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"translation.cms.save",
|
||||
ctx.data,
|
||||
);
|
||||
@@ -33,14 +62,20 @@ export const saveClientTranslations = adminAction(
|
||||
{ permission: PERMS.SETTINGS_EDIT, schema: saveClientTranslationsSchema },
|
||||
async (ctx) => {
|
||||
const result = await contentMutationService.execute(
|
||||
{ correlationId: String(ctx.requestId), expectedActorId: Number(ctx.session.user.id), legacy: true },
|
||||
{
|
||||
correlationId: String(ctx.requestId),
|
||||
expectedActorId: Number(ctx.session.user.id),
|
||||
legacy: true,
|
||||
},
|
||||
"translation.client.save",
|
||||
ctx.data,
|
||||
);
|
||||
if (!result.ok) throw new ActionError("Translation save failed");
|
||||
return actionOk({
|
||||
commentsLost: result.data.output?.commentsLost === true,
|
||||
unpatchedKeys: Array.isArray(result.data.output?.unpatchedKeys) ? result.data.output.unpatchedKeys : [],
|
||||
unpatchedKeys: Array.isArray(result.data.output?.unpatchedKeys)
|
||||
? result.data.output.unpatchedKeys
|
||||
: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user