@carlonicora/nextjs-jsonapi 1.110.0 → 1.111.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BlockNoteEditor-K7IIWM3S.mjs → BlockNoteEditor-A5LNFWHW.mjs} +2 -2
- package/dist/{BlockNoteEditor-7OKW6HHN.js → BlockNoteEditor-HNQNKCFP.js} +9 -9
- package/dist/{BlockNoteEditor-7OKW6HHN.js.map → BlockNoteEditor-HNQNKCFP.js.map} +1 -1
- package/dist/billing/index.js +299 -299
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-AKXUNE3A.js → chunk-DRJUN4R6.js} +22 -19
- package/dist/chunk-DRJUN4R6.js.map +1 -0
- package/dist/{chunk-TXVVZLUQ.mjs → chunk-LYLGU2BK.mjs} +67 -64
- package/dist/chunk-LYLGU2BK.mjs.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/help/index.js +30 -30
- package/dist/features/help/index.mjs +1 -1
- package/dist/scripts/generate-web-module/__tests__/container.spec.d.ts +2 -0
- package/dist/scripts/generate-web-module/__tests__/container.spec.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/container.spec.js +54 -0
- package/dist/scripts/generate-web-module/__tests__/container.spec.js.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/data-service.spec.d.ts +2 -0
- package/dist/scripts/generate-web-module/__tests__/data-service.spec.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/data-service.spec.js +19 -0
- package/dist/scripts/generate-web-module/__tests__/data-service.spec.js.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/editor.spec.d.ts +2 -0
- package/dist/scripts/generate-web-module/__tests__/editor.spec.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/editor.spec.js +35 -0
- package/dist/scripts/generate-web-module/__tests__/editor.spec.js.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/fixtures.d.ts +4 -0
- package/dist/scripts/generate-web-module/__tests__/fixtures.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/fixtures.js +45 -0
- package/dist/scripts/generate-web-module/__tests__/fixtures.js.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/model-interface.spec.d.ts +2 -0
- package/dist/scripts/generate-web-module/__tests__/model-interface.spec.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/model-interface.spec.js +74 -0
- package/dist/scripts/generate-web-module/__tests__/model-interface.spec.js.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/plumbing.spec.d.ts +2 -0
- package/dist/scripts/generate-web-module/__tests__/plumbing.spec.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/plumbing.spec.js +21 -0
- package/dist/scripts/generate-web-module/__tests__/plumbing.spec.js.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/selector.spec.d.ts +2 -0
- package/dist/scripts/generate-web-module/__tests__/selector.spec.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/selector.spec.js +25 -0
- package/dist/scripts/generate-web-module/__tests__/selector.spec.js.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/table-module-list.spec.d.ts +2 -0
- package/dist/scripts/generate-web-module/__tests__/table-module-list.spec.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/__tests__/table-module-list.spec.js +55 -0
- package/dist/scripts/generate-web-module/__tests__/table-module-list.spec.js.map +1 -0
- package/dist/scripts/generate-web-module/generator.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/generator.js +14 -11
- package/dist/scripts/generate-web-module/generator.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/container.template.d.ts +5 -1
- package/dist/scripts/generate-web-module/templates/components/container.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/container.template.js +43 -63
- package/dist/scripts/generate-web-module/templates/components/container.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/content.template.d.ts +7 -4
- package/dist/scripts/generate-web-module/templates/components/content.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/content.template.js +103 -14
- package/dist/scripts/generate-web-module/templates/components/content.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/editor.template.d.ts +1 -1
- package/dist/scripts/generate-web-module/templates/components/editor.template.js +139 -150
- package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/index.d.ts +0 -1
- package/dist/scripts/generate-web-module/templates/components/index.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/index.js +1 -3
- package/dist/scripts/generate-web-module/templates/components/index.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/list-container.template.js +2 -2
- package/dist/scripts/generate-web-module/templates/components/list.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/list.template.js +6 -1
- package/dist/scripts/generate-web-module/templates/components/list.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/selector.template.js +48 -52
- package/dist/scripts/generate-web-module/templates/components/selector.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/interface.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/interface.template.js +38 -11
- package/dist/scripts/generate-web-module/templates/data/interface.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/model.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/model.template.js +83 -26
- package/dist/scripts/generate-web-module/templates/data/model.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/service.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/data/service.template.js +21 -1
- package/dist/scripts/generate-web-module/templates/data/service.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/index.d.ts +1 -1
- package/dist/scripts/generate-web-module/templates/index.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/index.js +1 -2
- package/dist/scripts/generate-web-module/templates/index.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/module.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/module.template.js +11 -3
- package/dist/scripts/generate-web-module/templates/module.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/table-hook.template.js +31 -1
- package/dist/scripts/generate-web-module/templates/table-hook.template.js.map +1 -1
- package/dist/scripts/generate-web-module/transformers/import-resolver.d.ts +0 -1
- package/dist/scripts/generate-web-module/transformers/import-resolver.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/transformers/import-resolver.js +0 -1
- package/dist/scripts/generate-web-module/transformers/import-resolver.js.map +1 -1
- package/dist/scripts/generate-web-module/transformers/relationship-key.d.ts +5 -0
- package/dist/scripts/generate-web-module/transformers/relationship-key.d.ts.map +1 -0
- package/dist/scripts/generate-web-module/transformers/relationship-key.js +21 -0
- package/dist/scripts/generate-web-module/transformers/relationship-key.js.map +1 -0
- package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/transformers/relationship-resolver.js +2 -0
- package/dist/scripts/generate-web-module/transformers/relationship-resolver.js.map +1 -1
- package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts +21 -0
- package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/types/template-data.interface.d.ts +17 -0
- package/dist/scripts/generate-web-module/types/template-data.interface.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-web-module/__tests__/container.spec.ts +58 -0
- package/scripts/generate-web-module/__tests__/data-service.spec.ts +18 -0
- package/scripts/generate-web-module/__tests__/editor.spec.ts +36 -0
- package/scripts/generate-web-module/__tests__/fixtures.ts +43 -0
- package/scripts/generate-web-module/__tests__/model-interface.spec.ts +77 -0
- package/scripts/generate-web-module/__tests__/plumbing.spec.ts +21 -0
- package/scripts/generate-web-module/__tests__/selector.spec.ts +24 -0
- package/scripts/generate-web-module/__tests__/table-module-list.spec.ts +59 -0
- package/scripts/generate-web-module/generator.ts +14 -13
- package/scripts/generate-web-module/templates/components/container.template.ts +51 -66
- package/scripts/generate-web-module/templates/components/content.template.ts +107 -15
- package/scripts/generate-web-module/templates/components/editor.template.ts +140 -152
- package/scripts/generate-web-module/templates/components/index.ts +0 -1
- package/scripts/generate-web-module/templates/components/list-container.template.ts +2 -2
- package/scripts/generate-web-module/templates/components/list.template.ts +6 -1
- package/scripts/generate-web-module/templates/components/selector.template.ts +48 -52
- package/scripts/generate-web-module/templates/data/interface.template.ts +44 -12
- package/scripts/generate-web-module/templates/data/model.template.ts +88 -28
- package/scripts/generate-web-module/templates/data/service.template.ts +23 -1
- package/scripts/generate-web-module/templates/index.ts +0 -1
- package/scripts/generate-web-module/templates/module.template.ts +13 -3
- package/scripts/generate-web-module/templates/table-hook.template.ts +31 -2
- package/scripts/generate-web-module/transformers/import-resolver.ts +0 -2
- package/scripts/generate-web-module/transformers/relationship-key.ts +18 -0
- package/scripts/generate-web-module/transformers/relationship-resolver.ts +2 -0
- package/scripts/generate-web-module/types/json-schema.interface.ts +9 -0
- package/scripts/generate-web-module/types/template-data.interface.ts +7 -0
- package/src/features/notification/components/modals/NotificationModal.tsx +2 -5
- package/src/features/notification/contexts/NotificationContext.tsx +29 -13
- package/src/features/notification/contexts/__tests__/NotificationContext.spec.tsx +70 -0
- package/dist/chunk-AKXUNE3A.js.map +0 -1
- package/dist/chunk-TXVVZLUQ.mjs.map +0 -1
- package/dist/scripts/generate-web-module/templates/components/details.template.d.ts +0 -14
- package/dist/scripts/generate-web-module/templates/components/details.template.d.ts.map +0 -1
- package/dist/scripts/generate-web-module/templates/components/details.template.js +0 -112
- package/dist/scripts/generate-web-module/templates/components/details.template.js.map +0 -1
- package/scripts/generate-web-module/templates/components/details.template.ts +0 -116
- /package/dist/{BlockNoteEditor-K7IIWM3S.mjs.map → BlockNoteEditor-A5LNFWHW.mjs.map} +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { makeFrontendData } from "./fixtures";
|
|
3
|
+
import { generateContainerTemplate } from "../templates/components/container.template";
|
|
4
|
+
import { generateListContainerTemplate } from "../templates/components/list-container.template";
|
|
5
|
+
import { generateContentTemplate } from "../templates/components/content.template";
|
|
6
|
+
|
|
7
|
+
describe("container template", () => {
|
|
8
|
+
it("uses RoundPageContainer with tabs and id, not details prop, and renders Content", () => {
|
|
9
|
+
const out = generateContainerTemplate(makeFrontendData());
|
|
10
|
+
expect(out).toContain("tabs: Tab[]");
|
|
11
|
+
expect(out).toContain("id={widget.id}");
|
|
12
|
+
expect(out).not.toContain("details={");
|
|
13
|
+
expect(out).not.toContain("WidgetDetails");
|
|
14
|
+
expect(out).toContain("<WidgetContent widget={widget} />");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("emits an Activity tab by default and no Tasks tab", () => {
|
|
18
|
+
const out = generateContainerTemplate(makeFrontendData());
|
|
19
|
+
expect(out).toContain("<ActivityFeed module={Modules.Widget} entityId={widget.id} />");
|
|
20
|
+
expect(out).not.toContain("TaskProvider");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("omits the Activity tab when containerTabs.activity is false", () => {
|
|
24
|
+
const out = generateContainerTemplate(makeFrontendData({ containerTabs: { activity: false, relations: [] } }));
|
|
25
|
+
expect(out).not.toContain("ActivityFeed");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("emits a relation tab when containerTabs.relations has an entry", () => {
|
|
29
|
+
const out = generateContainerTemplate(makeFrontendData({
|
|
30
|
+
containerTabs: { activity: true, relations: [{ module: "Quote", listProp: "widget", directory: "sales" }] },
|
|
31
|
+
}));
|
|
32
|
+
expect(out).toContain('import QuoteList from "@/features/sales/quote/components/lists/QuoteList"');
|
|
33
|
+
expect(out).toContain("key: Modules.Quote");
|
|
34
|
+
expect(out).toContain("<QuoteList widget={widget} />");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("gates default export on hasPermissionToModule", () => {
|
|
38
|
+
const out = generateContainerTemplate(makeFrontendData());
|
|
39
|
+
expect(out).toContain("hasPermissionToModule({ module: Modules.Widget, action: Action.Delete, data: widget })");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("list-container template", () => {
|
|
44
|
+
it("always renders fullWidth", () => {
|
|
45
|
+
const out = generateListContainerTemplate(makeFrontendData());
|
|
46
|
+
expect(out).toContain("<RoundPageContainer module={Modules.Widget} fullWidth>");
|
|
47
|
+
expect(out).toContain("<WidgetList fullWidth />");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("content template", () => {
|
|
52
|
+
it("is always generated (returns a string) and renders AttributeElement", () => {
|
|
53
|
+
const out = generateContentTemplate(makeFrontendData());
|
|
54
|
+
expect(typeof out).toBe("string");
|
|
55
|
+
expect(out).toContain("export function WidgetContent");
|
|
56
|
+
expect(out).toContain("AttributeElement");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { makeFrontendData } from "./fixtures";
|
|
3
|
+
import { generateServiceTemplate } from "../templates/data/service.template";
|
|
4
|
+
|
|
5
|
+
describe("frontend data service template", () => {
|
|
6
|
+
it("applies inclusions inside findOne", () => {
|
|
7
|
+
const out = generateServiceTemplate(makeFrontendData());
|
|
8
|
+
const findOne = out.split("static async findOne")[1].split("static async")[0];
|
|
9
|
+
expect(findOne).toContain("limitToFields");
|
|
10
|
+
expect(findOne).toContain("limitToType");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("emits a patch method", () => {
|
|
14
|
+
const out = generateServiceTemplate(makeFrontendData());
|
|
15
|
+
expect(out).toContain("static async patch(params: { id: string } & Partial<Omit<WidgetInput, \"id\">>)");
|
|
16
|
+
expect(out).toContain("method: HttpMethod.PATCH");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { makeFrontendData } from "./fixtures";
|
|
3
|
+
import { generateEditorTemplate } from "../templates/components/editor.template";
|
|
4
|
+
|
|
5
|
+
describe("editor template — EditorSheet", () => {
|
|
6
|
+
it("uses EditorSheet and not the legacy Dialog stack", () => {
|
|
7
|
+
const out = generateEditorTemplate(makeFrontendData());
|
|
8
|
+
expect(out).toContain("EditorSheet");
|
|
9
|
+
expect(out).not.toContain("CommonEditorButtons");
|
|
10
|
+
expect(out).not.toContain("CommonEditorDiscardDialog");
|
|
11
|
+
expect(out).not.toContain("useEditorDialog");
|
|
12
|
+
expect(out).not.toMatch(/DialogContent/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("gates the default export on hasPermissionToModule", () => {
|
|
16
|
+
const out = generateEditorTemplate(makeFrontendData());
|
|
17
|
+
expect(out).toContain("useCurrentUserContext");
|
|
18
|
+
expect(out).toContain("hasPermissionToModule({ module: Modules.Widget, action, data: props.widget })");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("wraps formSchema in useMemo and defaults in useCallback", () => {
|
|
22
|
+
const out = generateEditorTemplate(makeFrontendData());
|
|
23
|
+
expect(out).toContain("const formSchema = useMemo(");
|
|
24
|
+
expect(out).toContain("const getDefaultValues = useCallback(");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("passes EditorSheet lifecycle props and onSubmit returns the saved entity", () => {
|
|
28
|
+
const out = generateEditorTemplate(makeFrontendData());
|
|
29
|
+
expect(out).toContain("onRevalidate={revalidatePaths}");
|
|
30
|
+
expect(out).toContain("onReset={getDefaultValues}");
|
|
31
|
+
expect(out).toContain("module={Modules.Widget}");
|
|
32
|
+
// onSubmit returns the service result (no manual setOpen/errorToast)
|
|
33
|
+
expect(out).not.toContain("setOpen(false)");
|
|
34
|
+
expect(out).not.toContain("errorToast(");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { FrontendRelationship, FrontendTemplateData } from "../types/template-data.interface";
|
|
2
|
+
|
|
3
|
+
export function makeRelationship(o: Partial<FrontendRelationship> = {}): FrontendRelationship {
|
|
4
|
+
return {
|
|
5
|
+
name: "Account", variant: undefined, alias: undefined, directory: "crm",
|
|
6
|
+
single: true, nullable: false, isFoundation: false,
|
|
7
|
+
formFieldId: "account", formFieldIdPlural: "accounts", payloadFieldId: "accountId",
|
|
8
|
+
selectorComponent: "AccountSelector", zodSchema: "entityObjectSchema",
|
|
9
|
+
importPath: "@/features/crm/account/components/forms/AccountSelector",
|
|
10
|
+
interfaceImportPath: "@/features/crm/account/data/AccountInterface",
|
|
11
|
+
serviceImportPath: "@/features/crm/account/data/AccountService",
|
|
12
|
+
interfaceName: "AccountInterface", modelKebab: "account",
|
|
13
|
+
fields: undefined, targetHasName: true, dtoKey: "", showInTable: false,
|
|
14
|
+
...o,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function makeFrontendData(o: Partial<FrontendTemplateData> = {}): FrontendTemplateData {
|
|
19
|
+
return {
|
|
20
|
+
names: {
|
|
21
|
+
pascalCase: "Widget", camelCase: "widget", kebabCase: "widget",
|
|
22
|
+
pluralPascal: "Widgets", pluralCamel: "widgets", pluralKebab: "widgets",
|
|
23
|
+
titleCase: "Widget", pluralTitleCase: "Widgets",
|
|
24
|
+
},
|
|
25
|
+
moduleId: "00000000-0000-4000-a000-000000000001",
|
|
26
|
+
endpoint: "widgets", targetDir: "features/demo", importTargetDir: "demo",
|
|
27
|
+
extendsContent: false,
|
|
28
|
+
fields: [
|
|
29
|
+
{ name: "name", type: "string", tsType: "string", zodSchema: "z.string()", formComponent: "FormInput", nullable: false, isContentField: false },
|
|
30
|
+
{ name: "due_date", type: "date", tsType: "Date", zodSchema: "z.date().optional()", formComponent: "DatePicker", nullable: true, isContentField: false },
|
|
31
|
+
],
|
|
32
|
+
relationships: [],
|
|
33
|
+
i18nKeys: { moduleName: "widget", fields: {}, relationships: {}, type: { singular: "Widget", plural: "Widgets", icuPlural: "" } },
|
|
34
|
+
imports: { models: [], selectors: [], library: [] },
|
|
35
|
+
tableFieldNames: [],
|
|
36
|
+
relationshipServiceMethods: [],
|
|
37
|
+
featureId: undefined,
|
|
38
|
+
displayProp: "name",
|
|
39
|
+
containerTabs: { activity: true, relations: [] },
|
|
40
|
+
relatedInclusions: [],
|
|
41
|
+
...o,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { makeFrontendData, makeRelationship } from "./fixtures";
|
|
3
|
+
import { generateModelTemplate } from "../templates/data/model.template";
|
|
4
|
+
import { generateInterfaceTemplate } from "../templates/data/interface.template";
|
|
5
|
+
|
|
6
|
+
describe("model template — dates", () => {
|
|
7
|
+
it("imports formatLocalDate and uses it for date fields in createJsonApi", () => {
|
|
8
|
+
const out = generateModelTemplate(makeFrontendData());
|
|
9
|
+
expect(out).toContain("formatLocalDate");
|
|
10
|
+
expect(out).toContain("response.data.attributes.due_date = formatLocalDate(data.due_date)");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("model template — relationship wire keys", () => {
|
|
15
|
+
it("uses dtoKey verbatim for rehydrate and createJsonApi", () => {
|
|
16
|
+
const out = generateModelTemplate(makeFrontendData({
|
|
17
|
+
relationships: [makeRelationship({ name: "PipelineStage", single: true, dtoKey: "pipelinestage", interfaceName: "PipelineStageInterface", modelKebab: "pipeline-stage", selectorComponent: "PipelineStageSelector", interfaceImportPath: "@/features/crm/pipeline-stage/data/PipelineStageInterface" })],
|
|
18
|
+
}));
|
|
19
|
+
expect(out).toContain('"pipelinestage"');
|
|
20
|
+
expect(out).toContain("response.data.relationships.pipelinestage");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("emits per-item meta array for many-with-edge-fields", () => {
|
|
24
|
+
const out = generateModelTemplate(makeFrontendData({
|
|
25
|
+
relationships: [makeRelationship({
|
|
26
|
+
name: "Item", single: false, dtoKey: "estimateitems", alias: "EstimateItem",
|
|
27
|
+
interfaceName: "ItemInterface", modelKebab: "item", selectorComponent: "ItemMultiSelector",
|
|
28
|
+
interfaceImportPath: "@/features/plm/item/data/ItemInterface",
|
|
29
|
+
fields: [{ name: "quantity", type: "number", tsType: "number", zodSchema: "z.number()", formComponent: "FormInputNumber", nullable: false, isContentField: false }],
|
|
30
|
+
})],
|
|
31
|
+
}));
|
|
32
|
+
expect(out).toContain("data.estimateItemMeta?.find");
|
|
33
|
+
expect(out).toContain("response.data.relationships.estimateitems");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("model references and imports the named RelationshipMeta interface", () => {
|
|
37
|
+
const out = generateModelTemplate(makeFrontendData({
|
|
38
|
+
relationships: [makeRelationship({
|
|
39
|
+
name: "Item", single: false, dtoKey: "estimateitems", alias: "EstimateItem",
|
|
40
|
+
interfaceName: "ItemInterface", modelKebab: "item", selectorComponent: "ItemMultiSelector",
|
|
41
|
+
interfaceImportPath: "@/features/plm/item/data/ItemInterface",
|
|
42
|
+
fields: [{ name: "quantity", type: "number", tsType: "number", zodSchema: "z.number()", formComponent: "FormInputNumber", nullable: false, isContentField: false }],
|
|
43
|
+
})],
|
|
44
|
+
}));
|
|
45
|
+
expect(out).toContain("EstimateItemRelationshipMeta");
|
|
46
|
+
expect(out).toContain("ItemInterface & EstimateItemRelationshipMeta");
|
|
47
|
+
// imported from the module's own interface file (Widget)
|
|
48
|
+
expect(out).toMatch(/import \{[^}]*EstimateItemRelationshipMeta[^}]*\} from "@\/features\/demo\/widget\/data\/WidgetInterface"/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("interface template", () => {
|
|
53
|
+
it("emits a named RelationshipMeta interface and a meta array in Input", () => {
|
|
54
|
+
const out = generateInterfaceTemplate(makeFrontendData({
|
|
55
|
+
relationships: [makeRelationship({
|
|
56
|
+
name: "Item", single: false, dtoKey: "estimateitems", alias: "EstimateItem",
|
|
57
|
+
interfaceName: "ItemInterface", modelKebab: "item",
|
|
58
|
+
interfaceImportPath: "@/features/plm/item/data/ItemInterface",
|
|
59
|
+
fields: [{ name: "quantity", type: "number", tsType: "number", zodSchema: "z.number()", formComponent: "FormInputNumber", nullable: false, isContentField: false }],
|
|
60
|
+
})],
|
|
61
|
+
}));
|
|
62
|
+
expect(out).toContain("export interface EstimateItemRelationshipMeta");
|
|
63
|
+
expect(out).toContain("estimateItemMeta?:");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("keeps readOnly fields in the interface but out of Input", () => {
|
|
67
|
+
const out = generateInterfaceTemplate(makeFrontendData({
|
|
68
|
+
fields: [
|
|
69
|
+
{ name: "name", type: "string", tsType: "string", zodSchema: "z.string()", formComponent: "FormInput", nullable: false, isContentField: false },
|
|
70
|
+
{ name: "effective_value", type: "number", tsType: "number", zodSchema: "z.number()", formComponent: "FormInputNumber", nullable: true, isContentField: false, readOnly: true },
|
|
71
|
+
],
|
|
72
|
+
}));
|
|
73
|
+
expect(out).toContain("get effective_value()");
|
|
74
|
+
const inputBlock = out.split("Input = {")[1].split("};")[0];
|
|
75
|
+
expect(inputBlock).not.toContain("effective_value");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { makeFrontendData, makeRelationship } from "./fixtures";
|
|
3
|
+
import { resolveRelationshipKey } from "../transformers/relationship-key";
|
|
4
|
+
|
|
5
|
+
describe("frontend plumbing", () => {
|
|
6
|
+
it("defaults displayProp to name and activity tab on", () => {
|
|
7
|
+
const d = makeFrontendData();
|
|
8
|
+
expect(d.displayProp).toBe("name");
|
|
9
|
+
expect(d.containerTabs.activity).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("resolves wire key from explicit dtoKey verbatim", () => {
|
|
13
|
+
expect(resolveRelationshipKey(makeRelationship({ dtoKey: "estimateitems" }))).toBe("estimateitems");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("derives wire key identically to the backend when dtoKey absent", () => {
|
|
17
|
+
expect(resolveRelationshipKey(makeRelationship({ name: "Account", single: true }))).toBe("account");
|
|
18
|
+
expect(resolveRelationshipKey(makeRelationship({ name: "Person", single: false }))).toBe("persons");
|
|
19
|
+
expect(resolveRelationshipKey(makeRelationship({ alias: "EstimateItem", single: false }))).toBe("estimate-items");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { makeFrontendData } from "./fixtures";
|
|
3
|
+
import { generateSelectorTemplate } from "../templates/components/selector.template";
|
|
4
|
+
|
|
5
|
+
describe("selector template — combobox style", () => {
|
|
6
|
+
it("uses Button combobox trigger + Command primitives + Loader2", () => {
|
|
7
|
+
const out = generateSelectorTemplate(makeFrontendData());
|
|
8
|
+
expect(out).toContain('role="combobox"');
|
|
9
|
+
expect(out).toContain("ChevronsUpDown");
|
|
10
|
+
expect(out).toContain("CommandInput");
|
|
11
|
+
expect(out).toContain("CommandEmpty");
|
|
12
|
+
expect(out).toContain("CommandGroup");
|
|
13
|
+
expect(out).toContain("Loader2");
|
|
14
|
+
expect(out).toContain("w-(--radix-popover-trigger-width) p-0");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("drops the legacy bespoke styling", () => {
|
|
18
|
+
const out = generateSelectorTemplate(makeFrontendData());
|
|
19
|
+
expect(out).not.toContain("CircleX");
|
|
20
|
+
expect(out).not.toContain("RefreshCwIcon");
|
|
21
|
+
expect(out).not.toContain("SearchIcon");
|
|
22
|
+
expect(out).not.toContain("bg-input/20");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { makeFrontendData, makeRelationship } from "./fixtures";
|
|
3
|
+
import { generateTableHookTemplate } from "../templates/table-hook.template";
|
|
4
|
+
import { generateModuleTemplate } from "../templates/module.template";
|
|
5
|
+
import { generateListTemplate } from "../templates/components/list.template";
|
|
6
|
+
|
|
7
|
+
describe("table-hook template", () => {
|
|
8
|
+
it("emits cellDate for date fields", () => {
|
|
9
|
+
const out = generateTableHookTemplate(makeFrontendData());
|
|
10
|
+
expect(out).toContain("[WidgetFields.due_date]");
|
|
11
|
+
expect(out).toContain("cellDate({");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("emits a relationship column when showInTable", () => {
|
|
15
|
+
const out = generateTableHookTemplate(makeFrontendData({
|
|
16
|
+
relationships: [makeRelationship({ name: "Account", single: true, showInTable: true })],
|
|
17
|
+
}));
|
|
18
|
+
expect(out).toContain("widget.account?.name");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("uses variant for single-relationship column key when no alias", () => {
|
|
22
|
+
const out = generateTableHookTemplate(makeFrontendData({
|
|
23
|
+
relationships: [makeRelationship({ name: "User", variant: "Author", single: true, showInTable: true })],
|
|
24
|
+
}));
|
|
25
|
+
expect(out).toContain("WidgetFields.author");
|
|
26
|
+
expect(out).not.toContain("WidgetFields.user]");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("emits a many relationship column joined by name", () => {
|
|
30
|
+
const out = generateTableHookTemplate(makeFrontendData({
|
|
31
|
+
relationships: [makeRelationship({ name: "Tag", single: false, showInTable: true })],
|
|
32
|
+
}));
|
|
33
|
+
expect(out).toContain("WidgetFields.tags");
|
|
34
|
+
expect(out).toContain('.map((r) => r.name).join(", ")');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("module template", () => {
|
|
39
|
+
it("emits feature when featureId set", () => {
|
|
40
|
+
const out = generateModuleTemplate(makeFrontendData({ featureId: "Crm" }));
|
|
41
|
+
expect(out).toContain('import { FeatureIds } from "@/enums/feature.ids"');
|
|
42
|
+
expect(out).toContain("feature: FeatureIds.Crm");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("emits cross-module inclusions", () => {
|
|
46
|
+
const out = generateModuleTemplate(makeFrontendData({
|
|
47
|
+
relatedInclusions: [{ endpoint: "accounts", fields: ["name", "image"] }],
|
|
48
|
+
}));
|
|
49
|
+
expect(out).toContain('createJsonApiInclusion("accounts", [`name`, `image`])');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("list template", () => {
|
|
54
|
+
it("forwards fullWidth to ContentListTable", () => {
|
|
55
|
+
const out = generateListTemplate(makeFrontendData());
|
|
56
|
+
expect(out).toContain("fullWidth");
|
|
57
|
+
expect(out).toContain("fullWidth={fullWidth}");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
generateContextTemplate,
|
|
13
13
|
generateDeleterTemplate,
|
|
14
14
|
generateDetailPageTemplate,
|
|
15
|
-
generateDetailsTemplate,
|
|
16
15
|
generateEditorTemplate,
|
|
17
16
|
generateFieldsTemplate,
|
|
18
17
|
generateAliasesTemplate,
|
|
@@ -186,6 +185,10 @@ function buildTemplateData(schema: JsonModuleDefinition, targetHasNameMap?: Map<
|
|
|
186
185
|
|
|
187
186
|
// Map fields
|
|
188
187
|
const allFields = mapFields(schema.fields, names.camelCase);
|
|
188
|
+
allFields.forEach((f) => {
|
|
189
|
+
const src = schema.fields.find((sf) => sf.name === f.name);
|
|
190
|
+
f.readOnly = src?.readOnly === true;
|
|
191
|
+
});
|
|
189
192
|
|
|
190
193
|
// Get inherited fields to filter out for forms (Content fields are handled specially)
|
|
191
194
|
const inheritedFields = extendsContent ? ["name", "tldr", "abstract", "content"] : [];
|
|
@@ -234,6 +237,13 @@ function buildTemplateData(schema: JsonModuleDefinition, targetHasNameMap?: Map<
|
|
|
234
237
|
imports,
|
|
235
238
|
tableFieldNames,
|
|
236
239
|
relationshipServiceMethods,
|
|
240
|
+
featureId: schema.featureId,
|
|
241
|
+
displayProp: schema.displayProp ?? (allFields.some((f) => f.name === "name") ? "name" : "id"),
|
|
242
|
+
containerTabs: {
|
|
243
|
+
activity: schema.containerTabs?.activity !== false,
|
|
244
|
+
relations: schema.containerTabs?.relations ?? [],
|
|
245
|
+
},
|
|
246
|
+
relatedInclusions: schema.inclusions?.related ?? [],
|
|
237
247
|
};
|
|
238
248
|
}
|
|
239
249
|
|
|
@@ -338,22 +348,13 @@ function generateAllFiles(data: FrontendTemplateData, schema: JsonModuleDefiniti
|
|
|
338
348
|
type: "component",
|
|
339
349
|
});
|
|
340
350
|
|
|
351
|
+
// Content component — always emitted (Details tab body)
|
|
341
352
|
files.push({
|
|
342
|
-
path: paths.
|
|
343
|
-
content:
|
|
353
|
+
path: paths.content,
|
|
354
|
+
content: generateContentTemplate(data),
|
|
344
355
|
type: "component",
|
|
345
356
|
});
|
|
346
357
|
|
|
347
|
-
// Content component only for Content-extending modules
|
|
348
|
-
const contentTemplate = generateContentTemplate(data);
|
|
349
|
-
if (contentTemplate) {
|
|
350
|
-
files.push({
|
|
351
|
-
path: paths.content,
|
|
352
|
-
content: contentTemplate,
|
|
353
|
-
type: "component",
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
358
|
files.push({
|
|
358
359
|
path: paths.container,
|
|
359
360
|
content: generateContainerTemplate(data),
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Container Template
|
|
3
3
|
*
|
|
4
|
-
* Generates {Module}Container.tsx component for
|
|
4
|
+
* Generates {Module}Container.tsx component for the item detail page.
|
|
5
|
+
*
|
|
6
|
+
* Emits a RoundPageContainer driven by a `tabs` array: a default "Details" tab
|
|
7
|
+
* rendering {Module}Content, one tab per configured relation list, and an
|
|
8
|
+
* optional "Activity" tab. The default export gates on delete permission.
|
|
5
9
|
*/
|
|
6
10
|
|
|
11
|
+
import { pluralize, toCamelCase, toKebabCase } from "../../transformers/name-transformer";
|
|
7
12
|
import { FrontendTemplateData } from "../../types/template-data.interface";
|
|
8
13
|
|
|
9
14
|
/**
|
|
@@ -13,86 +18,66 @@ import { FrontendTemplateData } from "../../types/template-data.interface";
|
|
|
13
18
|
* @returns Generated file content
|
|
14
19
|
*/
|
|
15
20
|
export function generateContainerTemplate(data: FrontendTemplateData): string {
|
|
16
|
-
const { names,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const { names, containerTabs } = data;
|
|
22
|
+
const camel = names.camelCase;
|
|
23
|
+
|
|
24
|
+
const relationImports: string[] = [];
|
|
25
|
+
const relationTabs: string[] = [];
|
|
26
|
+
for (const rel of containerTabs.relations) {
|
|
27
|
+
const relKebab = toKebabCase(rel.module);
|
|
28
|
+
const relPluralCamel = pluralize(toCamelCase(rel.module));
|
|
29
|
+
relationImports.push(
|
|
30
|
+
`import ${rel.module}List from "@/features/${rel.directory}/${relKebab}/components/lists/${rel.module}List";`,
|
|
31
|
+
);
|
|
32
|
+
relationTabs.push(
|
|
33
|
+
` {\n key: Modules.${rel.module},\n label: t(\`entities.${relPluralCamel.toLowerCase()}\`, { count: 2 }),\n content: <${rel.module}List ${rel.listProp}={${camel}} />,\n },`,
|
|
34
|
+
);
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
const tabEntries: string[] = [
|
|
38
|
+
` {\n label: "Details",\n content: <${names.pascalCase}Content ${camel}={${camel}} />,\n },`,
|
|
39
|
+
...relationTabs,
|
|
40
|
+
];
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
if (containerTabs.activity) {
|
|
43
|
+
tabEntries.push(
|
|
44
|
+
` {\n label: "Activity",\n content: <ActivityFeed module={Modules.${names.pascalCase}} entityId={${camel}.id} />,\n },`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const activityImport = containerTabs.activity
|
|
49
|
+
? `import ActivityFeed from "@/features/activity/activity-feed/components/ActivityFeed";\n`
|
|
50
|
+
: "";
|
|
51
|
+
const relationImportBlock = relationImports.length ? `${relationImports.join("\n")}\n` : "";
|
|
32
52
|
|
|
33
53
|
return `"use client";
|
|
34
54
|
|
|
35
|
-
import ${names.pascalCase}Content from "@/features/${data.importTargetDir}/${names.kebabCase}/components/details/${names.pascalCase}Content";
|
|
36
|
-
import ${names.pascalCase}Details from "@/features/${data.importTargetDir}/${names.kebabCase}/components/details/${names.pascalCase}Details";
|
|
55
|
+
${activityImport}${relationImportBlock}import { ${names.pascalCase}Content } from "@/features/${data.importTargetDir}/${names.kebabCase}/components/details/${names.pascalCase}Content";
|
|
37
56
|
import { use${names.pascalCase}Context } from "@/features/${data.importTargetDir}/${names.kebabCase}/contexts/${names.pascalCase}Context";
|
|
38
57
|
import { ${names.pascalCase}Interface } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
|
|
39
|
-
import { RoundPageContainer } from "@carlonicora/nextjs-jsonapi/components";
|
|
40
|
-
import {
|
|
58
|
+
import { RoundPageContainer, Tab } from "@carlonicora/nextjs-jsonapi/components";
|
|
59
|
+
import { useCurrentUserContext } from "@carlonicora/nextjs-jsonapi/contexts";
|
|
60
|
+
import { Action, Modules } from "@carlonicora/nextjs-jsonapi/core";
|
|
61
|
+
import { useTranslations } from "next-intl";
|
|
41
62
|
|
|
42
|
-
type ${names.pascalCase}ContainerProps = {
|
|
43
|
-
${names.camelCase}: ${names.pascalCase}Interface;
|
|
44
|
-
};
|
|
63
|
+
type ${names.pascalCase}ContainerProps = { ${camel}: ${names.pascalCase}Interface };
|
|
45
64
|
|
|
46
|
-
function ${names.pascalCase}ContainerInternal({ ${
|
|
47
|
-
|
|
48
|
-
<RoundPageContainer module={Modules.${names.pascalCase}} details={<${names.pascalCase}Details />}>
|
|
49
|
-
<${names.pascalCase}Content />
|
|
50
|
-
</RoundPageContainer>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
65
|
+
function ${names.pascalCase}ContainerInternal({ ${camel} }: ${names.pascalCase}ContainerProps) {
|
|
66
|
+
const t = useTranslations();
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
const tabs: Tab[] = [
|
|
69
|
+
${tabEntries.join("\n")}
|
|
70
|
+
];
|
|
57
71
|
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Generate container for non-Content modules
|
|
65
|
-
*/
|
|
66
|
-
function generateSimpleContainerTemplate(data: FrontendTemplateData): string {
|
|
67
|
-
const { names } = data;
|
|
68
|
-
|
|
69
|
-
return `"use client";
|
|
70
|
-
|
|
71
|
-
import ${names.pascalCase}Details from "@/features/${data.importTargetDir}/${names.kebabCase}/components/details/${names.pascalCase}Details";
|
|
72
|
-
import { use${names.pascalCase}Context } from "@/features/${data.importTargetDir}/${names.kebabCase}/contexts/${names.pascalCase}Context";
|
|
73
|
-
import { ${names.pascalCase}Interface } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
|
|
74
|
-
import { RoundPageContainer } from "@carlonicora/nextjs-jsonapi/components";
|
|
75
|
-
import { Modules } from "@carlonicora/nextjs-jsonapi/core";
|
|
76
|
-
|
|
77
|
-
type ${names.pascalCase}ContainerProps = {
|
|
78
|
-
${names.camelCase}: ${names.pascalCase}Interface;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
function ${names.pascalCase}ContainerInternal({ ${names.camelCase} }: ${names.pascalCase}ContainerProps) {
|
|
82
|
-
return (
|
|
83
|
-
<RoundPageContainer module={Modules.${names.pascalCase}} details={<${names.pascalCase}Details />}>
|
|
84
|
-
<div className="flex w-full flex-col gap-y-4">
|
|
85
|
-
{/* Add custom content sections here */}
|
|
86
|
-
</div>
|
|
87
|
-
</RoundPageContainer>
|
|
88
|
-
);
|
|
72
|
+
return <RoundPageContainer module={Modules.${names.pascalCase}} id={${camel}.id} tabs={tabs} />;
|
|
89
73
|
}
|
|
90
74
|
|
|
91
75
|
export default function ${names.pascalCase}Container() {
|
|
92
|
-
const {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
76
|
+
const { hasPermissionToModule } = useCurrentUserContext();
|
|
77
|
+
const { ${camel} } = use${names.pascalCase}Context();
|
|
78
|
+
if (!${camel}) return null;
|
|
79
|
+
if (!hasPermissionToModule({ module: Modules.${names.pascalCase}, action: Action.Delete, data: ${camel} })) return null;
|
|
80
|
+
return <${names.pascalCase}ContainerInternal ${camel}={${camel}} />;
|
|
96
81
|
}
|
|
97
82
|
`;
|
|
98
83
|
}
|