Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/rows/routes/Rows_Overview.server.ts
T

381 lines
16 KiB
TypeScript

import { LoaderFunctionArgs, redirect, ActionFunction } from "react-router";
import type { TFunction } from "i18next";
import { MetaTagsDto } from "~/application/dtos/seo/MetaTagsDto";
import NotificationService from "~/modules/notifications/services/.server/NotificationService";
import { EntitiesApi } from "~/utils/api/.server/EntitiesApi";
import { RowCommentsApi } from "~/utils/api/.server/RowCommentsApi";
import { RowsApi } from "~/utils/api/.server/RowsApi";
import UrlUtils from "~/utils/app/UrlUtils";
import { setRowCommentReaction } from "~/utils/db/entities/rowCommentReaction.db.server";
import { getRowCommentInRow, updateRowCommentInRow } from "~/utils/db/entities/rowComments.db.server";
import { createRowTask, deleteRowTaskInRow, getRowTaskInRow, updateRowTaskInRow } from "~/utils/db/entities/rowTasks.db.server";
import { getUser } from "~/utils/db/users.db.server";
import EntityHelper from "~/utils/helpers/EntityHelper";
import { getEntityPermission } from "~/utils/helpers/PermissionsHelper";
import { verifyUserHasPermission } from "~/utils/helpers/.server/PermissionsService";
import RowHelper from "~/utils/helpers/RowHelper";
import RowsRequestUtils from "../utils/RowsRequestUtils";
import { createMetrics } from "~/modules/metrics/services/.server/MetricTracker";
import FormulaService from "~/modules/formulas/services/.server/FormulaService";
import EventsService from "~/modules/events/services/.server/EventsService";
import { RowTasksCreatedDto } from "~/modules/events/dtos/RowTasksCreatedDto";
import { RowTasksUpdatedDto } from "~/modules/events/dtos/RowTasksUpdatedDto";
import { RowTasksDeletedDto } from "~/modules/events/dtos/RowTasksDeletedDto";
import { RowCommentsCreatedDto } from "~/modules/events/dtos/RowCommentsCreatedDto";
import { RowCommentsReactedDto } from "~/modules/events/dtos/RowCommentsReactedDto";
import { RowCommentsDeletedDto } from "~/modules/events/dtos/RowCommentsDeletedDto";
import { safeRedirectPathFromRequest } from "~/utils/app/RedirectSafety.server";
import i18next from "i18next";
import { translateEntityTitlePlural } from "~/utils/i18n/translateLabel";
import { getRowsRouteErrorCode, getRowsRouteErrorStatus, translateRowsRouteError } from "./Rows_Error.server";
export namespace Rows_Overview {
export type LoaderData = {
meta: MetaTagsDto;
rowData: RowsApi.GetRowData;
routes: EntitiesApi.Routes;
relationshipRows: RowsApi.GetRelationshipRowsData;
};
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { time, getServerTimingHeader } = await createMetrics({ request, params }, `[Rows_Overview] ${params.entity}`);
const { t, userId, tenantId, entity } = await RowsRequestUtils.getLoader({ request, params });
await time(verifyUserHasPermission(request, getEntityPermission(entity, "read"), tenantId), "verifyUserHasPermission");
if (!entity.isAutogenerated || entity.type === "system") {
throw redirect(tenantId ? UrlUtils.currentTenantUrl(params, "404") : "/404");
}
const rowData = await time(
RowsApi.get(params.id!, {
entity,
tenantId,
userId,
}),
"RowsApi.get"
);
await time(
FormulaService.trigger({ trigger: "BEFORE_VIEWED", rows: [rowData.item], entity: rowData.entity, session: { tenantId, userId }, t }),
"FormulaService.trigger.BEFORE_VIEWED"
);
const entityPluralLabel = translateEntityTitlePlural(t, i18next, entity);
const data: LoaderData = {
meta: [{ title: `${RowHelper.getTextDescription({ entity, item: rowData.item, t })} | ${entityPluralLabel} | ${process.env.APP_NAME}` }],
rowData,
routes: EntitiesApi.getNoCodeRoutes({ request, params }),
relationshipRows: await time(RowsApi.getRelationshipRows({ entity, tenantId, userId }), "RowsApi.getRelationshipRows"),
};
return Response.json(data, { headers: getServerTimingHeader() });
};
export type ActionData = {
updatedRow?: RowsApi.GetRowData;
success?: string;
error?: string;
errorCode?: import("~/utils/api/.server/RowsApi").RowApiErrorCode;
};
export const action: ActionFunction = async ({ request, params }) => {
const { time, getServerTimingHeader } = await createMetrics({ request, params }, `[Rows_Overview] ${params.entity}`);
const { t, userId, tenantId, entity, form } = await RowsRequestUtils.getAction({ request, params });
const action = form.get("action")?.toString() ?? "";
const user = await time(getUser(userId), "getUser");
const rowData = await time(
RowsApi.get(params.id!, {
entity,
tenantId,
userId,
}),
"RowsApi.get"
);
const { item } = rowData;
if (action === "edit") {
try {
const rowValues = RowHelper.getRowPropertiesFromForm({ t, entity, form, existing: item });
await time(
RowsApi.update(params.id!, {
entity,
request,
tenantId,
userId,
rowValues,
}),
"RowsApi.update"
);
} catch (error: unknown) {
return rowActionError(t, error, getServerTimingHeader());
}
// const redirectTo = form.get("redirect")?.toString() || new URL(request.url).searchParams.get("redirect")?.toString();
// if (redirectTo) {
// return redirect(redirectTo, { headers: getServerTimingHeader() });
// }
const updatedRow = await time(RowsApi.get(params.id!, { entity, tenantId, userId }), "RowsApi.get");
return Response.json({ updatedRow, success: t("shared.saved") }, { headers: getServerTimingHeader() });
} else if (action === "delete") {
try {
await time(verifyUserHasPermission(request, getEntityPermission(entity, "delete"), tenantId), "verifyUserHasPermission");
await time(
RowsApi.del(params.id!, {
entity,
request,
tenantId,
userId,
}),
"RowsApi.del"
);
if (item.createdByUser) {
await time(
NotificationService.send({
channel: "my-rows",
to: item.createdByUser,
tenantId,
notification: {
from: { user },
message: t("notifications.messages.row.deleted", {
email: user?.email ?? t("shared.unknown"),
row: RowHelper.getTextDescription({ entity, item, t }),
}),
action: {
title: t("shared.view"),
url: EntityHelper.getRoutes({ routes: EntitiesApi.getNoCodeRoutes({ request, params }), entity, item })?.overview ?? "",
},
},
}),
"NotificationService.send"
);
}
} catch (error: unknown) {
return rowActionError(t, error, getServerTimingHeader());
}
const redirectTo = form.get("redirect")?.toString() || new URL(request.url).searchParams.get("redirect")?.toString();
if (redirectTo) {
return redirect(safeRedirectPathFromRequest(request, redirectTo), { headers: getServerTimingHeader() });
}
const routes = EntitiesApi.getNoCodeRoutes({ request, params });
const listRoute = EntityHelper.getRoutes({ routes, entity })?.list;
if (listRoute) {
return redirect(listRoute, { headers: getServerTimingHeader() });
}
return Response.json({ deleted: true }, { headers: getServerTimingHeader() });
} else if (action === "comment") {
if (!rowData.rowPermissions.canComment) {
return rowActionForbidden(t, getServerTimingHeader());
}
let comment = form.get("comment")?.toString();
if (!comment) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400, headers: getServerTimingHeader() });
}
await time(
RowCommentsApi.create(item.id, {
comment,
tenantId,
userId,
}),
"RowCommentsApi.create"
);
await EventsService.create({
request,
event: "row.comments.created",
tenantId,
userId,
data: {
rowId: item.id,
comment: { id: item.id, text: comment },
user: !user ? null : { id: user.id, email: user.email },
} satisfies RowCommentsCreatedDto,
});
if (item.createdByUser) {
await time(
NotificationService.send({
channel: "my-rows",
to: item.createdByUser,
tenantId,
notification: {
from: { user },
message: t("notifications.messages.row.commented", {
email: user?.email ?? t("shared.unknown"),
row: RowHelper.getTextDescription({ entity, item, t }),
}),
action: {
title: t("shared.view"),
url: EntityHelper.getRoutes({ routes: EntitiesApi.getNoCodeRoutes({ request, params }), entity, item })?.overview ?? "",
},
},
}),
"NotificationService.send"
);
}
return Response.json({ newComment: comment }, { headers: getServerTimingHeader() });
} else if (action === "comment-reaction") {
if (!rowData.rowPermissions.canComment) {
return rowActionForbidden(t, getServerTimingHeader());
}
const rowCommentId = form.get("comment-id")?.toString();
const reaction = form.get("reaction")?.toString();
if (!rowCommentId || !reaction) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400, headers: getServerTimingHeader() });
}
const comment = await time(getRowCommentInRow({ id: rowCommentId, rowId: item.id }), "getRowCommentInRow");
if (!comment) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400, headers: getServerTimingHeader() });
}
if (comment) {
await time(
setRowCommentReaction({
createdByUserId: userId,
rowCommentId,
reaction,
}),
"setRowCommentReaction"
);
await EventsService.create({
request,
event: "row.comments.reacted",
tenantId,
userId,
data: {
rowId: item.id,
comment: { id: comment.id, text: comment.value },
reaction,
user: !user ? null : { id: user.id, email: user.email },
} satisfies RowCommentsReactedDto,
});
}
return Response.json({ newCommentReaction: reaction }, { headers: getServerTimingHeader() });
} else if (action === "comment-delete") {
const rowCommentId = form.get("comment-id")?.toString() ?? "";
const comment = await getRowCommentInRow({ id: rowCommentId, rowId: item.id });
if (!comment) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400, headers: getServerTimingHeader() });
}
if (!rowData.rowPermissions.canUpdate && comment.createdByUserId !== userId) {
return rowActionForbidden(t, getServerTimingHeader());
}
await time(updateRowCommentInRow({ id: comment.id, rowId: item.id }, { isDeleted: true }), "updateRowCommentInRow");
await EventsService.create({
request,
event: "row.comments.deleted",
tenantId,
userId,
data: {
rowId: item.id,
comment: { id: comment.id, text: comment.value },
user: !user ? null : { id: user.id, email: user.email },
} satisfies RowCommentsDeletedDto,
});
return Response.json({ deletedComment: rowCommentId }, { headers: getServerTimingHeader() });
} else if (action === "task-new") {
if (!rowData.rowPermissions.canUpdate) {
return rowActionForbidden(t, getServerTimingHeader());
}
const taskTitle = form.get("task-title")?.toString();
if (!taskTitle) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400, headers: getServerTimingHeader() });
}
const task = await time(
createRowTask({
createdByUserId: userId,
rowId: item.id,
title: taskTitle,
}),
"createRowTask"
);
await EventsService.create({
request,
event: "row.tasks.created",
tenantId,
userId,
data: {
rowId: item.id,
task: { id: task.id, name: task.title },
user: !user ? null : { id: user.id, email: user.email },
} satisfies RowTasksCreatedDto,
});
return Response.json({ newTask: task }, { headers: getServerTimingHeader() });
} else if (action === "task-complete-toggle") {
if (!rowData.rowPermissions.canUpdate) {
return rowActionForbidden(t, getServerTimingHeader());
}
const taskId = form.get("task-id")?.toString() ?? "";
const task = await time(getRowTaskInRow({ id: taskId, rowId: item.id }), "getRowTaskInRow");
if (!task) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400, headers: getServerTimingHeader() });
}
if (task) {
if (task.completed) {
await time(
updateRowTaskInRow(
{ id: taskId, rowId: item.id },
{
completed: false,
completedAt: null,
completedByUserId: null,
}
),
"updateRowTask"
);
} else {
await time(
updateRowTaskInRow(
{ id: taskId, rowId: item.id },
{
completed: true,
completedAt: new Date(),
completedByUserId: userId,
}
),
"updateRowTask"
);
}
await EventsService.create({
request,
event: "row.tasks.created",
tenantId,
userId,
data: {
rowId: item.id,
old: { id: task.id, name: task.title, completedAt: task.completedAt },
new: { id: task.id, name: task.title, completedAt: task.completed ? new Date() : null },
user: !user ? null : { id: user.id, email: user.email },
} satisfies RowTasksUpdatedDto,
});
}
return Response.json({ completedTask: taskId }, { headers: getServerTimingHeader() });
} else if (action === "task-delete") {
if (!rowData.rowPermissions.canUpdate) {
return rowActionForbidden(t, getServerTimingHeader());
}
const taskId = form.get("task-id")?.toString() ?? "";
const task = await time(getRowTaskInRow({ id: taskId, rowId: item.id }), "getRowTaskInRow");
if (!task) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400, headers: getServerTimingHeader() });
}
if (task) {
await time(deleteRowTaskInRow({ id: taskId, rowId: item.id }), "deleteRowTaskInRow");
await EventsService.create({
request,
event: "row.tasks.deleted",
tenantId,
userId,
data: {
rowId: item.id,
task: { id: task.id, name: task.title },
user: !user ? null : { id: user.id, email: user.email },
} satisfies RowTasksDeletedDto,
});
}
return Response.json({ deletedTask: taskId }, { headers: getServerTimingHeader() });
} else {
return Response.json({ error: t("shared.invalidForm") }, { status: 400, headers: getServerTimingHeader() });
}
};
}
function rowActionForbidden(t: TFunction, headers: HeadersInit) {
return Response.json({ error: t("foundation.errors.forbidden") }, { status: 403, headers });
}
function rowActionError(t: TFunction, error: unknown, headers: HeadersInit) {
return Response.json(
{ error: translateRowsRouteError(t, error), errorCode: getRowsRouteErrorCode(error) } satisfies Rows_Overview.ActionData,
{ status: getRowsRouteErrorStatus(error), headers }
);
}