Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import ActivityApiTemplate from "./routes/api/CodeTemplate.Routes.Activity.Api";
import EditApiTemplate from "./routes/api/CodeTemplate.Routes.Edit.Api";
import ShareApiTemplate from "./routes/api/CodeTemplate.Routes.Share.Api";
import TagsApiTemplate from "./routes/api/CodeTemplate.Routes.Tags.Api";
import ServiceTemplate from "./services/CodeTemplate.Service";
import type { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
const entity = {
name: "tickets",
title: "Tickets",
titlePlural: "Tickets",
prefix: "TCK",
hasTasks: true,
properties: [],
} as unknown as EntityWithDetails;
describe("generated entity route tenant scope", () => {
it("generates edit actions through scoped services and validates task row ownership", () => {
const source = EditApiTemplate.generate({ entity });
expect(source).not.toContain('import { getRowById } from "~/utils/db/entities/rows.db.server"');
expect(source).toContain("const item = await TicketsService.get(params.id!, {");
expect(source).toContain("tenantId,");
expect(source).toContain("userId,");
expect(source).toContain("getRowTaskInRow({ id: taskId, rowId: item.row.id })");
expect(source).toContain("updateRowTaskInRow({ id: taskId, rowId: item.row.id }");
expect(source).toContain("deleteRowTaskInRow({ id: taskId, rowId: item.row.id })");
expect(source).toContain("if (!task)");
expect(source).not.toContain("getRowTask(taskId)");
expect(source).toContain("to: item.row.createdByUser");
expect(source).toContain("tenantId,");
expect(source.match(/if \(!rowPermissions\.canUpdate\)/g)).toHaveLength(3);
expect(source).toContain('return Response.json({ error: t("shared.unauthorized") }, { status: 403 });');
expect(source).not.toContain('Response.json({ error: t("shared.notFound"), status: 404 })');
expect(source).not.toContain('Response.json({ error: t("shared.unauthorized"), status: 404 })');
});
it("generates update services through RowsApi tenant and permission checks", () => {
const source = ServiceTemplate.generate({ entity });
expect(source).not.toContain("getRowById");
expect(source).toContain("const { item: row, rowPermissions } = await RowsApi.get(id, {");
expect(source).toContain("tenantId: session.tenantId");
expect(source).toContain("userId: session.userId");
expect(source).toContain("if (!row || !rowPermissions.canUpdate)");
});
it("generates activity comment mutations with row-scoped lookups", () => {
const source = ActivityApiTemplate.generate({ entity });
expect(source).toContain("tenantId,");
expect(source).toContain("getRowCommentInRow({ id: rowCommentId, rowId: rowData.item.id })");
expect(source).toContain("updateRowCommentInRow({ id: rowCommentId, rowId: rowData.item.id }");
expect(source).toContain('t("models.row.activity.errors.commentForbidden")');
expect(source).toContain('t("models.row.activity.errors.updateCommentForbidden")');
expect(source).toContain('t("models.row.activity.errors.deleteCommentForbidden")');
expect(source).toContain('t("notifications.messages.row.commented"');
expect(source).not.toContain("getRowComment(rowCommentId)");
expect(source).not.toContain("You don't have access");
expect(source).not.toContain("commented on");
});
it("generates missing-record responses with HTTP status init", () => {
const sources = [
EditApiTemplate.generate({ entity }),
TagsApiTemplate.generate({ entity }),
ShareApiTemplate.generate({ entity }),
ActivityApiTemplate.generate({ entity }),
];
for (const source of sources) {
expect(source).not.toContain('Response.json({ error: t("shared.notFound"), status: 404 })');
expect(source).not.toContain('Response.json({ error: t("shared.unauthorized"), status: 403 })');
}
});
});
@@ -0,0 +1,114 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import CodeGeneratorPropertiesHelper from "../../utils/CodeGeneratorPropertiesHelper";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
const imports: string[] = [];
imports.push(`import { useNavigation, useSubmit, Form } from "react-router";
import { useTranslation } from "react-i18next";
import { useRef } from "react";
import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary";
import ButtonSecondary from "~/components/ui/buttons/ButtonSecondary";
import InputGroup from "~/components/ui/forms/InputGroup";
import ActionResultModal from "~/components/ui/modals/ActionResultModal";
import ConfirmModal, { RefConfirmModal } from "~/components/ui/modals/ConfirmModal";
import { useRootData } from "~/utils/data/useRootData";`);
imports.push(`import { ${capitalized}Dto } from "../dtos/${capitalized}Dto";`);
let template = `
export default function ${capitalized}Form({
item,
actionData,
isUpdating,
isCreating,
canUpdate,
canDelete,
onCancel,
}: {
item?: ${capitalized}Dto;
actionData: { success?: string; error?: string } | undefined;
isUpdating?: boolean;
isCreating?: boolean;
canUpdate?: boolean;
canDelete?: boolean;
onCancel?: () => void;
}) {
const { t } = useTranslation();
const navigation = useNavigation();
const submit = useSubmit();
const rootData = useRootData();
const confirmDelete = useRef<RefConfirmModal>(null);
function onDelete() {
confirmDelete.current?.show(t("shared.confirmDelete"), t("shared.delete"), t("shared.cancel"), t("shared.warningCannotUndo"));
}
function onDeleteConfirmed() {
const form = new FormData();
form.set("action", "delete");
if (rootData.csrf) {
form.set("csrf", rootData.csrf);
}
submit(form, {
method: "post",
});
}
function isDisabled() {
if (isUpdating && !canUpdate) {
return true;
}
if (!isUpdating && !isCreating) {
return true;
}
}
return (
<Form key={!isDisabled() ? "enabled" : "disabled"} method="post" className="space-y-4">
{item ? <input name="action" value="edit" hidden readOnly /> : <input name="action" value="create" hidden readOnly />}
{rootData.csrf && <input type="hidden" name="csrf" value={rootData.csrf} />}
<InputGroup title={t("shared.details")}>
<div className="space-y-2">
{PROPERTIES_UI_CONTROLS}
</div>
</InputGroup>
{(isCreating || (isUpdating && canUpdate)) && (
<div className="flex justify-between space-x-2">
<div>
{canDelete && (
<ButtonSecondary disabled={navigation.state !== "idle"} destructive onClick={onDelete}>
{t("shared.delete")}
</ButtonSecondary>
)}
</div>
<div className="flex space-x-2">
{onCancel && <ButtonSecondary onClick={onCancel}>{t("shared.cancel")}</ButtonSecondary>}
<ButtonPrimary disabled={navigation.state !== "idle"} type="submit">
{item ? t("shared.save") : t("shared.create")}
</ButtonPrimary>
</div>
</div>
)}
<ConfirmModal ref={confirmDelete} onYes={onDeleteConfirmed} />
<ActionResultModal actionData={actionData} showSuccess={false} />
</Form>
);
}`;
const uiFormControls: string[] = [];
entity.properties
.filter((f) => !f.isDefault)
.forEach((property, index) => {
CodeGeneratorPropertiesHelper.uiForm({ code: uiFormControls, imports, property, index });
});
template = template.replace("{PROPERTIES_UI_CONTROLS}", uiFormControls.join("\n "));
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, template].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,25 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import CodeGeneratorPropertiesHelper from "../../utils/CodeGeneratorPropertiesHelper";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
const imports: string[] = [];
const code: string[] = [];
code.push("");
code.push(`export type ${capitalized}CreateDto = {`);
entity.properties
.filter((f) => !f.isDefault && f.showInCreate && CodeGeneratorPropertiesHelper.isWritableProperty(f))
.forEach((property) => {
CodeGeneratorPropertiesHelper.type({ code, imports, property });
});
code.push("};");
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, ...code].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,29 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import CodeGeneratorPropertiesHelper from "../../utils/CodeGeneratorPropertiesHelper";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
const imports: string[] = [];
const code: string[] = [];
imports.push(`import { RowWithDetails } from "~/utils/db/entities/rows.db.server";`);
code.push("");
code.push(`export type ${capitalized}Dto = {`);
code.push(` row: RowWithDetails;`);
code.push(` prefix: string;`);
code.push(` // Custom Row Properties:`);
entity.properties
.filter((f) => !f.isDefault)
.forEach((property) => {
CodeGeneratorPropertiesHelper.type({ code, imports, property });
});
code.push("};");
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, ...code].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,54 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import CodeGeneratorPropertiesHelper from "../../utils/CodeGeneratorPropertiesHelper";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
const imports: string[] = [];
imports.push(`import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import { RowWithDetails } from "~/utils/db/entities/rows.db.server";
import FormHelper from "~/utils/helpers/FormHelper";
import RowValueHelper from "~/utils/helpers/RowValueHelper";`);
imports.push(`import { ${capitalized}Dto } from "../dtos/${capitalized}Dto";`);
// if (entity.properties.some((p) => p.type === PropertyType.MEDIA)) {
// imports.push(`import { MediaDto } from "~/application/dtos/entities/MediaDto";`);
// }
let template = `
function rowToDto({ entity, row }: { entity: EntityWithDetails; row: RowWithDetails }): ${capitalized}Dto {
return {
row,
prefix: entity.prefix,
{PROPERTIES_ROW_TO_DTO}
};
}
function formToDto(form: FormData): Partial<${capitalized}Dto> {
return {
{PROPERTIES_FORM_TO_DTO}
};
}
export default {
rowToDto,
formToDto,
};`;
const rowToDto: string[] = [];
const formToDto: string[] = [];
entity.properties
.filter((f) => !f.isDefault)
.forEach((property, index) => {
CodeGeneratorPropertiesHelper.rowToDto({ code: rowToDto, property });
CodeGeneratorPropertiesHelper.formToDto({ code: formToDto, property });
});
template = template.replace("{PROPERTIES_ROW_TO_DTO}", rowToDto.join("\n "));
template = template.replace("{PROPERTIES_FORM_TO_DTO}", formToDto.join("\n "));
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, template].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,126 @@
/* eslint-disable no-template-curly-in-string */
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized, name } = CodeGeneratorHelper.getNames(entity);
let template = `import { LoaderFunctionArgs, ActionFunction } from "react-router";
import { RowPermissionsDto } from "~/application/dtos/entities/RowPermissionsDto";
import { getTranslations } from "~/locale/i18next.server";
import NotificationService from "~/modules/notifications/services/.server/NotificationService";
import { RowCommentsApi } from "~/utils/api/.server/RowCommentsApi";
import { RowsApi } from "~/utils/api/.server/RowsApi";
import { getEntityByName } from "~/utils/db/entities/entities.db.server";
import { setRowCommentReaction } from "~/utils/db/entities/rowCommentReaction.db.server";
import { getRowCommentInRow, updateRowCommentInRow } from "~/utils/db/entities/rowComments.db.server";
import { LogWithDetails, getRowLogsById } from "~/utils/db/logs.db.server";
import { getUser } from "~/utils/db/users.db.server";
import RowHelper from "~/utils/helpers/RowHelper";
import { getTenantIdOrNull } from "~/utils/services/.server/urlService";
import { requireAuth, validateCSRFToken } from "~/utils/session.server";
import { MetaTagsDto } from "~/application/dtos/seo/MetaTagsDto";
export namespace ${capitalized}RoutesActivityApi {
export type LoaderData = {
metatags: MetaTagsDto;
logs: LogWithDetails[];
permissions: RowPermissionsDto;
};
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const tenantId = await getTenantIdOrNull({ request, params });
const { userId } = await requireAuth({ request, params });
const { t } = await getTranslations(request);
const entity = await getEntityByName({ tenantId, name: "${name}" });
const rowData = await RowsApi.get(params.id!, { entity, tenantId, userId });
const data: LoaderData = {
metatags: [{ title: t("shared.activity") + " | " + process.env.APP_NAME }],
logs: await getRowLogsById(rowData.item.id),
permissions: rowData.rowPermissions,
};
return data;
};
export type ActionData = {
success?: string;
error?: string;
};
export const action: ActionFunction = async ({ request, params }) => {
const { t } = await getTranslations(request);
const { userId } = await requireAuth({ request, params });
const tenantId = await getTenantIdOrNull({ request, params });
await validateCSRFToken(request);
const form = await request.formData();
const action = form.get("action")?.toString() ?? "";
const user = await getUser(userId);
const entity = await getEntityByName({ tenantId, name: "${name}" });
const rowData = await RowsApi.get(params.id!, { entity, tenantId, userId });
if (action === "comment") {
let comment = form.get("comment")?.toString();
if (!comment) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
if (!rowData.rowPermissions.canComment) {
return Response.json({ error: t("models.row.activity.errors.commentForbidden") }, { status: 403 });
}
await RowCommentsApi.create(params.id!, {
comment,
tenantId,
userId,
});
const item = rowData.item;
if (item!.createdByUser) {
await NotificationService.send({
channel: "my-rows",
to: item!.createdByUser,
tenantId,
notification: {
from: { user },
message: t("notifications.messages.row.commented", { email: user?.email, row: RowHelper.getRowFolio(entity, item!) }),
// action: {
// title: t("shared.view"),
// url: "",
// },
},
});
}
return Response.json({ newComment: comment });
} else if (action === "comment-reaction") {
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 });
}
const rowComment = await getRowCommentInRow({ id: rowCommentId, rowId: rowData.item.id });
if (!rowComment || !rowData.rowPermissions.canComment) {
return Response.json({ error: t("models.row.activity.errors.updateCommentForbidden") }, { status: 403 });
}
await setRowCommentReaction({
createdByUserId: userId,
rowCommentId,
reaction,
});
return Response.json({ newCommentReaction: reaction });
} else if (action === "comment-delete") {
const rowCommentId = form.get("comment-id")?.toString();
if (!rowCommentId) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
const rowComment = await getRowCommentInRow({ id: rowCommentId, rowId: rowData.item.id });
if (!rowComment || (!rowData.rowPermissions.canDelete && rowComment.createdByUserId !== userId)) {
return Response.json({ error: t("models.row.activity.errors.deleteCommentForbidden") }, { status: 403 });
}
await updateRowCommentInRow({ id: rowCommentId, rowId: rowData.item.id }, { isDeleted: true });
return Response.json({ deletedComment: rowCommentId });
} else {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
};
}
`;
return template;
}
export default {
generate,
};
@@ -0,0 +1,225 @@
/* eslint-disable no-template-curly-in-string */
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import CodeGeneratorPropertiesHelper from "~/modules/codeGenerator/utils/CodeGeneratorPropertiesHelper";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized, name } = CodeGeneratorHelper.getNames(entity);
const imports: string[] = [
`import { LoaderFunctionArgs, ActionFunction, redirect } from "react-router";
import { RowPermissionsDto } from "~/application/dtos/entities/RowPermissionsDto";
import { getTranslations } from "~/locale/i18next.server";
import NotificationService from "~/modules/notifications/services/.server/NotificationService";
import UrlUtils from "~/utils/app/UrlUtils";
import { getEntityByName } from "~/utils/db/entities/entities.db.server";
import { getUser } from "~/utils/db/users.db.server";
import { getUserRowPermission } from "~/utils/helpers/.server/PermissionsService";
import RowHelper from "~/utils/helpers/RowHelper";
import { requireAuth } from "~/utils/loaders.middleware";
import { getTenantIdOrNull } from "~/utils/services/.server/urlService";
import { validateCSRFToken } from "~/utils/session.server";
import { MetaTagsDto } from "~/application/dtos/seo/MetaTagsDto";`,
];
if (entity.hasTasks) {
imports.push(
`import { RowTaskWithDetails, getRowTasks, createRowTask, getRowTaskInRow, updateRowTaskInRow, deleteRowTaskInRow } from "~/utils/db/entities/rowTasks.db.server";`
);
}
imports.push(`import { ${capitalized}Dto } from "../../dtos/${capitalized}Dto";
import ${capitalized}Helpers from "../../helpers/${capitalized}Helpers";
import { ${capitalized}Service } from "../../services/${capitalized}Service";`);
let template = `
export namespace ${capitalized}RoutesEditApi {
export type LoaderData = {
metatags: MetaTagsDto;
item: ${capitalized}Dto;
permissions: RowPermissionsDto;
{TASKS_LOADER_INTERFACE}
};
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { t } = await getTranslations(request);
const tenantId = await getTenantIdOrNull({ request, params });
const { userId } = await requireAuth({ request, params });
const item = await ${capitalized}Service.get(params.id!, {
tenantId,
userId,
});
if (!item) {
return Response.json({ error: t("shared.notFound") }, { status: 404 });
}
const permissions = await getUserRowPermission(item.row, tenantId, userId);
if (!permissions.canRead) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
const data: LoaderData = {
metatags: [{ title: item.prefix + "-" + String(item.row.folio ?? 0).padStart(4, "0") + " | " + process.env.APP_NAME }],
item,
permissions,
{TASKS_LOADER_DATA}
};
return data;
};
export type ActionData = {
success?: string;
error?: string;
};
export const action: ActionFunction = async ({ request, params }) => {
const { t } = await getTranslations(request);
const tenantId = await getTenantIdOrNull({ request, params });
const { userId } = await requireAuth({ request, params });
await validateCSRFToken(request);
const form = await request.formData();
const action = form.get("action")?.toString() ?? "";
const user = await getUser(userId);
const entity = await getEntityByName({ tenantId, name: "${name}" });
const item = await ${capitalized}Service.get(params.id!, {
tenantId,
userId,
});
if (!item) {
return Response.json({ error: t("shared.notFound") }, { status: 404 });
}
if (action === "edit") {
try {
const { {PROPERTIES_UPDATE_NAMES} } = ${capitalized}Helpers.formToDto(form);
await ${capitalized}Service.update(
params.id!,
{ {PROPERTIES_UPDATE_NAMES} },
{ tenantId, userId }
);
if (item.row.createdByUser) {
await NotificationService.send({
channel: "my-rows",
to: item.row.createdByUser,
tenantId,
notification: {
from: { user },
message: ${"`${user?.email} updated ${RowHelper.getRowFolio(entity, item.row)}`"},
// action: {
// title: t("shared.view"),
// url: "",
// },
},
});
}
return Response.json({ success: t("shared.updated") });
} catch (error: any) {
return Response.json({ error: error.message }, { status: 400 });
}
} else if (action === "delete") {
try {
await ${capitalized}Service.del(params.id!, {
tenantId,
userId,
});
if (item.row.createdByUser) {
await NotificationService.send({
channel: "my-rows",
to: item.row.createdByUser,
tenantId,
notification: {
from: { user },
message: ${"`${user?.email} deleted ${RowHelper.getRowFolio(entity, item.row)}`"},
},
});
}
return redirect(UrlUtils.getParentRoute(new URL(request.url).pathname));
} catch (error: any) {
return Response.json({ error: error.message }, { status: 400 });
}
} {TASKS_ACTIONS}
else {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
};
}`;
const propertiesUpdateNames: string[] = [];
entity.properties
.filter((f) => f.showInCreate && !f.isDefault && CodeGeneratorPropertiesHelper.isWritableProperty(f))
.forEach((property) => {
propertiesUpdateNames.push(property.name);
});
template = template.split("{PROPERTIES_UPDATE_NAMES}").join(propertiesUpdateNames.join(", "));
const tasksLoaderInterface: string[] = [];
const tasksLoaderData: string[] = [];
if (entity.hasTasks) {
tasksLoaderInterface.push("tasks: RowTaskWithDetails[]");
tasksLoaderData.push(`tasks: await getRowTasks(params.id!),`);
}
template = template.split("{TASKS_LOADER_INTERFACE}").join(tasksLoaderInterface.join("\n "));
template = template.split("{TASKS_LOADER_DATA}").join(tasksLoaderData.join("\n "));
let tasksActions = "";
if (entity.hasTasks) {
tasksActions = `else if (action === "task-new") {
const rowPermissions = await getUserRowPermission(item.row, tenantId, userId);
if (!rowPermissions.canUpdate) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
const taskTitle = form.get("task-title")?.toString();
if (!taskTitle) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
const task = await createRowTask({
createdByUserId: userId,
rowId: item.row.id,
title: taskTitle,
});
return Response.json({ newTask: task });
} else if (action === "task-complete-toggle") {
const rowPermissions = await getUserRowPermission(item.row, tenantId, userId);
if (!rowPermissions.canUpdate) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
const taskId = form.get("task-id")?.toString() ?? "";
const task = await getRowTaskInRow({ id: taskId, rowId: item.row.id });
if (!task) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
if (task) {
if (task.completed) {
await updateRowTaskInRow({ id: taskId, rowId: item.row.id }, {
completed: false,
completedAt: null,
completedByUserId: null,
});
} else {
await updateRowTaskInRow({ id: taskId, rowId: item.row.id }, {
completed: true,
completedAt: new Date(),
completedByUserId: userId,
});
}
}
return Response.json({ completedTask: taskId });
} else if (action === "task-delete") {
const rowPermissions = await getUserRowPermission(item.row, tenantId, userId);
if (!rowPermissions.canUpdate) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
const taskId = form.get("task-id")?.toString() ?? "";
const task = await getRowTaskInRow({ id: taskId, rowId: item.row.id });
if (!task) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
if (task) {
await deleteRowTaskInRow({ id: taskId, rowId: item.row.id });
}
return Response.json({ deletedTask: taskId });
}`;
}
template = template.split("{TASKS_ACTIONS}").join(tasksActions);
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, template].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,62 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized, name } = CodeGeneratorHelper.getNames(entity);
const titleExpression = CodeGeneratorHelper.getEntityTitleExpression(entity);
const imports: string[] = [];
imports.push(`import { LoaderFunctionArgs } from "react-router";
import { FilterablePropertyDto } from "~/application/dtos/data/FilterablePropertyDto";
import { PaginationDto } from "~/application/dtos/data/PaginationDto";
import { getTranslations } from "~/locale/i18next.server";
import { getEntityByName } from "~/utils/db/entities/entities.db.server";
import EntityHelper from "~/utils/helpers/EntityHelper";
import { getTenantIdOrNull } from "~/utils/services/.server/urlService";
import { requireAuth } from "~/utils/session.server";
import { MetaTagsDto } from "~/application/dtos/seo/MetaTagsDto";`);
imports.push(`import { ${capitalized}Dto } from "../../dtos/${capitalized}Dto";
import { ${capitalized}Service } from "../../services/${capitalized}Service";`);
let template = `
export namespace ${capitalized}RoutesIndexApi {
export type LoaderData = {
metatags: MetaTagsDto;
items: ${capitalized}Dto[];
pagination: PaginationDto;
filterableProperties?: FilterablePropertyDto[];
overviewItem?: ${capitalized}Dto | null;
};
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { t } = await getTranslations(request);
const tenantId = await getTenantIdOrNull({ request, params });
const { userId } = await requireAuth({ request, params });
const urlSearchParams = new URL(request.url).searchParams;
const { items, pagination } = await ${capitalized}Service.getAll({
tenantId,
userId,
urlSearchParams,
});
const data: LoaderData = {
metatags: [{ title: ${titleExpression} + " | " + process.env.APP_NAME }],
items,
pagination,
filterableProperties: EntityHelper.getFilters({ t, entity: await getEntityByName({ tenantId, name: "${name}" }) }),
};
const overviewId = urlSearchParams.get("overview") ?? "";
if (overviewId) {
data.overviewItem = await ${capitalized}Service.get(overviewId, {
tenantId,
userId,
});
}
return data;
};
}`;
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, template].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,88 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import CodeGeneratorPropertiesHelper from "~/modules/codeGenerator/utils/CodeGeneratorPropertiesHelper";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized, name } = CodeGeneratorHelper.getNames(entity);
const titleExpression = CodeGeneratorHelper.getEntityTitleExpression(entity);
const imports: string[] = [];
imports.push(`import { LoaderFunctionArgs, ActionFunction, redirect } from "react-router";
import { PlanFeatureUsageDto } from "~/application/dtos/subscriptions/PlanFeatureUsageDto";
import { getTranslations } from "~/locale/i18next.server";
import UrlUtils from "~/utils/app/UrlUtils";
import { requireAuth } from "~/utils/loaders.middleware";
import { getPlanFeatureUsage } from "~/utils/services/.server/subscriptionService";
import { getTenantIdOrNull } from "~/utils/services/.server/urlService";
import { validateCSRFToken } from "~/utils/session.server";
import { MetaTagsDto } from "~/application/dtos/seo/MetaTagsDto";`);
imports.push(`import ${capitalized}Helpers from "../../helpers/${capitalized}Helpers";
import { ${capitalized}Service } from "../../services/${capitalized}Service";`);
let template = `
export namespace ${capitalized}RoutesNewApi {
export type LoaderData = {
metatags: MetaTagsDto;
featureUsage: PlanFeatureUsageDto | undefined;
};
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { t } = await getTranslations(request);
await requireAuth({ request, params });
const tenantId = await getTenantIdOrNull({ request, params });
const data: LoaderData = {
metatags: [{ title: t("shared.create") + " " + ${titleExpression} + " | " + process.env.APP_NAME }],
featureUsage: tenantId ? await getPlanFeatureUsage(tenantId, "${name}") : undefined,
};
return data;
};
export type ActionData = {
success?: string;
error?: string;
};
export const action: ActionFunction = async ({ request, params }) => {
const { t } = await getTranslations(request);
const { userId } = await requireAuth({ request, params });
const tenantId = await getTenantIdOrNull({ request, params });
await validateCSRFToken(request);
const form = await request.formData();
const action = form.get("action")?.toString();
if (action === "create") {
try {
const { {PROPERTIES_CREATE_NAMES} } = ${capitalized}Helpers.formToDto(form);
{PROPERTIES_CREATE_VALIDATION_REQUIRED}
const item = await ${capitalized}Service.create(
{ {PROPERTIES_CREATE_NAMES} },
{ userId, tenantId }
);
return redirect(UrlUtils.getParentRoute(new URL(request.url).pathname) + "/" + item.row.id);
} catch (error: any) {
return Response.json({ error: error.message }, { status: 400 });
}
} else {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
};
}`;
const propertiesCreateNames: string[] = [];
const propertiesCreateValidationRequired: string[] = [];
entity.properties
.filter((f) => !f.isDefault && f.showInCreate && CodeGeneratorPropertiesHelper.isWritableProperty(f))
.forEach((property) => {
propertiesCreateNames.push(property.name);
if (property.isRequired) {
propertiesCreateValidationRequired.push(
`if (${property.name} === undefined) throw new Error(${CodeGeneratorHelper.getPropertyTitleExpression(property)} + " is required");`
);
}
});
template = template.split("{PROPERTIES_CREATE_NAMES}").join(propertiesCreateNames.join(", "));
template = template.split("{PROPERTIES_CREATE_VALIDATION_REQUIRED}").join(propertiesCreateValidationRequired.join("\n "));
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, template].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,123 @@
/* eslint-disable no-template-curly-in-string */
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
let template = `import { Tenant } from "@toolctrl/db/prisma-browser";
import { LoaderFunctionArgs, ActionFunction } from "react-router";
import { RowAccess } from "~/application/enums/entities/RowAccess";
import { getTranslations } from "~/locale/i18next.server";
import { RowPermissionsApi } from "~/utils/api/.server/RowPermissionsApi";
import { getUserRowPermission } from "~/utils/helpers/.server/PermissionsService";
import { getTenant, adminGetAllTenants } from "~/utils/db/tenants.db.server";
import { UserWithDetails, getUsersByTenant } from "~/utils/db/users.db.server";
import { requireAuth } from "~/utils/loaders.middleware";
import { getTenantIdOrNull } from "~/utils/services/.server/urlService";
import { validateCSRFToken } from "~/utils/session.server";
import { MetaTagsDto } from "~/application/dtos/seo/MetaTagsDto";
import { ${capitalized}Dto } from "../../dtos/${capitalized}Dto";
import { ${capitalized}Service } from "../../services/${capitalized}Service";
export namespace ${capitalized}RoutesShareApi {
export type LoaderData = {
metatags: MetaTagsDto;
item: ${capitalized}Dto;
tenants: Tenant[];
users: UserWithDetails[];
};
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { t } = await getTranslations(request);
const tenantId = await getTenantIdOrNull({ request, params });
const { userId } = await requireAuth({ request, params });
const item = await ${capitalized}Service.get(params.id!, {
tenantId,
userId,
});
if (!item) {
return Response.json({ error: t("shared.notFound") }, { status: 404 });
}
const rowPermissions = await getUserRowPermission(item.row, tenantId, userId);
if (!rowPermissions.canUpdate) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
let tenants: Tenant[] = [];
if (tenantId) {
tenants.push((await getTenant(tenantId))!);
} else {
tenants = await adminGetAllTenants();
}
const data: LoaderData = {
metatags: [{ title: t("shared.share") + " | " + process.env.APP_NAME }],
item,
tenants,
users: (await getUsersByTenant(tenantId)).filter((f) => f.id !== userId),
};
return data;
};
export type ActionData = {
success?: string;
error?: string;
};
export const action: ActionFunction = async ({ request, params }) => {
const { t } = await getTranslations(request);
const { userId } = await requireAuth({ request, params });
await validateCSRFToken(request);
const form = await request.formData();
const action = form.get("action")?.toString() ?? "";
const tenantId = await getTenantIdOrNull({ request, params });
const item = await ${capitalized}Service.get(params.id!, {
tenantId,
userId,
});
if (!item) {
return Response.json({ error: t("shared.notFound") }, { status: 404 });
}
const rowPermissions = await getUserRowPermission(item.row, tenantId, userId);
if (!rowPermissions.canUpdate) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
if (action === "share") {
const allowedShareTypes = ["tenant", "user", "role", "group"] as const;
type ShareType = (typeof allowedShareTypes)[number];
const maybeType = form.get("type")?.toString();
const type = allowedShareTypes.includes(maybeType as ShareType) ? (maybeType as ShareType) : null;
if (!type) {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
const id = form.get("id")?.toString() ?? "";
const access = form.get("access")?.toString() as RowAccess;
try {
await RowPermissionsApi.share(item.row, {
type,
id,
access,
}, { currentTenantId: tenantId });
return Response.json({ success: t("shared.saved") });
} catch (error: any) {
return Response.json({ error: error.message }, { status: 400 });
}
} else if (action === "set-access") {
const id = form.get("id")?.toString() ?? "";
const access = form.get("access")?.toString() as RowAccess;
await RowPermissionsApi.setAccessForRow(item.row.id, id, access);
return Response.json({ success: t("shared.saved") });
} else if (action === "remove") {
const id = form.get("id")?.toString() ?? "";
await RowPermissionsApi.delForRow(item.row.id, id);
return Response.json({ success: t("shared.deleted") });
} else {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
};
}
`;
return template;
}
export default {
generate,
};
@@ -0,0 +1,145 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized, name } = CodeGeneratorHelper.getNames(entity);
return `import { EntityTag } from "@toolctrl/db/prisma-browser";
import { LoaderFunctionArgs, ActionFunction } from "react-router";
import { Colors } from "~/application/enums/shared/Colors";
import { getTranslations } from "~/locale/i18next.server";
import { getEntityByName } from "~/utils/db/entities/entities.db.server";
import { getEntityTags, getEntityTag, createEntityTag, updateEntityTag, deleteEntityTag, getEntityTagById } from "~/utils/db/entities/entityTags.db.server";
import { getRowTag, createRowTag, deleteRowTags } from "~/utils/db/entities/rowTags.db.server";
import { getUserRowPermission } from "~/utils/helpers/.server/PermissionsService";
import { requireAuth } from "~/utils/loaders.middleware";
import { getTenantIdOrNull } from "~/utils/services/.server/urlService";
import { validateCSRFToken } from "~/utils/session.server";
import { MetaTagsDto } from "~/application/dtos/seo/MetaTagsDto";
import { ${capitalized}Dto } from "../../dtos/${capitalized}Dto";
import { ${capitalized}Service } from "../../services/${capitalized}Service";
export namespace ${capitalized}RoutesTagsApi {
export type LoaderData = {
metatags: MetaTagsDto;
item: ${capitalized}Dto;
tags: EntityTag[];
};
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { t } = await getTranslations(request);
const tenantId = await getTenantIdOrNull({ request, params });
const { userId } = await requireAuth({ request, params });
const entity = await getEntityByName({ tenantId, name: "${name}" });
const item = await ${capitalized}Service.get(params.id!, {
tenantId,
userId,
});
if (!item) {
return Response.json({ error: t("shared.notFound") }, { status: 404 });
}
const data: LoaderData = {
metatags: [{ title: t("models.row.tags") + " | " + process.env.APP_NAME }],
item,
tags: await getEntityTags(entity.id),
};
return data;
};
export type ActionData = {
success?: string;
error?: string;
};
export const action: ActionFunction = async ({ request, params }) => {
const { t } = await getTranslations(request);
const tenantId = await getTenantIdOrNull({ request, params });
const { userId } = await requireAuth({ request, params });
await validateCSRFToken(request);
const form = await request.formData();
const action = form.get("action")?.toString() ?? "";
const entity = await getEntityByName({ tenantId, name: "${name}" });
const item = await ${capitalized}Service.get(params.id!, {
tenantId: await getTenantIdOrNull({ request, params }),
userId,
});
if (!item) {
return Response.json({ error: t("shared.notFound") }, { status: 404 });
}
const rowPermissions = await getUserRowPermission(item.row, tenantId, userId);
if (!rowPermissions.canUpdate) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
const canMutateTagCatalog = !tenantId;
if (action === "new-tag") {
const value = form.get("tag-name")?.toString() ?? "";
const color = Number(form.get("tag-color") ?? Colors.INDIGO);
let tag = await getEntityTag(entity.id, value);
if (!tag) {
if (!canMutateTagCatalog) {
return Response.json({ error: t("shared.notFound") }, { status: 400 });
}
tag = await createEntityTag({
entityId: entity.id,
value,
color,
});
}
const existingTag = await getRowTag(params.id!, value);
if (tag && !existingTag) {
await createRowTag({
rowId: params.id!,
tagId: tag.id,
});
}
return Response.json({});
} else if (action === "edit-tag") {
if (!canMutateTagCatalog) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
const id = form.get("tag-id")?.toString() ?? "";
const value = form.get("tag-name")?.toString() ?? "";
const color = Number(form.get("tag-color"));
const tag = await getEntityTagById(id, entity.id);
if (!tag) {
return Response.json({ error: t("shared.notFound") }, { status: 400 });
}
await updateEntityTag({ id: tag.id, entityId: entity.id }, {
value,
color,
});
return Response.json({});
} else if (action === "set-tag") {
const id = form.get("tag-id")?.toString() ?? "";
const tagAction = form.get("tag-action")?.toString() ?? "";
const tag = await getEntityTagById(id, entity.id);
if (!tag) {
return Response.json({ error: t("shared.notFound") }, { status: 400 });
}
if (tagAction === "add") {
await createRowTag({
rowId: params.id!,
tagId: tag.id,
});
} else {
await deleteRowTags(params.id!, tag.id);
}
return Response.json({});
} else if (action === "delete-tag") {
if (!canMutateTagCatalog) {
return Response.json({ error: t("shared.unauthorized") }, { status: 403 });
}
const id = form.get("tag-id")?.toString() ?? "";
const tag = await getEntityTagById(id, entity.id);
if (tag) {
await deleteEntityTag({ id: tag.id, entityId: entity.id });
}
return Response.json({});
} else {
return Response.json({ error: t("shared.invalidForm") }, { status: 400 });
}
};
}
`;
}
export default {
generate,
};
@@ -0,0 +1,25 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity, moduleLocation }: { entity: EntityWithDetails; moduleLocation: string }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import { MetaFunction, LoaderFunctionArgs, ActionFunction } from "react-router";
import ServerError from "~/components/ui/errors/ServerError";
import { ${capitalized}RoutesActivityApi } from "~/${moduleLocation}/routes/api/${capitalized}Routes.Activity.Api";
import ${capitalized}RoutesActivityView from "~/${moduleLocation}/routes/views/${capitalized}Routes.Activity.View";
export const meta: MetaFunction<typeof loader> = ({ data }) => (data && "metatags" in data ? data.metatags : []);
export const loader = (args: LoaderFunctionArgs) => ${capitalized}RoutesActivityApi.loader(args);
export const action: ActionFunction = (args) => ${capitalized}RoutesActivityApi.action(args);
export default () => <${capitalized}RoutesActivityView />;
export function ErrorBoundary() {
return <ServerError />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,25 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity, moduleLocation }: { entity: EntityWithDetails; moduleLocation: string }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import { MetaFunction, LoaderFunctionArgs, ActionFunction } from "react-router";
import ServerError from "~/components/ui/errors/ServerError";
import { ${capitalized}RoutesEditApi } from "~/${moduleLocation}/routes/api/${capitalized}Routes.Edit.Api";
import ${capitalized}RoutesEditView from "~/${moduleLocation}/routes/views/${capitalized}Routes.Edit.View";
export const meta: MetaFunction<typeof loader> = ({ data }) => (data && "metatags" in data ? data.metatags : []);
export const loader = (args: LoaderFunctionArgs) => ${capitalized}RoutesEditApi.loader(args);
export const action: ActionFunction = (args) => ${capitalized}RoutesEditApi.action(args);
export default () => <${capitalized}RoutesEditView />;
export function ErrorBoundary() {
return <ServerError />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,24 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity, moduleLocation }: { entity: EntityWithDetails; moduleLocation: string }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import { MetaFunction, LoaderFunctionArgs } from "react-router";
import ServerError from "~/components/ui/errors/ServerError";
import { ${capitalized}RoutesIndexApi } from "~/${moduleLocation}/routes/api/${capitalized}Routes.Index.Api";
import ${capitalized}RoutesIndexView from "~/${moduleLocation}/routes/views/${capitalized}Routes.Index.View";
export const meta: MetaFunction<typeof loader> = ({ data }) => (data && "metatags" in data ? data.metatags : []);
export const loader = (args: LoaderFunctionArgs) => ${capitalized}RoutesIndexApi.loader(args);
export default () => <${capitalized}RoutesIndexView />;
export function ErrorBoundary() {
return <ServerError />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,25 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity, moduleLocation }: { entity: EntityWithDetails; moduleLocation: string }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import { MetaFunction, LoaderFunctionArgs, ActionFunction } from "react-router";
import ServerError from "~/components/ui/errors/ServerError";
import { ${capitalized}RoutesNewApi } from "~/${moduleLocation}/routes/api/${capitalized}Routes.New.Api";
import ${capitalized}RoutesNewView from "~/${moduleLocation}/routes/views/${capitalized}Routes.New.View";
export const meta: MetaFunction<typeof loader> = ({ data }) => (data && "metatags" in data ? data.metatags : []);
export const loader = (args: LoaderFunctionArgs) => ${capitalized}RoutesNewApi.loader(args);
export const action: ActionFunction = (args) => ${capitalized}RoutesNewApi.action(args);
export default () => <${capitalized}RoutesNewView />;
export function ErrorBoundary() {
return <ServerError />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,25 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity, moduleLocation }: { entity: EntityWithDetails; moduleLocation: string }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import { MetaFunction, LoaderFunctionArgs, ActionFunction } from "react-router";
import ServerError from "~/components/ui/errors/ServerError";
import { ${capitalized}RoutesShareApi } from "~/${moduleLocation}/routes/api/${capitalized}Routes.Share.Api";
import ${capitalized}RoutesShareView from "~/${moduleLocation}/routes/views/${capitalized}Routes.Share.View";
export const meta: MetaFunction<typeof loader> = ({ data }) => (data && "metatags" in data ? data.metatags : []);
export const loader = (args: LoaderFunctionArgs) => ${capitalized}RoutesShareApi.loader(args);
export const action: ActionFunction = (args) => ${capitalized}RoutesShareApi.action(args);
export default () => <${capitalized}RoutesShareView />;
export function ErrorBoundary() {
return <ServerError />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,25 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity, moduleLocation }: { entity: EntityWithDetails; moduleLocation: string }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import { MetaFunction, LoaderFunctionArgs, ActionFunction } from "react-router";
import ServerError from "~/components/ui/errors/ServerError";
import { ${capitalized}RoutesTagsApi } from "~/${moduleLocation}/routes/api/${capitalized}Routes.Tags.Api";
import ${capitalized}RoutesTagsView from "~/${moduleLocation}/routes/views/${capitalized}Routes.Tags.View";
export const meta: MetaFunction<typeof loader> = ({ data }) => (data && "metatags" in data ? data.metatags : []);
export const loader = (args: LoaderFunctionArgs) => ${capitalized}RoutesTagsApi.loader(args);
export const action: ActionFunction = (args) => ${capitalized}RoutesTagsApi.action(args);
export default () => <${capitalized}RoutesTagsView />;
export function ErrorBoundary() {
return <ServerError />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,19 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import RowActivity from "~/components/entities/rows/RowActivity";
import { ${capitalized}RoutesActivityApi } from "../api/${capitalized}Routes.Activity.Api";
import { useLoaderData } from "react-router";
export default function ${capitalized}RoutesActivityView() {
const data = useLoaderData<${capitalized}RoutesActivityApi.LoaderData>();
return <RowActivity items={data.logs} withTitle={false} hasActivity={true} hasComments={data.permissions.canComment} />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,149 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
const titleExpression = CodeGeneratorHelper.getEntityTitleExpression(entity);
const pluralExpression = CodeGeneratorHelper.getEntityPluralExpression(entity);
const imports: string[] = [
`import { useActionData, useLoaderData, useOutlet, useNavigate, useLocation, useSearchParams, useSubmit } from "react-router";
import { useTranslation } from "react-i18next";
import RowSettingsTabs from "~/components/entities/rows/RowSettingsTabs";
import ButtonSecondary from "~/components/ui/buttons/ButtonSecondary";
import EditPageLayout from "~/components/ui/layouts/EditPageLayout";
import SlideOverWideEmpty from "~/components/ui/slideOvers/SlideOverWideEmpty";
import UrlUtils from "~/utils/app/UrlUtils";
import { useAppOrAdminData } from "~/utils/data/useAppOrAdminData";
import ShareIcon from "~/components/ui/icons/ShareIcon";
import ClockIcon from "~/components/ui/icons/ClockIcon";
import PencilIcon from "~/components/ui/icons/PencilEmptyIcon";`,
];
if (entity.hasTasks) {
imports.push(`import RowTasks from "~/components/entities/rows/RowTasks";`);
}
if (entity.hasTags) {
imports.push(`import RowTags from "~/components/entities/rows/RowTags";`);
}
imports.push(`import ${capitalized}Form from "../../components/${capitalized}Form";
import { ${capitalized}RoutesEditApi } from "../api/${capitalized}Routes.Edit.Api";`);
let template = `
export default function ${capitalized}RoutesEditView() {
const { t } = useTranslation();
const appOrAdminData = useAppOrAdminData();
const data = useLoaderData<${capitalized}RoutesEditApi.LoaderData>();
const actionData = useActionData<${capitalized}RoutesEditApi.ActionData>();
const outlet = useOutlet();
const navigate = useNavigate();
const location = useLocation();
const submit = useSubmit();
const [searchParams, setSearchParams] = useSearchParams();
function canUpdate() {
return data.permissions.canUpdate;
}
function canDelete() {
return data.permissions.canDelete;
}
return (
<EditPageLayout
title={t("shared.edit") + " " + ${titleExpression}}
menu={[
{
title: ${pluralExpression},
routePath: UrlUtils.getParentRoute(location?.pathname),
},
{
title: t("shared.edit"),
routePath: "",
},
]}
>
<div className="relative items-center justify-between space-y-2 border-b border-border pb-4 sm:flex sm:space-y-0">
<div className="flex items-center space-x-2">
<div className="text-xl font-bold uppercase">
{data.item.prefix}-{String(data.item.row.folio ?? 0).padStart(4, "0")}
</div>
</div>
<div className="flex space-x-2">
<ButtonSecondary to="activity">
<ClockIcon className="h-4 w-4 text-muted-foreground" />
</ButtonSecondary>
{data.permissions.isOwner && (
<ButtonSecondary to="share">
<ShareIcon className="h-4 w-4 text-muted-foreground" />
</ButtonSecondary>
)}
{canUpdate() && (
<ButtonSecondary
onClick={() => {
if (searchParams.get("view") === "edit") {
setSearchParams({});
} else {
searchParams.set("view", "edit");
setSearchParams(searchParams);
}
}}
>
<PencilIcon className="h-4 w-4 text-muted-foreground" />
</ButtonSecondary>
)}
</div>
</div>
{!data.item ? (
<div>{t("shared.loading")}...</div>
) : (
<div className="mx-auto space-y-2 pt-2 lg:flex lg:space-x-4 lg:space-y-0 pb-10">
<div className="space-y-4 lg:w-4/6">
<${capitalized}Form
item={data.item}
actionData={actionData}
canUpdate={canUpdate()}
canDelete={canDelete()}
isUpdating={searchParams.get("view") === "edit"}
onCancel={() => {
setSearchParams({});
}}
/>
</div>
<div className="space-y-4 lg:w-2/6">
${entity.hasTags ? `<RowTags items={data.item.row.tags} onSetTagsRoute={canUpdate() ? "tags" : undefined} />` : ""}
${entity.hasTasks ? `<RowTasks items={data.tasks} />` : ""}
</div>
</div>
)}
<SlideOverWideEmpty
withTitle={false}
withClose={false}
title={""}
open={!!outlet}
onClose={() => {
navigate(".", { replace: true });
}}
className="sm:max-w-sm"
overflowYScroll={true}
>
<div className="-mx-1 -mt-3">
<div className="space-y-4">
<RowSettingsTabs canUpdate={canUpdate()} isOwner={data.permissions.isOwner} hasTags={${entity.hasTags ? "true" : "false"}} />
{outlet}
</div>
</div>
</SlideOverWideEmpty>
</EditPageLayout>
);
}`;
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, template].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,134 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import CodeGeneratorPropertiesHelper from "~/modules/codeGenerator/utils/CodeGeneratorPropertiesHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
const titleExpression = CodeGeneratorHelper.getEntityTitleExpression(entity);
const pluralExpression = CodeGeneratorHelper.getEntityPluralExpression(entity);
const imports: string[] = [];
imports.push(`import { useActionData, useLoaderData, useSearchParams, Link } from "react-router";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import RowCreatedByCell from "~/components/entities/rows/cells/RowCreatedByCell";
import RowFolioCell from "~/components/entities/rows/cells/RowFolioCell";
import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary";
import DateCell from "~/components/ui/dates/DateCell";
import ExternalLinkEmptyIcon from "~/components/ui/icons/ExternalLinkEmptyIcon";
import InputFilters from "~/components/ui/input/InputFilters";
import IndexPageLayout from "~/components/ui/layouts/IndexPageLayout";
import SlideOverWideEmpty from "~/components/ui/slideOvers/SlideOverWideEmpty";
import TableSimple from "~/components/ui/tables/TableSimple";`);
imports.push(`import ${capitalized}Form from "../../components/${capitalized}Form";
import { ${capitalized}Dto } from "../../dtos/${capitalized}Dto";
import { ${capitalized}RoutesIndexApi } from "../api/${capitalized}Routes.Index.Api";`);
let template = `
export default function ${capitalized}RoutesIndexView() {
const { t } = useTranslation();
const data = useLoaderData<${capitalized}RoutesIndexApi.LoaderData>();
const actionData = useActionData<{ error?: string; success?: string }>();
const [searchParams, setSearchParams] = useSearchParams();
const [overviewItem, setOverviewItem] = useState<${capitalized}Dto>();
useEffect(() => {
setOverviewItem(data.overviewItem ?? undefined);
}, [data.overviewItem]);
return (
<IndexPageLayout
title={${pluralExpression}}
buttons={
<>
{!!data.filterableProperties?.length && <InputFilters filters={data.filterableProperties} />}
<ButtonPrimary to="new">{t("shared.new")}</ButtonPrimary>
</>
}
>
<TableSimple
items={data.items}
pagination={data.pagination}
actions={[
{
title: t("shared.overview"),
onClick: (_, item) => {
searchParams.set("overview", item.row.id);
setSearchParams(searchParams);
},
},
{
title: t("shared.edit"),
onClickRoute: (_, item) => item.row.id,
},
]}
headers={[
{
name: "folio",
title: t("models.row.folio"),
value: (item) => <RowFolioCell prefix={item.prefix} folio={item.row.folio} href={item.row.id} />,
},
{PROPERTIES_UI_CELLS}
{
name: "createdAt",
title: t("shared.createdAt"),
value: (item) => <DateCell date={item.row.createdAt} />,
className: "text-muted-foreground text-xs",
breakpoint: "sm",
},
{
name: "createdByUser",
title: t("shared.createdBy"),
value: (item) => <RowCreatedByCell user={item.row.createdByUser} apiKey={item.row.createdByApiKey} />,
className: "text-muted-foreground text-xs",
breakpoint: "sm",
},
]}
/>
<SlideOverWideEmpty
withTitle={false}
withClose={false}
title={${titleExpression}}
open={!!searchParams.get("overview")?.toString()}
onClose={() => {
searchParams.delete("overview");
setSearchParams(searchParams);
setTimeout(() => {
setOverviewItem(undefined);
}, 100);
}}
className="sm:max-w-md"
buttons={
<>
<Link
to={overviewItem?.row.id ?? ""}
className="rounded-md bg-background text-muted-foreground hover:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
>
<span className="sr-only">{t("shared.openDetails")}</span>
<ExternalLinkEmptyIcon className="h-6 w-6" aria-hidden="true" />
</Link>
</>
}
>
{!overviewItem ? <div>{t("shared.loading")}...</div> : <${capitalized}Form item={overviewItem} actionData={actionData} />}
</SlideOverWideEmpty>
</IndexPageLayout>
);
}
`;
const uiCellControls: string[] = [];
entity.properties
.filter((f) => !f.isDefault)
.forEach((property) => {
CodeGeneratorPropertiesHelper.uiCell({ code: uiCellControls, imports, property });
});
template = template.replace("{PROPERTIES_UI_CELLS}", uiCellControls.join("\n "));
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, template].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,48 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
const titleExpression = CodeGeneratorHelper.getEntityTitleExpression(entity);
const pluralExpression = CodeGeneratorHelper.getEntityPluralExpression(entity);
return `import { useActionData, useLoaderData, useLocation } from "react-router";
import { useTranslation } from "react-i18next";
import CheckPlanFeatureLimit from "~/components/core/settings/subscription/CheckPlanFeatureLimit";
import NewPageLayout from "~/components/ui/layouts/NewPageLayout";
import UrlUtils from "~/utils/app/UrlUtils";
import ${capitalized}Form from "../../components/${capitalized}Form";
import { ${capitalized}RoutesNewApi } from "../api/${capitalized}Routes.New.Api";
export default function ${capitalized}RoutesNewView() {
const { t } = useTranslation();
const data = useLoaderData<${capitalized}RoutesNewApi.LoaderData>();
const actionData = useActionData<${capitalized}RoutesNewApi.ActionData>();
const location = useLocation();
return (
<NewPageLayout
title={t("shared.create") + " " + ${titleExpression}}
menu={[
{
title: ${pluralExpression},
routePath: UrlUtils.getParentRoute(location.pathname),
},
{
title: t("shared.create"),
routePath: "",
},
]}
>
<div className="mx-auto max-w-2xl">
<CheckPlanFeatureLimit item={data.featureUsage}>
<${capitalized}Form isCreating={true} actionData={actionData} />
</CheckPlanFeatureLimit>
</div>
</NewPageLayout>
);
}
`;
}
export default {
generate,
};
@@ -0,0 +1,19 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import RowSettingsPermissions from "~/components/entities/rows/RowSettingsPermissions";
import { ${capitalized}RoutesShareApi } from "../api/${capitalized}Routes.Share.Api";
import { useLoaderData } from "react-router";
export default function ${capitalized}RoutesShareView() {
const data = useLoaderData<${capitalized}RoutesShareApi.LoaderData>();
return <RowSettingsPermissions item={data.item.row} items={data.item.row.permissions} tenants={data.tenants} users={data.users} />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,19 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized } = CodeGeneratorHelper.getNames(entity);
return `import RowSettingsTags from "~/components/entities/rows/RowSettingsTags";
import { ${capitalized}RoutesTagsApi } from "../api/${capitalized}Routes.Tags.Api";
import { useLoaderData } from "react-router";
export default function ${capitalized}RoutesTagsView() {
const data = useLoaderData<${capitalized}RoutesTagsApi.LoaderData>();
return <RowSettingsTags item={data.item.row} tags={data.tags} />;
}
`;
}
export default {
generate,
};
@@ -0,0 +1,35 @@
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import CodeGeneratorPropertiesHelper from "../../utils/CodeGeneratorPropertiesHelper";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized, name } = CodeGeneratorHelper.getNames(entity);
const code: string[] = [
`model ${capitalized} {
id String @id @unique
row Row @relation(fields: [id], references: [id], onDelete: Cascade)`,
];
entity.properties
.filter((f) => !f.isDefault)
.forEach((property) => {
CodeGeneratorPropertiesHelper.schema({ code, property });
});
code.push("}");
code.push("");
code.push(`// Add at the end of the Row model
model Row {
// ...
${name} ${capitalized}?
}`);
return [...code].join("\n");
}
export default {
generate,
};
@@ -0,0 +1,131 @@
import { PropertyType } from "~/application/enums/entities/PropertyType";
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
import CodeGeneratorPropertiesHelper from "../../utils/CodeGeneratorPropertiesHelper";
function generate({ entity }: { entity: EntityWithDetails }): string {
const { capitalized, name } = CodeGeneratorHelper.getNames(entity);
const imports: string[] = [];
imports.push(`import { PaginationDto } from "~/application/dtos/data/PaginationDto";
import { RowsApi } from "~/utils/api/.server/RowsApi";
import { getEntityByName } from "~/utils/db/entities/entities.db.server";
import RowHelper from "~/utils/helpers/RowHelper";
import RowValueService, { RowValueUpdateDto } from "~/utils/helpers/.server/RowValueService";`);
if (entity.properties.some((p) => p.type === PropertyType.MEDIA)) {
imports.push(`import { RowMedia } from "@toolctrl/db/prisma-browser";`);
}
imports.push(`import { ${capitalized}Dto } from "../dtos/${capitalized}Dto";
import { ${capitalized}CreateDto } from "../dtos/${capitalized}CreateDto";
import ${capitalized}Helpers from "../helpers/${capitalized}Helpers";`);
let template = `
export namespace ${capitalized}Service {
async function getEntity(tenantId: string | null) {
let entityName = "${name}";
const entity = await getEntityByName({ tenantId, name: entityName });
if (!entity) {
throw Error("Entity not found: " + entityName);
}
return entity;
}
export async function getAll({
tenantId,
userId,
urlSearchParams,
}: {
tenantId: string | null;
userId?: string;
urlSearchParams?: URLSearchParams;
}): Promise<{ items: ${capitalized}Dto[]; pagination: PaginationDto }> {
const entity = await getEntity(tenantId);
const data = await RowsApi.getAll({
entity,
tenantId,
userId,
urlSearchParams,
});
return {
items: data.items.map((row) => ${capitalized}Helpers.rowToDto({ entity, row })),
pagination: data.pagination,
};
}
export async function get(id: string, session: { tenantId: string | null; userId?: string }): Promise<${capitalized}Dto | null> {
const entity = await getEntity(session.tenantId);
const { item, rowPermissions } = await RowsApi.get(id, {
tenantId: session.tenantId,
userId: session.userId,
entity,
});
if (!rowPermissions.canRead) {
return null;
}
return ${capitalized}Helpers.rowToDto({ entity, row: item });
}
export async function create(data: ${capitalized}CreateDto, session: { tenantId: string | null; userId?: string }): Promise<${capitalized}Dto> {
const entity = await getEntity(session.tenantId);
const rowValues = RowHelper.getRowPropertiesFromForm({
entity,
values: [
{PROPERTIES_CREATE_VALUES}
],
});
const item = await RowsApi.create({
tenantId: session.tenantId,
userId: session.userId,
entity,
rowValues,
});
return ${capitalized}Helpers.rowToDto({ entity, row: item });
}
export async function update(id: string, data: Partial<${capitalized}Dto>, session: { tenantId: string | null; userId?: string }): Promise<${capitalized}Dto> {
const entity = await getEntity(session.tenantId);
const { item: row, rowPermissions } = await RowsApi.get(id, {
tenantId: session.tenantId,
userId: session.userId,
entity,
});
if (!row || !rowPermissions.canUpdate) {
throw Error("Not found");
}
const values: RowValueUpdateDto[] = [];
{PROPERTIES_UPDATE_VALUES}
const item = await RowValueService.update({
entity,
row,
values,
session,
});
return ${capitalized}Helpers.rowToDto({ entity, row: item });
}
export async function del(id: string, session: { tenantId: string | null; userId?: string }): Promise<void> {
const entity = await getEntity(session.tenantId);
await RowsApi.del(id, {
entity,
tenantId: session.tenantId,
userId: session.userId,
});
}
}`;
const createValues: string[] = [];
const updateValues: string[] = [];
entity.properties
.filter((f) => !f.isDefault)
.forEach((property) => {
if (property.showInCreate) {
CodeGeneratorPropertiesHelper.createDtoToRow({ code: createValues, property });
}
CodeGeneratorPropertiesHelper.updateDtoToRow({ code: updateValues, property });
});
template = template.replace("{PROPERTIES_CREATE_VALUES}", createValues.join("\n "));
template = template.replace("{PROPERTIES_UPDATE_VALUES}", updateValues.join("\n "));
const uniqueImports = [...new Set(imports)];
return [...uniqueImports, template].join("\n");
}
export default {
generate,
};