Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,271 @@
|
||||
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
|
||||
import { t as i18nT } from "i18next";
|
||||
import CodeTemplateForm from "../templates/components/CodeTemplate.Form";
|
||||
import CodeTemplateDto from "../templates/dtos/CodeTemplate.Dto";
|
||||
import CodeTemplateDtoCreate from "../templates/dtos/CodeTemplate.Dto.Create";
|
||||
import CodeTemplateHelper from "../templates/helpers/CodeTemplate.Helper";
|
||||
import CodeTemplateRoutesActivityApi from "../templates/routes/api/CodeTemplate.Routes.Activity.Api";
|
||||
import CodeTemplateRoutesEditApi from "../templates/routes/api/CodeTemplate.Routes.Edit.Api";
|
||||
import CodeTemplateRoutesIndexApi from "../templates/routes/api/CodeTemplate.Routes.Index.Api";
|
||||
import CodeTemplateRoutesNewApi from "../templates/routes/api/CodeTemplate.Routes.New.Api";
|
||||
import CodeTemplateRoutesShareApi from "../templates/routes/api/CodeTemplate.Routes.Share.Api";
|
||||
import CodeTemplateRoutesTagsApi from "../templates/routes/api/CodeTemplate.Routes.Tags.Api";
|
||||
import CodeTemplateRoutesActivityComponent from "../templates/routes/components/CodeTemplate.Routes.Activity.Component";
|
||||
import CodeTemplateRoutesEditComponent from "../templates/routes/components/CodeTemplate.Routes.Edit.Component";
|
||||
import CodeTemplateRoutesIndexComponent from "../templates/routes/components/CodeTemplate.Routes.Index.Component";
|
||||
import CodeTemplateRoutesNewComponent from "../templates/routes/components/CodeTemplate.Routes.New.Component";
|
||||
import CodeTemplateRoutesShareComponent from "../templates/routes/components/CodeTemplate.Routes.Share.Component";
|
||||
import CodeTemplateRoutesTagsComponent from "../templates/routes/components/CodeTemplate.Routes.Tags.Component";
|
||||
import CodeTemplateRoutesActivityView from "../templates/routes/views/CodeTemplate.Routes.Activity.View";
|
||||
import CodeTemplateRoutesEditView from "../templates/routes/views/CodeTemplate.Routes.Edit.View";
|
||||
import CodeTemplateRoutesIndexView from "../templates/routes/views/CodeTemplate.Routes.Index.View";
|
||||
import CodeTemplateRoutesNewView from "../templates/routes/views/CodeTemplate.Routes.New.View";
|
||||
import CodeTemplateRoutesShareView from "../templates/routes/views/CodeTemplate.Routes.Share.View";
|
||||
import CodeTemplateRoutesTagsView from "../templates/routes/views/CodeTemplate.Routes.Tags.View";
|
||||
import CodeTemplateModelSchemaPrisma from "../templates/schemas/CodeTemplate.Model.Schema.Prisma";
|
||||
import CodeTemplateService from "../templates/services/CodeTemplate.Service";
|
||||
import CodeGeneratorHelper from "../utils/CodeGeneratorHelper";
|
||||
import Constants from "~/application/Constants";
|
||||
|
||||
export type CodeGeneratorOptions = {
|
||||
entity: EntityWithDetails;
|
||||
type: string;
|
||||
moduleDirectory: string;
|
||||
routesDirectory: string;
|
||||
deleteFilesOnFinish: boolean;
|
||||
};
|
||||
|
||||
export type CodeGeneratorFileDto = {
|
||||
type: "module" | "route" | "model";
|
||||
directory?: string;
|
||||
file: string;
|
||||
content: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
function getModuleFiles(options: CodeGeneratorOptions): CodeGeneratorFileDto[] {
|
||||
const { entity } = options;
|
||||
const { capitalized } = CodeGeneratorHelper.getNames(entity);
|
||||
|
||||
const files: CodeGeneratorFileDto[] = [
|
||||
{
|
||||
type: "module",
|
||||
directory: `dtos`,
|
||||
file: `${capitalized}Dto.ts`,
|
||||
content: CodeTemplateDto.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.dto.rowInterface"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `dtos`,
|
||||
file: `${capitalized}CreateDto.ts`,
|
||||
content: CodeTemplateDtoCreate.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.dto.createInput"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `components`,
|
||||
file: `${capitalized}Form.tsx`,
|
||||
content: CodeTemplateForm.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.components.form"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `helpers`,
|
||||
file: `${capitalized}Helpers.ts`,
|
||||
content: CodeTemplateHelper.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.helpers.formDataTransformers"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `services`,
|
||||
file: `${capitalized}Service.ts`,
|
||||
content: CodeTemplateService.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.services.crud"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/api`,
|
||||
file: `${capitalized}Routes.Index.Api.ts`,
|
||||
content: CodeTemplateRoutesIndexApi.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.api.index"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/api`,
|
||||
file: `${capitalized}Routes.New.Api.ts`,
|
||||
content: CodeTemplateRoutesNewApi.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.api.new"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/api`,
|
||||
file: `${capitalized}Routes.Edit.Api.ts`,
|
||||
content: CodeTemplateRoutesEditApi.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.api.edit"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/api`,
|
||||
file: `${capitalized}Routes.Activity.Api.ts`,
|
||||
content: CodeTemplateRoutesActivityApi.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.api.activity"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/api`,
|
||||
file: `${capitalized}Routes.Share.Api.ts`,
|
||||
content: CodeTemplateRoutesShareApi.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.api.share"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/api`,
|
||||
file: `${capitalized}Routes.Tags.Api.ts`,
|
||||
content: CodeTemplateRoutesTagsApi.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.api.tags"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/views`,
|
||||
file: `${capitalized}Routes.Index.View.tsx`,
|
||||
content: CodeTemplateRoutesIndexView.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.views.index"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/views`,
|
||||
file: `${capitalized}Routes.New.View.tsx`,
|
||||
content: CodeTemplateRoutesNewView.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.views.new"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/views`,
|
||||
file: `${capitalized}Routes.Edit.View.tsx`,
|
||||
content: CodeTemplateRoutesEditView.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.views.edit"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/views`,
|
||||
file: `${capitalized}Routes.Activity.View.tsx`,
|
||||
content: CodeTemplateRoutesActivityView.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.views.activity"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/views`,
|
||||
file: `${capitalized}Routes.Share.View.tsx`,
|
||||
content: CodeTemplateRoutesShareView.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.views.share"),
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
directory: `routes/views`,
|
||||
file: `${capitalized}Routes.Tags.View.tsx`,
|
||||
content: CodeTemplateRoutesTagsView.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.views.tags"),
|
||||
},
|
||||
];
|
||||
|
||||
if (options.type === "custom") {
|
||||
files.unshift({
|
||||
type: "model",
|
||||
file: `schema.prisma`,
|
||||
content: CodeTemplateModelSchemaPrisma.generate({ entity }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.schema.prisma"),
|
||||
});
|
||||
}
|
||||
|
||||
addTopComments(files, entity);
|
||||
return files;
|
||||
}
|
||||
|
||||
function getRouteFiles({ type, entity, moduleDirectory }: CodeGeneratorOptions) {
|
||||
let moduleLocation = moduleDirectory.replace("./app", "");
|
||||
if (moduleLocation.startsWith("/")) {
|
||||
moduleLocation = moduleLocation.substring(1);
|
||||
}
|
||||
const files: CodeGeneratorFileDto[] = [
|
||||
{
|
||||
type: "route",
|
||||
file: `index.tsx`,
|
||||
content: CodeTemplateRoutesIndexComponent.generate({ entity, moduleLocation }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.routeShells.index"),
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
file: `new.tsx`,
|
||||
content: CodeTemplateRoutesNewComponent.generate({ entity, moduleLocation }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.routeShells.new"),
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
file: `$id.tsx`,
|
||||
content: CodeTemplateRoutesEditComponent.generate({ entity, moduleLocation }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.routeShells.edit"),
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
file: `activity.tsx`,
|
||||
directory: "$id",
|
||||
content: CodeTemplateRoutesActivityComponent.generate({ entity, moduleLocation }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.routeShells.activity"),
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
file: `share.tsx`,
|
||||
directory: "$id",
|
||||
content: CodeTemplateRoutesShareComponent.generate({ entity, moduleLocation }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.routeShells.share"),
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
file: `tags.tsx`,
|
||||
directory: "$id",
|
||||
content: CodeTemplateRoutesTagsComponent.generate({ entity, moduleLocation }),
|
||||
description: i18nT("models.entity.codeGenerator.fileDescriptions.routeShells.tags"),
|
||||
},
|
||||
];
|
||||
|
||||
addTopComments(files, entity);
|
||||
return files;
|
||||
}
|
||||
|
||||
function addTopComments(files: CodeGeneratorFileDto[], entity: EntityWithDetails) {
|
||||
const rawLabelWarnings = CodeGeneratorHelper.getRawLabelWarnings(entity);
|
||||
files.forEach((file) => {
|
||||
const comments: string[] = [];
|
||||
comments.push(file.description);
|
||||
comments.push(...rawLabelWarnings);
|
||||
comments.push("Date: " + new Date().toISOString().slice(0, 10));
|
||||
comments.push("Version: App v" + Constants.VERSION);
|
||||
file.content = comments.map((comment) => `// ${comment}`).join("\n") + "\n\n" + file.content;
|
||||
});
|
||||
}
|
||||
|
||||
function moduleDirectoryOptions(entity: EntityWithDetails) {
|
||||
let tests = `./apps/web/app/modules/codeGeneratorTests/` + entity.slug;
|
||||
let app = `./apps/web/app/modules/` + entity.slug;
|
||||
return [
|
||||
{ name: tests, value: tests },
|
||||
{ name: app, value: app },
|
||||
];
|
||||
}
|
||||
|
||||
function routeDirectoryOptions(entity: EntityWithDetails) {
|
||||
let tests = `./apps/web/app/routes/admin/entities/code-generator/tests/` + entity.slug;
|
||||
let app = `./apps/web/app/routes/app.$tenant/` + entity.slug;
|
||||
return [
|
||||
{ name: tests, value: tests },
|
||||
{ name: app, value: app },
|
||||
];
|
||||
}
|
||||
|
||||
export default {
|
||||
getModuleFiles,
|
||||
getRouteFiles,
|
||||
moduleDirectoryOptions,
|
||||
routeDirectoryOptions,
|
||||
};
|
||||
+78
@@ -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 })');
|
||||
}
|
||||
});
|
||||
});
|
||||
+114
@@ -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,
|
||||
};
|
||||
+25
@@ -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,
|
||||
};
|
||||
+54
@@ -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,
|
||||
};
|
||||
+126
@@ -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,
|
||||
};
|
||||
+225
@@ -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,
|
||||
};
|
||||
+62
@@ -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,
|
||||
};
|
||||
+88
@@ -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,
|
||||
};
|
||||
+123
@@ -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,
|
||||
};
|
||||
+145
@@ -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,
|
||||
};
|
||||
+25
@@ -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,
|
||||
};
|
||||
+25
@@ -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,
|
||||
};
|
||||
+24
@@ -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,
|
||||
};
|
||||
+25
@@ -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,
|
||||
};
|
||||
+25
@@ -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,
|
||||
};
|
||||
+25
@@ -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,
|
||||
};
|
||||
+19
@@ -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,
|
||||
};
|
||||
+149
@@ -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,
|
||||
};
|
||||
+134
@@ -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,
|
||||
};
|
||||
+48
@@ -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,
|
||||
};
|
||||
+19
@@ -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,
|
||||
};
|
||||
+19
@@ -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,
|
||||
};
|
||||
+35
@@ -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,
|
||||
};
|
||||
+131
@@ -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,
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { EntityWithDetails } from "~/utils/db/entities/entities.db.server";
|
||||
|
||||
const TRANSLATION_KEY_PATTERN = /^[a-z0-9]+(?:\.[A-Za-z0-9_-]+)+$/;
|
||||
|
||||
function isTranslationKey(value: string) {
|
||||
return TRANSLATION_KEY_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function toGeneratedString(value: string) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function getTranslationExpression(value: string, fallbackKey: string) {
|
||||
if (isTranslationKey(value)) {
|
||||
return `t(${toGeneratedString(value)})`;
|
||||
}
|
||||
return `t(${toGeneratedString(fallbackKey)}, { defaultValue: ${toGeneratedString(value)} })`;
|
||||
}
|
||||
|
||||
function getEntityTitleExpression(entity: EntityWithDetails) {
|
||||
return getTranslationExpression(entity.title, `generated.entities.${entity.name}.title`);
|
||||
}
|
||||
|
||||
function getEntityPluralExpression(entity: EntityWithDetails) {
|
||||
return getTranslationExpression(entity.titlePlural, `generated.entities.${entity.name}.titlePlural`);
|
||||
}
|
||||
|
||||
function getPropertyTitleExpression(property: { name: string; title: string }) {
|
||||
return getTranslationExpression(property.title, `generated.fields.${property.name}`);
|
||||
}
|
||||
|
||||
function formatRawLabel(value: string) {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function getRawLabelWarnings(entity: EntityWithDetails) {
|
||||
const rawLabels = [
|
||||
!isTranslationKey(entity.title) ? `entity title "${formatRawLabel(entity.title)}"` : undefined,
|
||||
!isTranslationKey(entity.titlePlural) ? `entity plural "${formatRawLabel(entity.titlePlural)}"` : undefined,
|
||||
...entity.properties.map((property) => (!isTranslationKey(property.title) ? `property ${property.name} title "${formatRawLabel(property.title)}"` : undefined)),
|
||||
].filter((label): label is string => Boolean(label));
|
||||
|
||||
if (rawLabels.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return [`Generator warning: raw labels are emitted with stable fallback i18n keys; add locale keys for ${rawLabels.join(", ")}.`];
|
||||
}
|
||||
|
||||
function getNames(entity: EntityWithDetails) {
|
||||
return {
|
||||
capitalized: entity.name.charAt(0).toUpperCase() + entity.name.slice(1),
|
||||
name: entity.name,
|
||||
title: entity.title,
|
||||
plural: entity.titlePlural,
|
||||
slug: entity.slug,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
getNames,
|
||||
getEntityTitleExpression,
|
||||
getEntityPluralExpression,
|
||||
getPropertyTitleExpression,
|
||||
getRawLabelWarnings,
|
||||
getTranslationExpression,
|
||||
isTranslationKey,
|
||||
};
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PropertyType } from "~/application/enums/entities/PropertyType";
|
||||
import type { PropertyWithDetails } from "~/utils/db/entities/entities.db.server";
|
||||
import CodeGeneratorPropertiesHelper from "./CodeGeneratorPropertiesHelper";
|
||||
|
||||
function createProperty(type: PropertyType, patch: Partial<PropertyWithDetails> = {}): PropertyWithDetails {
|
||||
return {
|
||||
name: `field${type}`,
|
||||
title: `entities.fields.field${type}`,
|
||||
type,
|
||||
subtype: null,
|
||||
isDefault: false,
|
||||
isRequired: false,
|
||||
showInCreate: true,
|
||||
attributes: [],
|
||||
options: [],
|
||||
formula: null,
|
||||
...patch,
|
||||
} as unknown as PropertyWithDetails;
|
||||
}
|
||||
|
||||
describe("CodeGeneratorPropertiesHelper", () => {
|
||||
it("does not emit unresolved placeholder markers for any declared property type", () => {
|
||||
const propertyTypes = Object.values(PropertyType).filter((value): value is PropertyType => typeof value === "number");
|
||||
|
||||
for (const propertyType of propertyTypes) {
|
||||
const property = createProperty(propertyType);
|
||||
const imports: string[] = [];
|
||||
const code: string[] = [];
|
||||
|
||||
CodeGeneratorPropertiesHelper.type({ code, imports, property });
|
||||
CodeGeneratorPropertiesHelper.rowToDto({ code, property });
|
||||
CodeGeneratorPropertiesHelper.formToDto({ code, property });
|
||||
CodeGeneratorPropertiesHelper.createDtoToRow({ code, property });
|
||||
CodeGeneratorPropertiesHelper.updateDtoToRow({ code, property });
|
||||
CodeGeneratorPropertiesHelper.uiForm({ code, imports, property, index: 0 });
|
||||
CodeGeneratorPropertiesHelper.uiCell({ code, imports, property });
|
||||
CodeGeneratorPropertiesHelper.schema({ code, property });
|
||||
|
||||
const generated = code.join("\n");
|
||||
expect(generated).not.toContain("TODO");
|
||||
expect(generated).not.toContain("not implemented");
|
||||
expect(generated.trim()).not.toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("maps complex generated schema properties to valid Prisma scalar types", () => {
|
||||
const complexTypes = [
|
||||
PropertyType.MEDIA,
|
||||
PropertyType.MULTI_SELECT,
|
||||
PropertyType.MULTI_TEXT,
|
||||
PropertyType.RANGE_NUMBER,
|
||||
PropertyType.RANGE_DATE,
|
||||
];
|
||||
|
||||
for (const propertyType of complexTypes) {
|
||||
const code: string[] = [];
|
||||
CodeGeneratorPropertiesHelper.schema({ code, property: createProperty(propertyType) });
|
||||
|
||||
expect(code.join("\n")).toContain(" Json?");
|
||||
expect(code.join("\n")).not.toContain("TODO");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not treat raw English labels as direct i18n keys", () => {
|
||||
const property = createProperty(PropertyType.TEXT, {
|
||||
name: "firstName",
|
||||
title: "First Name",
|
||||
});
|
||||
const imports: string[] = [];
|
||||
const formCode: string[] = [];
|
||||
const cellCode: string[] = [];
|
||||
|
||||
CodeGeneratorPropertiesHelper.uiForm({ code: formCode, imports, property, index: 0 });
|
||||
CodeGeneratorPropertiesHelper.uiCell({ code: cellCode, imports, property });
|
||||
|
||||
expect(formCode.join("\n")).toContain('t("generated.fields.firstName", { defaultValue: "First Name" })');
|
||||
expect(cellCode.join("\n")).toContain('t("generated.fields.firstName", { defaultValue: "First Name" })');
|
||||
expect(formCode.join("\n")).not.toContain('t("First Name")');
|
||||
expect(cellCode.join("\n")).not.toContain('t("First Name")');
|
||||
});
|
||||
|
||||
it("keeps explicit i18n keys as translation keys", () => {
|
||||
const property = createProperty(PropertyType.TEXT, {
|
||||
name: "firstName",
|
||||
title: "models.employee.firstName",
|
||||
});
|
||||
const imports: string[] = [];
|
||||
const formCode: string[] = [];
|
||||
|
||||
CodeGeneratorPropertiesHelper.uiForm({ code: formCode, imports, property, index: 0 });
|
||||
|
||||
expect(formCode.join("\n")).toContain('t("models.employee.firstName")');
|
||||
expect(formCode.join("\n")).not.toContain("{ defaultValue:");
|
||||
});
|
||||
|
||||
it("treats formula as a read-only computed field in generated surfaces", () => {
|
||||
const property = createProperty(PropertyType.FORMULA, {
|
||||
name: "computedScore",
|
||||
title: "entities.fields.computedScore",
|
||||
formula: { name: "Score", resultAs: "number", calculationTrigger: "manual" },
|
||||
});
|
||||
const imports: string[] = [];
|
||||
const typeCode: string[] = [];
|
||||
const rowCode: string[] = [];
|
||||
const formCode: string[] = [];
|
||||
const createCode: string[] = [];
|
||||
const updateCode: string[] = [];
|
||||
const formUiCode: string[] = [];
|
||||
const cellCode: string[] = [];
|
||||
const schemaCode: string[] = [];
|
||||
|
||||
CodeGeneratorPropertiesHelper.type({ code: typeCode, imports, property });
|
||||
CodeGeneratorPropertiesHelper.rowToDto({ code: rowCode, property });
|
||||
CodeGeneratorPropertiesHelper.formToDto({ code: formCode, property });
|
||||
CodeGeneratorPropertiesHelper.createDtoToRow({ code: createCode, property });
|
||||
CodeGeneratorPropertiesHelper.updateDtoToRow({ code: updateCode, property });
|
||||
CodeGeneratorPropertiesHelper.uiForm({ code: formUiCode, imports, property, index: 0 });
|
||||
CodeGeneratorPropertiesHelper.uiCell({ code: cellCode, imports, property });
|
||||
CodeGeneratorPropertiesHelper.schema({ code: schemaCode, property });
|
||||
|
||||
expect(typeCode.join("\n")).toContain("computedScore: number | undefined");
|
||||
expect(rowCode.join("\n")).toContain('RowValueHelper.getNumber({ entity, row, name: "computedScore" })');
|
||||
expect(formCode).toEqual([]);
|
||||
expect(createCode).toEqual([]);
|
||||
expect(updateCode).toEqual([]);
|
||||
expect(formUiCode.join("\n")).not.toContain('name="computedScore"');
|
||||
expect(cellCode.join("\n")).toContain("RowNumberCell");
|
||||
expect(schemaCode.join("\n")).toContain("computed Formula field");
|
||||
expect(schemaCode.join("\n")).not.toContain("Json?");
|
||||
});
|
||||
|
||||
it("maps formula resultAs variants to stable DTO display types", () => {
|
||||
const cases = [
|
||||
["number", "number | undefined", "getNumber", "RowNumberCell"],
|
||||
["string", "string | undefined", "getText", "truncate"],
|
||||
["date", "Date | undefined", "getDate", "RowDateCell"],
|
||||
["boolean", "boolean | undefined", "getBoolean", "RowBooleanCell"],
|
||||
] as const;
|
||||
|
||||
for (const [resultAs, dtoType, rowHelper, cellMarker] of cases) {
|
||||
const property = createProperty(PropertyType.FORMULA, {
|
||||
name: `computed${resultAs}`,
|
||||
formula: { name: resultAs, resultAs, calculationTrigger: "manual" },
|
||||
});
|
||||
const imports: string[] = [];
|
||||
const typeCode: string[] = [];
|
||||
const rowCode: string[] = [];
|
||||
const cellCode: string[] = [];
|
||||
|
||||
CodeGeneratorPropertiesHelper.type({ code: typeCode, imports, property });
|
||||
CodeGeneratorPropertiesHelper.rowToDto({ code: rowCode, property });
|
||||
CodeGeneratorPropertiesHelper.uiCell({ code: cellCode, imports, property });
|
||||
|
||||
expect(typeCode.join("\n")).toContain(dtoType);
|
||||
expect(rowCode.join("\n")).toContain(`RowValueHelper.${rowHelper}`);
|
||||
expect(cellCode.join("\n")).toContain(cellMarker);
|
||||
}
|
||||
});
|
||||
});
|
||||
+689
@@ -0,0 +1,689 @@
|
||||
import { PropertyAttributeName } from "~/application/enums/entities/PropertyAttributeName";
|
||||
import { PropertyType } from "~/application/enums/entities/PropertyType";
|
||||
import { Colors } from "~/application/enums/shared/Colors";
|
||||
import CodeGeneratorHelper from "~/modules/codeGenerator/utils/CodeGeneratorHelper";
|
||||
import { PropertyWithDetails } from "~/utils/db/entities/entities.db.server";
|
||||
import PropertyAttributeHelper from "~/utils/helpers/PropertyAttributeHelper";
|
||||
import { SelectOptionsDisplay } from "~/utils/shared/SelectOptionsUtils";
|
||||
|
||||
function getPropertyTypeLabel(property: PropertyWithDetails) {
|
||||
return PropertyType[property.type] ?? `UNKNOWN_${property.type}`;
|
||||
}
|
||||
|
||||
function unsupportedGeneratedComment(property: PropertyWithDetails, surface: string) {
|
||||
return `Unsupported generated ${surface} for ${property.name} (${getPropertyTypeLabel(property)})`;
|
||||
}
|
||||
|
||||
function isWritableProperty(property: PropertyWithDetails) {
|
||||
return property.type !== PropertyType.FORMULA;
|
||||
}
|
||||
|
||||
function getFormulaResultAs(property: PropertyWithDetails) {
|
||||
const resultAs = property.formula?.resultAs;
|
||||
if (resultAs === "number" || resultAs === "string" || resultAs === "date" || resultAs === "boolean") {
|
||||
return resultAs;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function getFormulaType(property: PropertyWithDetails) {
|
||||
const resultAs = getFormulaResultAs(property);
|
||||
if (resultAs === "number") {
|
||||
return "number";
|
||||
}
|
||||
if (resultAs === "string") {
|
||||
return "string";
|
||||
}
|
||||
if (resultAs === "date") {
|
||||
return "Date";
|
||||
}
|
||||
if (resultAs === "boolean") {
|
||||
return "boolean";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function type({ code, imports, property }: { code: string[]; imports: string[]; property: PropertyWithDetails }) {
|
||||
let type = "";
|
||||
if (property.type === PropertyType.TEXT) {
|
||||
type = property.isRequired ? "string" : "string | undefined";
|
||||
} else if (property.type === PropertyType.NUMBER) {
|
||||
type = property.isRequired ? "number" : "number | undefined";
|
||||
} else if (property.type === PropertyType.DATE) {
|
||||
type = property.isRequired ? "Date" : "Date | undefined";
|
||||
} else if (property.type === PropertyType.SELECT) {
|
||||
type = property.isRequired ? "string" : "string | undefined";
|
||||
} else if (property.type === PropertyType.BOOLEAN) {
|
||||
type = property.isRequired ? "boolean" : "boolean | undefined";
|
||||
} else if (property.type === PropertyType.MEDIA) {
|
||||
imports.push(`import { MediaDto } from "~/application/dtos/entities/MediaDto";`);
|
||||
if (PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Max) === 1) {
|
||||
type = property.isRequired ? "MediaDto" : "MediaDto | undefined";
|
||||
} else {
|
||||
type = property.isRequired ? "MediaDto[]" : "MediaDto[] | undefined";
|
||||
}
|
||||
} else if (property.type === PropertyType.MULTI_SELECT) {
|
||||
imports.push(`import { RowValueMultipleDto } from "~/application/dtos/entities/RowValueMultipleDto";`);
|
||||
type = "RowValueMultipleDto[]";
|
||||
} else if (property.type === PropertyType.RANGE_NUMBER) {
|
||||
imports.push(`import { RowValueRangeDto } from "~/application/dtos/entities/RowValueRangeDto";`);
|
||||
type = property.isRequired ? "RowValueRangeDto" : "RowValueRangeDto | undefined";
|
||||
} else if (property.type === PropertyType.RANGE_DATE) {
|
||||
imports.push(`import { RowValueRangeDto } from "~/application/dtos/entities/RowValueRangeDto";`);
|
||||
type = property.isRequired ? "RowValueRangeDto" : "RowValueRangeDto | undefined";
|
||||
} else if (property.type === PropertyType.MULTI_TEXT) {
|
||||
imports.push(`import { RowValueMultipleDto } from "~/application/dtos/entities/RowValueMultipleDto";`);
|
||||
type = "RowValueMultipleDto[]";
|
||||
} else if (property.type === PropertyType.FORMULA) {
|
||||
const formulaType = getFormulaType(property);
|
||||
type = property.isRequired ? formulaType : `${formulaType} | undefined`;
|
||||
} else {
|
||||
type = "unknown";
|
||||
}
|
||||
|
||||
let comments: string[] = [];
|
||||
comments.push(property.isRequired ? "required" : "optional");
|
||||
property.attributes
|
||||
.filter((f) => f.value)
|
||||
.forEach((attribute) => {
|
||||
comments.push(`${attribute.name}: ${attribute.value}`);
|
||||
});
|
||||
|
||||
code.push(` ${property.name}: ${type}; // ${comments.join(", ")}`);
|
||||
}
|
||||
|
||||
function rowToDto({ code, property }: { code: string[]; property: PropertyWithDetails }) {
|
||||
let propertyCode = "";
|
||||
let comments: string[] = [];
|
||||
comments.push(property.isRequired ? "required" : "optional");
|
||||
if (property.type === PropertyType.TEXT) {
|
||||
let fallbackValue = property.isRequired ? ` ?? ""` : ``;
|
||||
propertyCode = `${property.name}: RowValueHelper.getText({ entity, row, name: "${property.name}" })${fallbackValue},`;
|
||||
} else if (property.type === PropertyType.NUMBER) {
|
||||
let fallbackValue = property.isRequired ? ` ?? 0` : ``;
|
||||
propertyCode = `${property.name}: RowValueHelper.getNumber({ entity, row, name: "${property.name}" })${fallbackValue},`;
|
||||
} else if (property.type === PropertyType.DATE) {
|
||||
let fallbackValue = property.isRequired ? ` ?? new Date()` : ``;
|
||||
propertyCode = `${property.name}: RowValueHelper.getDate({ entity, row, name: "${property.name}" })${fallbackValue},`;
|
||||
} else if (property.type === PropertyType.SELECT) {
|
||||
let fallbackValue = property.isRequired ? ` ?? ""` : ``;
|
||||
propertyCode = `${property.name}: RowValueHelper.getText({ entity, row, name: "${property.name}" })${fallbackValue},`;
|
||||
} else if (property.type === PropertyType.BOOLEAN) {
|
||||
propertyCode = `${property.name}: RowValueHelper.getBoolean({ entity, row, name: "${property.name}" }) ?? false,`;
|
||||
} else if (property.type === PropertyType.MEDIA) {
|
||||
if (PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Max) === 1) {
|
||||
if (property.isRequired) {
|
||||
propertyCode = `${property.name}: RowValueHelper.getFirstMedia({ entity, row, name: "${property.name}" }) as MediaDto,`;
|
||||
} else {
|
||||
propertyCode = `${property.name}: RowValueHelper.getFirstMedia({ entity, row, name: "${property.name}" }),`;
|
||||
}
|
||||
} else {
|
||||
propertyCode = `${property.name}: RowValueHelper.getMedia({ entity, row, name: "${property.name}" }),`;
|
||||
}
|
||||
} else if (property.type === PropertyType.MULTI_SELECT) {
|
||||
propertyCode = `${property.name}: RowValueHelper.getMultiple({ entity, row, name: "${property.name}" }),`;
|
||||
} else if (property.type === PropertyType.MULTI_TEXT) {
|
||||
propertyCode = `${property.name}: RowValueHelper.getMultiple({ entity, row, name: "${property.name}" }),`;
|
||||
} else if (property.type === PropertyType.RANGE_NUMBER) {
|
||||
propertyCode = `${property.name}: RowValueHelper.getNumberRange({ entity, row, name: "${property.name}" }),`;
|
||||
} else if (property.type === PropertyType.RANGE_DATE) {
|
||||
propertyCode = `${property.name}: RowValueHelper.getDateRange({ entity, row, name: "${property.name}" }),`;
|
||||
} else if (property.type === PropertyType.FORMULA) {
|
||||
const resultAs = getFormulaResultAs(property);
|
||||
if (resultAs === "number") {
|
||||
let fallbackValue = property.isRequired ? ` ?? 0` : ``;
|
||||
propertyCode = `${property.name}: RowValueHelper.getNumber({ entity, row, name: "${property.name}" })${fallbackValue},`;
|
||||
} else if (resultAs === "string") {
|
||||
let fallbackValue = property.isRequired ? ` ?? ""` : ``;
|
||||
propertyCode = `${property.name}: RowValueHelper.getText({ entity, row, name: "${property.name}" })${fallbackValue},`;
|
||||
} else if (resultAs === "date") {
|
||||
let fallbackValue = property.isRequired ? ` ?? new Date()` : ``;
|
||||
propertyCode = `${property.name}: RowValueHelper.getDate({ entity, row, name: "${property.name}" })${fallbackValue},`;
|
||||
} else if (resultAs === "boolean") {
|
||||
propertyCode = `${property.name}: RowValueHelper.getBoolean({ entity, row, name: "${property.name}" }) ?? false,`;
|
||||
} else {
|
||||
propertyCode = `${property.name}: undefined, // ${unsupportedGeneratedComment(property, "formula row mapping")}`;
|
||||
}
|
||||
} else {
|
||||
propertyCode = `${property.name}: undefined, // ${unsupportedGeneratedComment(property, "row mapping")}`;
|
||||
}
|
||||
|
||||
if (comments.length > 0) {
|
||||
propertyCode = propertyCode + ` // ${comments.join(", ")}`;
|
||||
}
|
||||
code.push(propertyCode);
|
||||
}
|
||||
|
||||
function formToDto({ code, property }: { code: string[]; property: PropertyWithDetails }) {
|
||||
if (!isWritableProperty(property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let propertyCode = "";
|
||||
let comments: string[] = [];
|
||||
comments.push(property.isRequired ? "required" : "optional");
|
||||
if (property.type === PropertyType.TEXT) {
|
||||
propertyCode = `${property.name}: FormHelper.getText(form, "${property.name}"),`;
|
||||
} else if (property.type === PropertyType.NUMBER) {
|
||||
propertyCode = `${property.name}: FormHelper.getNumber(form, "${property.name}"),`;
|
||||
} else if (property.type === PropertyType.DATE) {
|
||||
propertyCode = `${property.name}: FormHelper.getDate(form, "${property.name}"),`;
|
||||
} else if (property.type === PropertyType.SELECT) {
|
||||
propertyCode = `${property.name}: FormHelper.getText(form, "${property.name}"),`;
|
||||
} else if (property.type === PropertyType.BOOLEAN) {
|
||||
propertyCode = `${property.name}: FormHelper.getBoolean(form, "${property.name}"),`;
|
||||
} else if (property.type === PropertyType.MEDIA) {
|
||||
if (PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Max) === 1) {
|
||||
propertyCode = `${property.name}: FormHelper.getFormFirstMedia(form, "${property.name}"),`;
|
||||
} else {
|
||||
propertyCode = `${property.name}: FormHelper.getFormMedia(form, "${property.name}"),`;
|
||||
}
|
||||
} else if (property.type === PropertyType.MULTI_SELECT) {
|
||||
propertyCode = `${property.name}: FormHelper.getMultiple(form, "${property.name}"),`;
|
||||
} else if (property.type === PropertyType.RANGE_NUMBER) {
|
||||
propertyCode = `${property.name}: FormHelper.getNumberRange(form, "${property.name}"),`;
|
||||
} else if (property.type === PropertyType.RANGE_DATE) {
|
||||
propertyCode = `${property.name}: FormHelper.getDateRange(form, "${property.name}"),`;
|
||||
} else if (property.type === PropertyType.MULTI_TEXT) {
|
||||
propertyCode = `${property.name}: FormHelper.getMultiple(form, "${property.name}"),`;
|
||||
} else {
|
||||
propertyCode = `${property.name}: undefined, // ${unsupportedGeneratedComment(property, "form mapping")}`;
|
||||
}
|
||||
|
||||
if (comments.length > 0) {
|
||||
propertyCode = propertyCode + ` // ${comments.join(", ")}`;
|
||||
}
|
||||
code.push(propertyCode);
|
||||
}
|
||||
|
||||
function createDtoToRow({ code, property }: { code: string[]; property: PropertyWithDetails }) {
|
||||
if (!isWritableProperty(property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionalChaining = property.isRequired ? "" : "?";
|
||||
let propertyCode = ``;
|
||||
if (property.type === PropertyType.TEXT) {
|
||||
propertyCode = `{ name: "${property.name}", value: data.${property.name} },`;
|
||||
} else if (property.type === PropertyType.NUMBER) {
|
||||
propertyCode = `{ name: "${property.name}", value: data.${property.name}${optionalChaining}.toString() },`;
|
||||
} else if (property.type === PropertyType.DATE) {
|
||||
propertyCode = `{ name: "${property.name}", value: data.${property.name}${optionalChaining}.toISOString() },`;
|
||||
} else if (property.type === PropertyType.SELECT) {
|
||||
propertyCode = `{ name: "${property.name}", value: data.${property.name} },`;
|
||||
} else if (property.type === PropertyType.BOOLEAN) {
|
||||
propertyCode = `{ name: "${property.name}", value: data.${property.name}${optionalChaining}.toString() },`;
|
||||
} else if (property.type === PropertyType.MEDIA) {
|
||||
if (PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Max) === 1) {
|
||||
propertyCode = `{ name: "${property.name}", media: data.${property.name} ? [data.${property.name}] : [] },`;
|
||||
} else {
|
||||
propertyCode = `{ name: "${property.name}", media: data.${property.name} },`;
|
||||
}
|
||||
} else if (property.type === PropertyType.MULTI_SELECT) {
|
||||
propertyCode = `{ name: "${property.name}", multiple: data.${property.name} },`;
|
||||
} else if (property.type === PropertyType.RANGE_NUMBER) {
|
||||
propertyCode = `{ name: "${property.name}", range: data.${property.name} },`;
|
||||
} else if (property.type === PropertyType.RANGE_DATE) {
|
||||
propertyCode = `{ name: "${property.name}", range: data.${property.name} },`;
|
||||
} else if (property.type === PropertyType.MULTI_TEXT) {
|
||||
propertyCode = `{ name: "${property.name}", multiple: data.${property.name} },`;
|
||||
} else {
|
||||
propertyCode = `// ${unsupportedGeneratedComment(property, "create mapping")}`;
|
||||
}
|
||||
code.push(propertyCode);
|
||||
}
|
||||
|
||||
function updateDtoToRow({ code, property }: { code: string[]; property: PropertyWithDetails }) {
|
||||
if (!isWritableProperty(property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let propertyCode = `if (data.${property.name} !== undefined) {\n`;
|
||||
|
||||
if (property.type === PropertyType.TEXT) {
|
||||
propertyCode += `values.push({ name: "${property.name}", textValue: data.${property.name} });`;
|
||||
} else if (property.type === PropertyType.NUMBER) {
|
||||
propertyCode += `values.push({ name: "${property.name}", numberValue: data.${property.name} });`;
|
||||
} else if (property.type === PropertyType.DATE) {
|
||||
propertyCode += `values.push({ name: "${property.name}", dateValue: data.${property.name} });`;
|
||||
} else if (property.type === PropertyType.SELECT) {
|
||||
propertyCode += `values.push({ name: "${property.name}", textValue: data.${property.name} });`;
|
||||
} else if (property.type === PropertyType.BOOLEAN) {
|
||||
propertyCode += `values.push({ name: "${property.name}", booleanValue: data.${property.name} });`;
|
||||
} else if (property.type === PropertyType.MEDIA) {
|
||||
if (PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Max) === 1) {
|
||||
propertyCode += `values.push({ name: "${property.name}", media: data.${property.name} ? [data.${property.name} as RowMedia] : [] });`;
|
||||
} else {
|
||||
propertyCode += `values.push({ name: "${property.name}", media: data.${property.name} ? data.${property.name} as RowMedia[] : [] });`;
|
||||
}
|
||||
} else if (property.type === PropertyType.MULTI_SELECT) {
|
||||
propertyCode += `values.push({ name: "${property.name}", multiple: data.${property.name} });`;
|
||||
} else if (property.type === PropertyType.RANGE_NUMBER) {
|
||||
propertyCode += `values.push({ name: "${property.name}", range: data.${property.name} });`;
|
||||
} else if (property.type === PropertyType.RANGE_DATE) {
|
||||
propertyCode += `values.push({ name: "${property.name}", range: data.${property.name} });`;
|
||||
} else if (property.type === PropertyType.MULTI_TEXT) {
|
||||
propertyCode += `values.push({ name: "${property.name}", multiple: data.${property.name} });`;
|
||||
} else {
|
||||
propertyCode += `// ${unsupportedGeneratedComment(property, "update mapping")}`;
|
||||
}
|
||||
|
||||
propertyCode = propertyCode + `\n}`;
|
||||
|
||||
code.push(propertyCode);
|
||||
}
|
||||
|
||||
function getColor(color: number) {
|
||||
return "Colors." + Colors[color];
|
||||
}
|
||||
|
||||
function uiForm({ code, imports, property, index }: { code: string[]; imports: string[]; property: PropertyWithDetails; index: number }) {
|
||||
const titleExpression = CodeGeneratorHelper.getPropertyTitleExpression(property);
|
||||
if (property.type === PropertyType.FORMULA) {
|
||||
code.push(`{item && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium text-muted-foreground">{${titleExpression}}</div>
|
||||
<div className="rounded-md border bg-muted/40 px-3 py-2 text-sm">{item.${property.name}?.toString() ?? ""}</div>
|
||||
</div>
|
||||
)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!property.showInCreate) {
|
||||
// render if row exists only
|
||||
code.push(`{item && (`);
|
||||
}
|
||||
|
||||
const props: string[] = [];
|
||||
if (property.isRequired) {
|
||||
props.push(`required`);
|
||||
}
|
||||
if (index === 0) {
|
||||
props.push(`autoFocus`);
|
||||
}
|
||||
props.push("disabled={isDisabled()}");
|
||||
|
||||
let min = PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Min) ?? 0;
|
||||
let max = PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Max) ?? 0;
|
||||
|
||||
if (property.type === PropertyType.TEXT) {
|
||||
let defaultValue = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.DefaultValue);
|
||||
let value = `item?.${property.name}`;
|
||||
if (defaultValue) {
|
||||
value = `item?.${property.name} ?? (!item ? "${defaultValue}" : undefined)`;
|
||||
}
|
||||
props.push(`defaultValue={${value}}`);
|
||||
let placeholder = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.Placeholder);
|
||||
if (placeholder) {
|
||||
props.push(`placeholder="${placeholder}"`);
|
||||
}
|
||||
if (min > 0) {
|
||||
props.push(`minLength={${min}}`);
|
||||
}
|
||||
if (max > 0) {
|
||||
props.push(`maxLength={${max}}`);
|
||||
}
|
||||
let rows = property.attributes.find((f) => f.name === PropertyAttributeName.Rows);
|
||||
if (rows) {
|
||||
props.push(`rows={${PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Rows)}}`);
|
||||
}
|
||||
let pattern = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.Pattern);
|
||||
if (pattern) {
|
||||
props.push(`pattern="${pattern}"`);
|
||||
}
|
||||
let editor = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.Editor);
|
||||
if (editor === "monaco") {
|
||||
props.push(`editor="monaco" editorLanguage="markdown"`);
|
||||
} else if (editor === "wysiwyg") {
|
||||
props.push(`editor="wysiwyg" autoFocus={${index === 0}}`);
|
||||
}
|
||||
if (!property.subtype || property.subtype === "singleLine") {
|
||||
imports.push(`import InputText from "~/components/ui/input/InputText";`);
|
||||
code.push(`<InputText name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
} else {
|
||||
imports.push(`import InputTextSubtype from "~/components/ui/input/subtypes/InputTextSubtype";`);
|
||||
code.push(`<InputTextSubtype subtype="${property.subtype}" name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
}
|
||||
} else if (property.type === PropertyType.NUMBER) {
|
||||
imports.push(`import InputNumber from "~/components/ui/input/InputNumber";`);
|
||||
let defaultValue = PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.DefaultValue);
|
||||
let value = `item?.${property.name}`;
|
||||
if (defaultValue) {
|
||||
value = `item?.${property.name} ?? (!item ? ${defaultValue} : undefined)`;
|
||||
}
|
||||
props.push(`defaultValue={${value}}`);
|
||||
if (min > 0) {
|
||||
props.push(`min={${min}}`);
|
||||
}
|
||||
if (max > 0) {
|
||||
props.push(`max={${max}}`);
|
||||
}
|
||||
let step = PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Step);
|
||||
if (step) {
|
||||
props.push(`step="${step}"`);
|
||||
}
|
||||
code.push(`<InputNumber name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
} else if (property.type === PropertyType.DATE) {
|
||||
imports.push(`import InputDate from "~/components/ui/input/InputDate";`);
|
||||
code.push(`<InputDate name="${property.name}" title={${titleExpression}} defaultValue={item?.${property.name}} ${props.join(" ")} />`);
|
||||
} else if (property.type === PropertyType.SELECT) {
|
||||
let defaultValue = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.DefaultValue);
|
||||
let value = `item?.${property.name}`;
|
||||
if (defaultValue) {
|
||||
value = `item?.${property.name} ?? (!item ? "${defaultValue}" : undefined)`;
|
||||
}
|
||||
props.push(`defaultValue={${value}}`);
|
||||
const withColors = property.options.filter((f) => f.color).length > 0;
|
||||
|
||||
imports.push(`import { Colors } from "~/application/enums/shared/Colors";`);
|
||||
props.push(
|
||||
`options={[${property.options.map((option) => {
|
||||
return `{ name: ${option.name === null ? `"${option.value}"` : `"${option.name}"`}, value: "${option.value}", color: ${getColor(option.color)} }`;
|
||||
})}]}`
|
||||
);
|
||||
if (!property.subtype || property.subtype === "dropdown") {
|
||||
// props.push(`withSearch={false}`);
|
||||
props.push(`withColors={${withColors ? "true" : "false"}}`);
|
||||
imports.push(`import InputSelect from "~/components/ui/input/InputSelect";`);
|
||||
code.push(`<InputSelect name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
} else if (property.subtype === "radioGroupCards") {
|
||||
imports.push(`import InputRadioGroupCards from "~/components/ui/input/InputRadioGroupCards";`);
|
||||
code.push(`<InputRadioGroupCards name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
}
|
||||
} else if (property.type === PropertyType.BOOLEAN) {
|
||||
imports.push(`import InputCheckbox from "~/components/ui/input/InputCheckbox";`);
|
||||
let defaultValue = PropertyAttributeHelper.getPropertyAttributeValue_Boolean(property, PropertyAttributeName.DefaultValue);
|
||||
let value = `item?.${property.name}`;
|
||||
if (defaultValue) {
|
||||
value = `item?.${property.name} ?? (!item ? ${defaultValue} : undefined)`;
|
||||
}
|
||||
props.push(`value={${value}}`);
|
||||
code.push(`<InputCheckbox name="${property.name}" title={${titleExpression}} asToggle ${props.join(" ")} />`);
|
||||
} else if (property.type === PropertyType.MEDIA) {
|
||||
imports.push(`import InputMedia from "~/components/ui/input/InputMedia";`);
|
||||
if (PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Max) === 1) {
|
||||
props.push(`initialMedia={item?.${property.name} ? [item?.${property.name}] : []}`);
|
||||
} else {
|
||||
props.push(`initialMedia={item?.${property.name}}`);
|
||||
}
|
||||
let acceptFileTypes = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.AcceptFileTypes);
|
||||
if (min > 0) {
|
||||
props.push(`min={${min}}`);
|
||||
}
|
||||
if (max > 0) {
|
||||
props.push(`max={${max}}`);
|
||||
}
|
||||
if (acceptFileTypes) {
|
||||
props.push(`accept="${acceptFileTypes}"`);
|
||||
}
|
||||
let maxSize = PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.MaxSize) ?? 0;
|
||||
if (maxSize > 0) {
|
||||
props.push(`maxSize={${maxSize}}`);
|
||||
}
|
||||
code.push(`<InputMedia name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
} else if (property.type === PropertyType.MULTI_SELECT) {
|
||||
props.push(`value={item?.${property.name}}`);
|
||||
|
||||
imports.push(`import { Colors } from "~/application/enums/shared/Colors";`);
|
||||
props.push(
|
||||
`options={[${property.options.map((option) => {
|
||||
return `{ name: ${option.name === null ? `"${option.value}"` : `"${option.name}"`}, value: "${option.value}", color: ${getColor(option.color)} }`;
|
||||
})}]}`
|
||||
);
|
||||
if (!property.subtype || property.subtype === "combobox") {
|
||||
// imports.push(`import InputCombobox from "~/components/ui/input/InputCombobox";`);
|
||||
imports.push(`import PropertyMultiSelector from "~/components/entities/properties/PropertyMultiSelector";`);
|
||||
code.push(`<PropertyMultiSelector name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
} else if (property.subtype === "checkboxCards") {
|
||||
imports.push(`import PropertyMultiSelector from "~/components/entities/properties/PropertyMultiSelector";`);
|
||||
code.push(`<PropertyMultiSelector subtype="checkboxCards" name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
}
|
||||
} else if (property.type === PropertyType.RANGE_NUMBER) {
|
||||
imports.push(`import InputRangeNumber from "~/components/ui/input/ranges/InputRangeNumber";`);
|
||||
code.push(
|
||||
`<InputRangeNumber name="${property.name}" title={${titleExpression}} ${props.join(" ")} defaultValueMin={item?.${
|
||||
property.name
|
||||
}?.numberMin ? Number(item.${property.name}.numberMin) : undefined} defaultValueMax={item?.${property.name}?.numberMax ? Number(item.${
|
||||
property.name
|
||||
}.numberMax) : undefined} />`
|
||||
);
|
||||
} else if (property.type === PropertyType.RANGE_DATE) {
|
||||
imports.push(`import InputRangeDate from "~/components/ui/input/ranges/InputRangeDate";`);
|
||||
code.push(
|
||||
`<InputRangeDate name="${property.name}" title={${titleExpression}} ${props.join(" ")} defaultValueMin={item?.${
|
||||
property.name
|
||||
}?.dateMin} defaultValueMax={item?.${property.name}?.dateMax} />`
|
||||
);
|
||||
} else if (property.type === PropertyType.MULTI_TEXT) {
|
||||
imports.push(`import InputMultiText from "~/components/ui/input/InputMultiText";`);
|
||||
let separator = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.Separator);
|
||||
if (separator) {
|
||||
props.push(`separator="${separator}"`);
|
||||
}
|
||||
props.push(`value={item?.${property.name}}`);
|
||||
code.push(`<InputMultiText name="${property.name}" title={${titleExpression}} ${props.join(" ")} />`);
|
||||
} else {
|
||||
code.push(`{/* ${unsupportedGeneratedComment(property, "input")} */}`);
|
||||
}
|
||||
|
||||
if (!property.showInCreate) {
|
||||
// render if row exists only
|
||||
code.push(`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function uiCell({ code, imports, property }: { code: string[]; imports: string[]; property: PropertyWithDetails }) {
|
||||
const titleExpression = CodeGeneratorHelper.getPropertyTitleExpression(property);
|
||||
const props: string[] = [];
|
||||
if (property.type === PropertyType.TEXT) {
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <div className="max-w-sm truncate">{item.${property.name}}</div>,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.NUMBER) {
|
||||
imports.push(`import RowNumberCell from "~/components/entities/rows/cells/RowNumberCell";`);
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.FormatNumber);
|
||||
if (format) {
|
||||
props.push(`format="${format}"`);
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowNumberCell value={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.DATE) {
|
||||
imports.push(`import RowDateCell from "~/components/entities/rows/cells/RowDateCell";`);
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.FormatDate);
|
||||
if (format) {
|
||||
props.push(`format="${format}"`);
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowDateCell value={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.SELECT) {
|
||||
imports.push(`import RowSelectedOptionCell from "~/components/entities/rows/cells/RowSelectedOptionCell";`);
|
||||
// const withColors = property.options.filter((f) => f.color).length > 0;
|
||||
// if (withColors) {
|
||||
imports.push(`import { Colors } from "~/application/enums/shared/Colors";`);
|
||||
// }
|
||||
props.push(
|
||||
`options={[${property.options.map((option) => {
|
||||
return `{ name: ${option.name === null ? "null" : `"${option.name}"`}, value: "${option.value}", color: ${getColor(option.color)} }`;
|
||||
})}]}`
|
||||
);
|
||||
let display: SelectOptionsDisplay = "Value";
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.SelectOptions);
|
||||
if (format) {
|
||||
display = format as SelectOptionsDisplay;
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowSelectedOptionCell value={item?.${property.name}} display="${display}" ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.BOOLEAN) {
|
||||
imports.push(`import RowBooleanCell from "~/components/entities/rows/cells/RowBooleanCell";`);
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.FormatBoolean);
|
||||
if (format) {
|
||||
props.push(`format="${format}"`);
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowBooleanCell value={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.MEDIA) {
|
||||
imports.push(`import RowMediaCell from "~/components/entities/rows/cells/RowMediaCell";`);
|
||||
if (PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Max) === 1) {
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowMediaCell media={item.${property.name} ? [item.${property.name}] : []} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else {
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowMediaCell media={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
}
|
||||
} else if (property.type === PropertyType.MULTI_SELECT) {
|
||||
imports.push(`import PropertyMultipleValueBadge from "~/components/entities/properties/PropertyMultipleValueBadge";`);
|
||||
props.push(
|
||||
`options={[${property.options.map((option) => {
|
||||
return `{ name: ${option.name === null ? "null" : `"${option.name}"`}, value: "${option.value}" }`;
|
||||
})}]}`
|
||||
);
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <PropertyMultipleValueBadge values={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.RANGE_NUMBER) {
|
||||
imports.push(`import RowRangeNumberCell from "~/components/entities/rows/cells/RowRangeNumberCell";`);
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.FormatNumber);
|
||||
if (format) {
|
||||
props.push(`format="${format}"`);
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowRangeNumberCell value={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.RANGE_DATE) {
|
||||
imports.push(`import RowRangeDateCell from "~/components/entities/rows/cells/RowRangeDateCell";`);
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.FormatNumber);
|
||||
if (format) {
|
||||
props.push(`format="${format}"`);
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowRangeDateCell value={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.MULTI_TEXT) {
|
||||
imports.push(`import PropertyMultipleValueBadge from "~/components/entities/properties/PropertyMultipleValueBadge";`);
|
||||
props.push(
|
||||
`options={[${property.options.map((option) => {
|
||||
return `{ name: ${option.name === null ? "null" : `"${option.name}"`}, value: "${option.value}" }`;
|
||||
})}]}`
|
||||
);
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <PropertyMultipleValueBadge values={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (property.type === PropertyType.FORMULA) {
|
||||
const resultAs = getFormulaResultAs(property);
|
||||
if (resultAs === "number") {
|
||||
imports.push(`import RowNumberCell from "~/components/entities/rows/cells/RowNumberCell";`);
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.FormatNumber);
|
||||
if (format) {
|
||||
props.push(`format="${format}"`);
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowNumberCell value={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (resultAs === "date") {
|
||||
imports.push(`import RowDateCell from "~/components/entities/rows/cells/RowDateCell";`);
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.FormatDate);
|
||||
if (format) {
|
||||
props.push(`format="${format}"`);
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowDateCell value={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else if (resultAs === "boolean") {
|
||||
imports.push(`import RowBooleanCell from "~/components/entities/rows/cells/RowBooleanCell";`);
|
||||
let format = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.FormatBoolean);
|
||||
if (format) {
|
||||
props.push(`format="${format}"`);
|
||||
}
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <RowBooleanCell value={item.${property.name}} ${props.join(" ")} />,
|
||||
},`);
|
||||
} else {
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: (item) => <div className="max-w-sm truncate">{item.${property.name}}</div>,
|
||||
},`);
|
||||
}
|
||||
} else {
|
||||
code.push(`{
|
||||
name: "${property.name}",
|
||||
title: ${titleExpression},
|
||||
value: () => null,
|
||||
},`);
|
||||
}
|
||||
}
|
||||
|
||||
function schema({ code, property }: { code: string[]; property: PropertyWithDetails }) {
|
||||
const typeMap: Record<PropertyType, string> = {
|
||||
[PropertyType.TEXT]: "String",
|
||||
[PropertyType.NUMBER]: "Decimal",
|
||||
[PropertyType.DATE]: "DateTime",
|
||||
[PropertyType.SELECT]: "String",
|
||||
[PropertyType.BOOLEAN]: "Boolean",
|
||||
[PropertyType.MEDIA]: "Json",
|
||||
[PropertyType.MULTI_SELECT]: "Json",
|
||||
[PropertyType.MULTI_TEXT]: "Json",
|
||||
[PropertyType.RANGE_NUMBER]: "Json",
|
||||
[PropertyType.RANGE_DATE]: "Json",
|
||||
[PropertyType.FORMULA]: "",
|
||||
};
|
||||
|
||||
const type = typeMap[property.type as PropertyType];
|
||||
if (type === undefined) {
|
||||
return;
|
||||
}
|
||||
if (property.type === PropertyType.FORMULA) {
|
||||
code.push(` // ${property.name} is a computed Formula field; no editable Prisma column is generated by default.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const suffix = property.isRequired ? "" : "?";
|
||||
const schemaString = ` ${property.name} ${type}${suffix}`;
|
||||
code.push(schemaString);
|
||||
}
|
||||
|
||||
export default {
|
||||
type,
|
||||
rowToDto,
|
||||
formToDto,
|
||||
createDtoToRow,
|
||||
updateDtoToRow,
|
||||
uiForm,
|
||||
uiCell,
|
||||
schema,
|
||||
isWritableProperty,
|
||||
};
|
||||
Reference in new issue
Block a user