@carlonicora/nextjs-jsonapi 1.1.1 → 1.3.0

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.
Files changed (230) hide show
  1. package/dist/{BlockNoteEditor-6NVBI7XL.js → BlockNoteEditor-CNXQ3WL3.js} +7 -7
  2. package/dist/{BlockNoteEditor-6NVBI7XL.js.map → BlockNoteEditor-CNXQ3WL3.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-4VOBTXWC.mjs → BlockNoteEditor-DC33K4IF.mjs} +3 -3
  4. package/dist/{chunk-KPRHIJO6.js → chunk-KDFDGXCX.js} +53 -53
  5. package/dist/{chunk-KPRHIJO6.js.map → chunk-KDFDGXCX.js.map} +1 -1
  6. package/dist/{chunk-FYRFMABS.js → chunk-L6LH7WA4.js} +21 -2
  7. package/dist/chunk-L6LH7WA4.js.map +1 -0
  8. package/dist/{chunk-MA2L2PL2.mjs → chunk-MHVXFWZY.mjs} +20 -1
  9. package/dist/chunk-MHVXFWZY.mjs.map +1 -0
  10. package/dist/{chunk-HP4Q5ZVG.mjs → chunk-Y4XI3AZR.mjs} +3 -3
  11. package/dist/client/index.js +3 -3
  12. package/dist/client/index.mjs +2 -2
  13. package/dist/components/index.js +3 -3
  14. package/dist/components/index.mjs +2 -2
  15. package/dist/contexts/index.js +3 -3
  16. package/dist/contexts/index.mjs +2 -2
  17. package/dist/features/index.d.mts +3 -1
  18. package/dist/features/index.d.ts +3 -1
  19. package/dist/features/index.js +4 -2
  20. package/dist/features/index.js.map +1 -1
  21. package/dist/features/index.mjs +3 -1
  22. package/dist/hooks/index.js +3 -3
  23. package/dist/hooks/index.mjs +2 -2
  24. package/dist/scripts/generate-web-module/generator.d.ts +15 -0
  25. package/dist/scripts/generate-web-module/generator.d.ts.map +1 -0
  26. package/dist/scripts/generate-web-module/generator.js +320 -0
  27. package/dist/scripts/generate-web-module/generator.js.map +1 -0
  28. package/dist/scripts/generate-web-module/index.d.ts +16 -0
  29. package/dist/scripts/generate-web-module/index.d.ts.map +1 -0
  30. package/dist/scripts/generate-web-module/index.js +80 -0
  31. package/dist/scripts/generate-web-module/index.js.map +1 -0
  32. package/dist/scripts/generate-web-module/templates/components/container.template.d.ts +14 -0
  33. package/dist/scripts/generate-web-module/templates/components/container.template.d.ts.map +1 -0
  34. package/dist/scripts/generate-web-module/templates/components/container.template.js +124 -0
  35. package/dist/scripts/generate-web-module/templates/components/container.template.js.map +1 -0
  36. package/dist/scripts/generate-web-module/templates/components/content.template.d.ts +15 -0
  37. package/dist/scripts/generate-web-module/templates/components/content.template.d.ts.map +1 -0
  38. package/dist/scripts/generate-web-module/templates/components/content.template.js +39 -0
  39. package/dist/scripts/generate-web-module/templates/components/content.template.js.map +1 -0
  40. package/dist/scripts/generate-web-module/templates/components/deleter.template.d.ts +14 -0
  41. package/dist/scripts/generate-web-module/templates/components/deleter.template.d.ts.map +1 -0
  42. package/dist/scripts/generate-web-module/templates/components/deleter.template.js +51 -0
  43. package/dist/scripts/generate-web-module/templates/components/deleter.template.js.map +1 -0
  44. package/dist/scripts/generate-web-module/templates/components/details.template.d.ts +14 -0
  45. package/dist/scripts/generate-web-module/templates/components/details.template.d.ts.map +1 -0
  46. package/dist/scripts/generate-web-module/templates/components/details.template.js +109 -0
  47. package/dist/scripts/generate-web-module/templates/components/details.template.js.map +1 -0
  48. package/dist/scripts/generate-web-module/templates/components/editor.template.d.ts +14 -0
  49. package/dist/scripts/generate-web-module/templates/components/editor.template.d.ts.map +1 -0
  50. package/dist/scripts/generate-web-module/templates/components/editor.template.js +459 -0
  51. package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -0
  52. package/dist/scripts/generate-web-module/templates/components/index.d.ts +15 -0
  53. package/dist/scripts/generate-web-module/templates/components/index.d.ts.map +1 -0
  54. package/dist/scripts/generate-web-module/templates/components/index.js +29 -0
  55. package/dist/scripts/generate-web-module/templates/components/index.js.map +1 -0
  56. package/dist/scripts/generate-web-module/templates/components/list-container.template.d.ts +14 -0
  57. package/dist/scripts/generate-web-module/templates/components/list-container.template.d.ts.map +1 -0
  58. package/dist/scripts/generate-web-module/templates/components/list-container.template.js +30 -0
  59. package/dist/scripts/generate-web-module/templates/components/list-container.template.js.map +1 -0
  60. package/dist/scripts/generate-web-module/templates/components/list.template.d.ts +14 -0
  61. package/dist/scripts/generate-web-module/templates/components/list.template.d.ts.map +1 -0
  62. package/dist/scripts/generate-web-module/templates/components/list.template.js +84 -0
  63. package/dist/scripts/generate-web-module/templates/components/list.template.js.map +1 -0
  64. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts +14 -0
  65. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -0
  66. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +184 -0
  67. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -0
  68. package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts +14 -0
  69. package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts.map +1 -0
  70. package/dist/scripts/generate-web-module/templates/components/selector.template.js +199 -0
  71. package/dist/scripts/generate-web-module/templates/components/selector.template.js.map +1 -0
  72. package/dist/scripts/generate-web-module/templates/context.template.d.ts +14 -0
  73. package/dist/scripts/generate-web-module/templates/context.template.d.ts.map +1 -0
  74. package/dist/scripts/generate-web-module/templates/context.template.js +121 -0
  75. package/dist/scripts/generate-web-module/templates/context.template.js.map +1 -0
  76. package/dist/scripts/generate-web-module/templates/data/fields.template.d.ts +14 -0
  77. package/dist/scripts/generate-web-module/templates/data/fields.template.d.ts.map +1 -0
  78. package/dist/scripts/generate-web-module/templates/data/fields.template.js +55 -0
  79. package/dist/scripts/generate-web-module/templates/data/fields.template.js.map +1 -0
  80. package/dist/scripts/generate-web-module/templates/data/index.d.ts +10 -0
  81. package/dist/scripts/generate-web-module/templates/data/index.d.ts.map +1 -0
  82. package/dist/scripts/generate-web-module/templates/data/index.js +26 -0
  83. package/dist/scripts/generate-web-module/templates/data/index.js.map +1 -0
  84. package/dist/scripts/generate-web-module/templates/data/interface.template.d.ts +14 -0
  85. package/dist/scripts/generate-web-module/templates/data/interface.template.d.ts.map +1 -0
  86. package/dist/scripts/generate-web-module/templates/data/interface.template.js +116 -0
  87. package/dist/scripts/generate-web-module/templates/data/interface.template.js.map +1 -0
  88. package/dist/scripts/generate-web-module/templates/data/model.template.d.ts +14 -0
  89. package/dist/scripts/generate-web-module/templates/data/model.template.d.ts.map +1 -0
  90. package/dist/scripts/generate-web-module/templates/data/model.template.js +274 -0
  91. package/dist/scripts/generate-web-module/templates/data/model.template.js.map +1 -0
  92. package/dist/scripts/generate-web-module/templates/data/service.template.d.ts +14 -0
  93. package/dist/scripts/generate-web-module/templates/data/service.template.d.ts.map +1 -0
  94. package/dist/scripts/generate-web-module/templates/data/service.template.js +168 -0
  95. package/dist/scripts/generate-web-module/templates/data/service.template.js.map +1 -0
  96. package/dist/scripts/generate-web-module/templates/index.d.ts +12 -0
  97. package/dist/scripts/generate-web-module/templates/index.d.ts.map +1 -0
  98. package/dist/scripts/generate-web-module/templates/index.js +37 -0
  99. package/dist/scripts/generate-web-module/templates/index.js.map +1 -0
  100. package/dist/scripts/generate-web-module/templates/module.template.d.ts +14 -0
  101. package/dist/scripts/generate-web-module/templates/module.template.d.ts.map +1 -0
  102. package/dist/scripts/generate-web-module/templates/module.template.js +64 -0
  103. package/dist/scripts/generate-web-module/templates/module.template.js.map +1 -0
  104. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.d.ts +14 -0
  105. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.d.ts.map +1 -0
  106. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js +65 -0
  107. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js.map +1 -0
  108. package/dist/scripts/generate-web-module/templates/pages/index.d.ts +8 -0
  109. package/dist/scripts/generate-web-module/templates/pages/index.d.ts.map +1 -0
  110. package/dist/scripts/generate-web-module/templates/pages/index.js +13 -0
  111. package/dist/scripts/generate-web-module/templates/pages/index.js.map +1 -0
  112. package/dist/scripts/generate-web-module/templates/pages/list-page.template.d.ts +14 -0
  113. package/dist/scripts/generate-web-module/templates/pages/list-page.template.d.ts.map +1 -0
  114. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js +37 -0
  115. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js.map +1 -0
  116. package/dist/scripts/generate-web-module/templates/table-hook.template.d.ts +14 -0
  117. package/dist/scripts/generate-web-module/templates/table-hook.template.d.ts.map +1 -0
  118. package/dist/scripts/generate-web-module/templates/table-hook.template.js +174 -0
  119. package/dist/scripts/generate-web-module/templates/table-hook.template.js.map +1 -0
  120. package/dist/scripts/generate-web-module/transformers/field-mapper.d.ts +55 -0
  121. package/dist/scripts/generate-web-module/transformers/field-mapper.d.ts.map +1 -0
  122. package/dist/scripts/generate-web-module/transformers/field-mapper.js +179 -0
  123. package/dist/scripts/generate-web-module/transformers/field-mapper.js.map +1 -0
  124. package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts +78 -0
  125. package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts.map +1 -0
  126. package/dist/scripts/generate-web-module/transformers/i18n-generator.js +182 -0
  127. package/dist/scripts/generate-web-module/transformers/i18n-generator.js.map +1 -0
  128. package/dist/scripts/generate-web-module/transformers/import-resolver.d.ts +106 -0
  129. package/dist/scripts/generate-web-module/transformers/import-resolver.d.ts.map +1 -0
  130. package/dist/scripts/generate-web-module/transformers/import-resolver.js +193 -0
  131. package/dist/scripts/generate-web-module/transformers/import-resolver.js.map +1 -0
  132. package/dist/scripts/generate-web-module/transformers/index.d.ts +12 -0
  133. package/dist/scripts/generate-web-module/transformers/index.d.ts.map +1 -0
  134. package/dist/scripts/generate-web-module/transformers/index.js +28 -0
  135. package/dist/scripts/generate-web-module/transformers/index.js.map +1 -0
  136. package/dist/scripts/generate-web-module/transformers/name-transformer.d.ts +60 -0
  137. package/dist/scripts/generate-web-module/transformers/name-transformer.d.ts.map +1 -0
  138. package/dist/scripts/generate-web-module/transformers/name-transformer.js +115 -0
  139. package/dist/scripts/generate-web-module/transformers/name-transformer.js.map +1 -0
  140. package/dist/scripts/generate-web-module/transformers/parent-detector.d.ts +57 -0
  141. package/dist/scripts/generate-web-module/transformers/parent-detector.d.ts.map +1 -0
  142. package/dist/scripts/generate-web-module/transformers/parent-detector.js +88 -0
  143. package/dist/scripts/generate-web-module/transformers/parent-detector.js.map +1 -0
  144. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts +68 -0
  145. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts.map +1 -0
  146. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js +219 -0
  147. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js.map +1 -0
  148. package/dist/scripts/generate-web-module/types/field-mapping.types.d.ts +68 -0
  149. package/dist/scripts/generate-web-module/types/field-mapping.types.d.ts.map +1 -0
  150. package/dist/scripts/generate-web-module/types/field-mapping.types.js +129 -0
  151. package/dist/scripts/generate-web-module/types/field-mapping.types.js.map +1 -0
  152. package/dist/scripts/generate-web-module/types/index.d.ts +9 -0
  153. package/dist/scripts/generate-web-module/types/index.d.ts.map +1 -0
  154. package/dist/scripts/generate-web-module/types/index.js +25 -0
  155. package/dist/scripts/generate-web-module/types/index.js.map +1 -0
  156. package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts +40 -0
  157. package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts.map +1 -0
  158. package/dist/scripts/generate-web-module/types/json-schema.interface.js +10 -0
  159. package/dist/scripts/generate-web-module/types/json-schema.interface.js.map +1 -0
  160. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts +128 -0
  161. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts.map +1 -0
  162. package/dist/scripts/generate-web-module/types/template-data.interface.js +9 -0
  163. package/dist/scripts/generate-web-module/types/template-data.interface.js.map +1 -0
  164. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.d.ts +29 -0
  165. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.d.ts.map +1 -0
  166. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.js +153 -0
  167. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.js.map +1 -0
  168. package/dist/scripts/generate-web-module/utils/file-writer.d.ts +38 -0
  169. package/dist/scripts/generate-web-module/utils/file-writer.d.ts.map +1 -0
  170. package/dist/scripts/generate-web-module/utils/file-writer.js +126 -0
  171. package/dist/scripts/generate-web-module/utils/file-writer.js.map +1 -0
  172. package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts +28 -0
  173. package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts.map +1 -0
  174. package/dist/scripts/generate-web-module/utils/i18n-updater.js +122 -0
  175. package/dist/scripts/generate-web-module/utils/i18n-updater.js.map +1 -0
  176. package/dist/scripts/generate-web-module/utils/index.d.ts +9 -0
  177. package/dist/scripts/generate-web-module/utils/index.d.ts.map +1 -0
  178. package/dist/scripts/generate-web-module/utils/index.js +20 -0
  179. package/dist/scripts/generate-web-module/utils/index.js.map +1 -0
  180. package/dist/scripts/generate-web-module/validators/json-schema-validator.d.ts +46 -0
  181. package/dist/scripts/generate-web-module/validators/json-schema-validator.d.ts.map +1 -0
  182. package/dist/scripts/generate-web-module/validators/json-schema-validator.js +265 -0
  183. package/dist/scripts/generate-web-module/validators/json-schema-validator.js.map +1 -0
  184. package/package.json +27 -21
  185. package/scripts/generate-web-module/generator.ts +363 -0
  186. package/scripts/generate-web-module/index.ts +49 -0
  187. package/scripts/generate-web-module/templates/components/container.template.ts +129 -0
  188. package/scripts/generate-web-module/templates/components/content.template.ts +40 -0
  189. package/scripts/generate-web-module/templates/components/deleter.template.ts +51 -0
  190. package/scripts/generate-web-module/templates/components/details.template.ts +115 -0
  191. package/scripts/generate-web-module/templates/components/editor.template.ts +495 -0
  192. package/scripts/generate-web-module/templates/components/index.ts +18 -0
  193. package/scripts/generate-web-module/templates/components/list-container.template.ts +30 -0
  194. package/scripts/generate-web-module/templates/components/list.template.ts +91 -0
  195. package/scripts/generate-web-module/templates/components/multi-selector.template.ts +184 -0
  196. package/scripts/generate-web-module/templates/components/selector.template.ts +199 -0
  197. package/scripts/generate-web-module/templates/context.template.ts +121 -0
  198. package/scripts/generate-web-module/templates/data/fields.template.ts +64 -0
  199. package/scripts/generate-web-module/templates/data/index.ts +10 -0
  200. package/scripts/generate-web-module/templates/data/interface.template.ts +136 -0
  201. package/scripts/generate-web-module/templates/data/model.template.ts +327 -0
  202. package/scripts/generate-web-module/templates/data/service.template.ts +185 -0
  203. package/scripts/generate-web-module/templates/index.ts +34 -0
  204. package/scripts/generate-web-module/templates/module.template.ts +69 -0
  205. package/scripts/generate-web-module/templates/pages/detail-page.template.ts +65 -0
  206. package/scripts/generate-web-module/templates/pages/index.ts +8 -0
  207. package/scripts/generate-web-module/templates/pages/list-page.template.ts +37 -0
  208. package/scripts/generate-web-module/templates/table-hook.template.ts +182 -0
  209. package/scripts/generate-web-module/transformers/field-mapper.ts +201 -0
  210. package/scripts/generate-web-module/transformers/i18n-generator.ts +199 -0
  211. package/scripts/generate-web-module/transformers/import-resolver.ts +250 -0
  212. package/scripts/generate-web-module/transformers/index.ts +12 -0
  213. package/scripts/generate-web-module/transformers/name-transformer.ts +115 -0
  214. package/scripts/generate-web-module/transformers/parent-detector.ts +87 -0
  215. package/scripts/generate-web-module/transformers/relationship-resolver.ts +221 -0
  216. package/scripts/generate-web-module/tsconfig.json +24 -0
  217. package/scripts/generate-web-module/types/field-mapping.types.ts +141 -0
  218. package/scripts/generate-web-module/types/index.ts +9 -0
  219. package/scripts/generate-web-module/types/json-schema.interface.ts +42 -0
  220. package/scripts/generate-web-module/types/template-data.interface.ts +164 -0
  221. package/scripts/generate-web-module/utils/bootstrapper-updater.ts +145 -0
  222. package/scripts/generate-web-module/utils/file-writer.ts +115 -0
  223. package/scripts/generate-web-module/utils/i18n-updater.ts +108 -0
  224. package/scripts/generate-web-module/utils/index.ts +9 -0
  225. package/scripts/generate-web-module/validators/json-schema-validator.ts +306 -0
  226. package/src/features/user/index.ts +1 -1
  227. package/dist/chunk-FYRFMABS.js.map +0 -1
  228. package/dist/chunk-MA2L2PL2.mjs.map +0 -1
  229. /package/dist/{BlockNoteEditor-4VOBTXWC.mjs.map → BlockNoteEditor-DC33K4IF.mjs.map} +0 -0
  230. /package/dist/{chunk-HP4Q5ZVG.mjs.map → chunk-Y4XI3AZR.mjs.map} +0 -0
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ListContainer Template
3
+ *
4
+ * Generates {Module}ListContainer.tsx component for the list page.
5
+ */
6
+
7
+ import { FrontendTemplateData } from "../../types/template-data.interface";
8
+
9
+ /**
10
+ * Generate the list container component file content
11
+ *
12
+ * @param data - Frontend template data
13
+ * @returns Generated file content
14
+ */
15
+ export function generateListContainerTemplate(data: FrontendTemplateData): string {
16
+ const { names } = data;
17
+
18
+ return `"use client";
19
+
20
+ import ${names.pascalCase}List from "@/features/${data.targetDir}/${names.kebabCase}/components/lists/${names.pascalCase}List";
21
+
22
+ function ${names.pascalCase}ListContainerInternal() {
23
+ return <${names.pascalCase}List />;
24
+ }
25
+
26
+ export default function ${names.pascalCase}ListContainer() {
27
+ return <${names.pascalCase}ListContainerInternal />;
28
+ }
29
+ `;
30
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * List Template
3
+ *
4
+ * Generates {Module}List.tsx component for displaying a list of items.
5
+ */
6
+
7
+ import { FrontendTemplateData } from "../../types/template-data.interface";
8
+ import { toCamelCase, pluralize } from "../../transformers/name-transformer";
9
+
10
+ /**
11
+ * Generate the list component file content
12
+ *
13
+ * @param data - Frontend template data
14
+ * @returns Generated file content
15
+ */
16
+ export function generateListTemplate(data: FrontendTemplateData): string {
17
+ const { names, fields, relationships, extendsContent, tableFieldNames } = data;
18
+
19
+ // Build field list for the table - use name + first few fields + updatedAt
20
+ const displayFields = buildDisplayFields(data);
21
+
22
+ return `"use client";
23
+
24
+ import ${names.pascalCase}Editor from "@/features/${data.targetDir}/${names.kebabCase}/components/forms/${names.pascalCase}Editor";
25
+ import { ${names.pascalCase}Fields } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Fields";
26
+ import { ${names.pascalCase}Interface } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
27
+ import { ${names.pascalCase}Service } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Service";
28
+ import "@/features/${data.targetDir}/${names.kebabCase}/hooks/use${names.pascalCase}TableStructure";
29
+ import { ContentListTable } from "@carlonicora/nextjs-jsonapi/components";
30
+ import { Modules } from "@carlonicora/nextjs-jsonapi/core";
31
+ import { DataListRetriever, useDataListRetriever } from "@carlonicora/nextjs-jsonapi/hooks";
32
+ import { useTranslations } from "next-intl";
33
+ import { ReactNode } from "react";
34
+
35
+ export default function ${names.pascalCase}List() {
36
+ const t = useTranslations();
37
+
38
+ const data: DataListRetriever<${names.pascalCase}Interface> = useDataListRetriever({
39
+ module: Modules.${names.pascalCase},
40
+ retriever: (params) => ${names.pascalCase}Service.findMany(params),
41
+ retrieverParams: {},
42
+ });
43
+
44
+ const functions: ReactNode[] = [<${names.pascalCase}Editor key="create-${names.kebabCase}" />];
45
+
46
+ return (
47
+ <ContentListTable
48
+ data={data}
49
+ fields={[${displayFields}]}
50
+ tableGeneratorType={Modules.${names.pascalCase}}
51
+ functions={functions}
52
+ title={t(\`types.${names.pluralKebab}\`, { count: 2 })}
53
+ />
54
+ );
55
+ }
56
+ `;
57
+ }
58
+
59
+ /**
60
+ * Build display fields array for the table
61
+ */
62
+ function buildDisplayFields(data: FrontendTemplateData): string {
63
+ const { names, fields, relationships, extendsContent } = data;
64
+ const displayFields: string[] = [];
65
+
66
+ // Always include name if it exists
67
+ if (extendsContent || fields.some((f) => f.name === "name")) {
68
+ displayFields.push(`${names.pascalCase}Fields.name`);
69
+ }
70
+
71
+ // Add author if present
72
+ const authorRel = relationships.find((r) => r.variant === "Author");
73
+ if (authorRel) {
74
+ const authorFieldName = toCamelCase(authorRel.variant!);
75
+ displayFields.push(`${names.pascalCase}Fields.${authorFieldName}`);
76
+ }
77
+
78
+ // Add first non-author relationship
79
+ const otherRel = relationships.find((r) => r.variant !== "Author");
80
+ if (otherRel) {
81
+ const relFieldName = otherRel.single
82
+ ? toCamelCase(otherRel.variant || otherRel.name)
83
+ : pluralize(toCamelCase(otherRel.name));
84
+ displayFields.push(`${names.pascalCase}Fields.${relFieldName}`);
85
+ }
86
+
87
+ // Add updatedAt
88
+ displayFields.push(`${names.pascalCase}Fields.updatedAt`);
89
+
90
+ return displayFields.join(", ");
91
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * MultiSelector Template
3
+ *
4
+ * Generates {Module}MultiSelector.tsx multi-select component.
5
+ */
6
+
7
+ import { FrontendTemplateData } from "../../types/template-data.interface";
8
+
9
+ /**
10
+ * Generate the multi-selector component file content
11
+ *
12
+ * @param data - Frontend template data
13
+ * @returns Generated file content
14
+ */
15
+ export function generateMultiSelectorTemplate(data: FrontendTemplateData): string {
16
+ const { names } = data;
17
+
18
+ return `"use client";
19
+
20
+ import { FormControl, FormField, FormItem, FormLabel, FormMessage, MultiSelect } from "@carlonicora/nextjs-jsonapi/shadcnui";
21
+ import { ${names.pascalCase}Interface } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
22
+ import { ${names.pascalCase}Service } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Service";
23
+ import { DataListRetriever, useDataListRetriever } from "@carlonicora/nextjs-jsonapi/hooks";
24
+ import { useDebounce } from "@carlonicora/nextjs-jsonapi/hooks";
25
+ import { Modules } from "@carlonicora/nextjs-jsonapi/core";
26
+ import { useTranslations } from "next-intl";
27
+ import { useCallback, useEffect, useState } from "react";
28
+ import { useWatch } from "react-hook-form";
29
+
30
+ type ${names.pascalCase}MultiSelectType = {
31
+ id: string;
32
+ name: string;
33
+ };
34
+
35
+ type ${names.pascalCase}MultiSelectorProps = {
36
+ id: string;
37
+ form: any;
38
+ current${names.pascalCase}?: ${names.pascalCase}Interface;
39
+ label?: string;
40
+ placeholder?: string;
41
+ onChange?: (${names.pluralCamel}?: ${names.pascalCase}Interface[]) => void;
42
+ maxCount?: number;
43
+ isRequired?: boolean;
44
+ };
45
+
46
+ export default function ${names.pascalCase}MultiSelector({
47
+ id,
48
+ form,
49
+ current${names.pascalCase},
50
+ label,
51
+ placeholder,
52
+ onChange,
53
+ maxCount = 3,
54
+ isRequired = false,
55
+ }: ${names.pascalCase}MultiSelectorProps) {
56
+ const t = useTranslations("features.${names.camelCase}");
57
+ const [${names.camelCase}Options, set${names.pascalCase}Options] = useState<any[]>([]);
58
+ const [searchTerm, setSearchTerm] = useState<string>("");
59
+
60
+ const selected${names.pluralPascal}: ${names.pascalCase}MultiSelectType[] = useWatch({ control: form.control, name: id }) || [];
61
+
62
+ const data: DataListRetriever<${names.pascalCase}Interface> = useDataListRetriever({
63
+ retriever: (params) => ${names.pascalCase}Service.findMany(params),
64
+ retrieverParams: {},
65
+ ready: true,
66
+ module: Modules.${names.pascalCase},
67
+ });
68
+
69
+ const updateSearch = useCallback(
70
+ (searchedTerm: string) => {
71
+ if (searchedTerm.trim()) {
72
+ data.addAdditionalParameter("search", searchedTerm.trim());
73
+ } else {
74
+ data.removeAdditionalParameter("search");
75
+ }
76
+ },
77
+ [data],
78
+ );
79
+
80
+ const debouncedUpdateSearch = useDebounce(updateSearch, 500);
81
+
82
+ useEffect(() => {
83
+ debouncedUpdateSearch(searchTerm);
84
+ }, [debouncedUpdateSearch, searchTerm]);
85
+
86
+ useEffect(() => {
87
+ if (data.data && data.data.length > 0) {
88
+ const ${names.pluralCamel} = data.data as ${names.pascalCase}Interface[];
89
+ const filtered${names.pluralPascal} = ${names.pluralCamel}.filter((${names.camelCase}) => ${names.camelCase}.id !== current${names.pascalCase}?.id);
90
+
91
+ const options = filtered${names.pluralPascal}.map((${names.camelCase}) => ({
92
+ label: ${names.camelCase}.name,
93
+ value: ${names.camelCase}.id,
94
+ ${names.camelCase}Data: ${names.camelCase},
95
+ }));
96
+
97
+ set${names.pascalCase}Options(options);
98
+ }
99
+ }, [data.data, current${names.pascalCase}]);
100
+
101
+ // Add options for any already selected ${names.pluralCamel} that aren't in search results
102
+ useEffect(() => {
103
+ if (selected${names.pluralPascal}.length > 0) {
104
+ // Create a map of existing option IDs for quick lookup
105
+ const existingOptionIds = new Set(${names.camelCase}Options.map((option) => option.value));
106
+
107
+ // Find selected ${names.pluralCamel} that don't have an option yet
108
+ const missingOptions = selected${names.pluralPascal}
109
+ .filter((${names.camelCase}) => !existingOptionIds.has(${names.camelCase}.id))
110
+ .map((${names.camelCase}) => ({
111
+ label: ${names.camelCase}.name,
112
+ value: ${names.camelCase}.id,
113
+ ${names.camelCase}Data: ${names.camelCase},
114
+ }));
115
+
116
+ if (missingOptions.length > 0) {
117
+ set${names.pascalCase}Options((prev) => [...prev, ...missingOptions]);
118
+ }
119
+ }
120
+ }, [selected${names.pluralPascal}, ${names.camelCase}Options]);
121
+
122
+ const handleValueChange = (selectedIds: string[]) => {
123
+ const updatedSelected${names.pluralPascal} = selectedIds.map((id) => {
124
+ const existing${names.pascalCase} = selected${names.pluralPascal}.find((${names.camelCase}) => ${names.camelCase}.id === id);
125
+ if (existing${names.pascalCase}) {
126
+ return existing${names.pascalCase};
127
+ }
128
+
129
+ const option = ${names.camelCase}Options.find((option) => option.value === id);
130
+ if (option?.${names.camelCase}Data) {
131
+ return {
132
+ id: option.${names.camelCase}Data.id,
133
+ name: option.${names.camelCase}Data.name,
134
+ };
135
+ }
136
+
137
+ return { id, name: id };
138
+ });
139
+
140
+ form.setValue(id, updatedSelected${names.pluralPascal});
141
+
142
+ if (onChange) {
143
+ const fullSelected${names.pluralPascal} = selectedIds
144
+ .map((id) => ${names.camelCase}Options.find((option) => option.value === id)?.${names.camelCase}Data)
145
+ .filter(Boolean) as ${names.pascalCase}Interface[];
146
+ onChange(fullSelected${names.pluralPascal});
147
+ }
148
+ };
149
+
150
+ const selected${names.pascalCase}Ids = selected${names.pluralPascal}.map((${names.camelCase}: ${names.pascalCase}MultiSelectType) => ${names.camelCase}.id);
151
+
152
+ return (
153
+ <div className="flex w-full flex-col">
154
+ <FormField
155
+ control={form.control}
156
+ name={id}
157
+ render={({ field }) => (
158
+ <FormItem className={\`\${label ? "mb-5" : "mb-1"}\`}>
159
+ {label && (
160
+ <FormLabel className="flex items-center">
161
+ {label}
162
+ {isRequired && <span className="text-destructive ml-2 font-semibold">*</span>}
163
+ </FormLabel>
164
+ )}
165
+ <FormControl>
166
+ <MultiSelect
167
+ options={${names.camelCase}Options}
168
+ onValueChange={handleValueChange}
169
+ defaultValue={selected${names.pascalCase}Ids}
170
+ placeholder={placeholder}
171
+ maxCount={maxCount}
172
+ animation={0}
173
+ onSearchChange={setSearchTerm}
174
+ />
175
+ </FormControl>
176
+ <FormMessage />
177
+ </FormItem>
178
+ )}
179
+ />
180
+ </div>
181
+ );
182
+ }
183
+ `;
184
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Selector Template
3
+ *
4
+ * Generates {Module}Selector.tsx searchable combobox component.
5
+ */
6
+
7
+ import { FrontendTemplateData } from "../../types/template-data.interface";
8
+
9
+ /**
10
+ * Generate the selector component file content
11
+ *
12
+ * @param data - Frontend template data
13
+ * @returns Generated file content
14
+ */
15
+ export function generateSelectorTemplate(data: FrontendTemplateData): string {
16
+ const { names } = data;
17
+
18
+ return `"use client";
19
+
20
+ import {
21
+ Command,
22
+ CommandItem,
23
+ CommandList,
24
+ FormControl,
25
+ FormField,
26
+ FormItem,
27
+ FormLabel,
28
+ FormMessage,
29
+ Input,
30
+ Popover,
31
+ PopoverContent,
32
+ PopoverTrigger,
33
+ } from "@carlonicora/nextjs-jsonapi/shadcnui";
34
+ import { ${names.pascalCase}Interface } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
35
+ import { ${names.pascalCase}Service } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Service";
36
+ import { DataListRetriever, useDataListRetriever } from "@carlonicora/nextjs-jsonapi/hooks";
37
+ import { useDebounce } from "@carlonicora/nextjs-jsonapi/hooks";
38
+ import { Modules } from "@carlonicora/nextjs-jsonapi/core";
39
+
40
+ import { CircleX, RefreshCwIcon, SearchIcon, XIcon } from "lucide-react";
41
+ import { useTranslations } from "next-intl";
42
+ import { useCallback, useEffect, useRef, useState } from "react";
43
+
44
+ type ${names.pascalCase}SelectorProps = {
45
+ id: string;
46
+ form: any;
47
+ label?: string;
48
+ placeholder?: string;
49
+ onChange?: (${names.camelCase}?: ${names.pascalCase}Interface) => void;
50
+ isRequired?: boolean;
51
+ };
52
+
53
+ export default function ${names.pascalCase}Selector({
54
+ id,
55
+ form,
56
+ label,
57
+ placeholder,
58
+ onChange,
59
+ isRequired = false,
60
+ }: ${names.pascalCase}SelectorProps) {
61
+ const t = useTranslations();
62
+
63
+ const [open, setOpen] = useState<boolean>(false);
64
+
65
+ const searchTermRef = useRef<string>("");
66
+ const [searchTerm, setSearchTerm] = useState<string>("");
67
+
68
+ const [isSearching, setIsSearching] = useState<boolean>(false);
69
+
70
+ const data: DataListRetriever<${names.pascalCase}Interface> = useDataListRetriever({
71
+ retriever: (params) => {
72
+ return ${names.pascalCase}Service.findMany(params);
73
+ },
74
+ retrieverParams: {},
75
+ module: Modules.${names.pascalCase},
76
+ });
77
+
78
+ const search = useCallback(
79
+ async (searchedTerm: string) => {
80
+ try {
81
+ if (searchedTerm === searchTermRef.current) return;
82
+ setIsSearching(true);
83
+ searchTermRef.current = searchedTerm;
84
+ await data.search(searchedTerm);
85
+ } finally {
86
+ setIsSearching(false);
87
+ }
88
+ },
89
+ [searchTermRef, data],
90
+ );
91
+
92
+ const updateSearchTerm = useDebounce(search, 500);
93
+
94
+ useEffect(() => {
95
+ setIsSearching(true);
96
+ updateSearchTerm(searchTerm);
97
+ }, [updateSearchTerm, searchTerm]);
98
+
99
+ const set${names.pascalCase} = (${names.camelCase}?: ${names.pascalCase}Interface) => {
100
+ if (onChange) onChange(${names.camelCase});
101
+ if (!${names.camelCase}) {
102
+ form.setValue(id, undefined);
103
+ setOpen(false);
104
+ return;
105
+ }
106
+
107
+ form.setValue(id, { id: ${names.camelCase}.id, name: ${names.camelCase}.name });
108
+ setOpen(false);
109
+
110
+ setTimeout(() => {
111
+ setOpen(false);
112
+ }, 0);
113
+ };
114
+
115
+ return (
116
+ <div className="flex w-full flex-col">
117
+ <FormField
118
+ control={form.control}
119
+ name={id}
120
+ render={({ field }) => (
121
+ <FormItem className={\`\${label ? "mb-5" : "mb-1"}\`}>
122
+ {label && (
123
+ <FormLabel className="flex items-center">
124
+ {label}
125
+ {isRequired && <span className="text-destructive ml-2 font-semibold">*</span>}
126
+ </FormLabel>
127
+ )}
128
+ <FormControl>
129
+ <Popover open={open} onOpenChange={setOpen} modal={true}>
130
+ <div className="flex w-full flex-row items-center justify-between">
131
+ <PopoverTrigger className="w-full">
132
+ <div className="flex w-full flex-row items-center justify-start rounded-md text-sm">
133
+ {field.value ? (
134
+ <>
135
+ <div className="flex w-full flex-row items-center justify-start rounded-md border p-2">
136
+ <span className="">{field.value?.name ?? ""}</span>
137
+ </div>
138
+ </>
139
+ ) : (
140
+ <div className="text-muted-foreground mr-7 flex h-10 w-full flex-row items-center justify-start rounded-md border p-2 text-sm">
141
+ {placeholder ?? t(\`generic.search.placeholder\`, { type: t(\`types.${names.pluralKebab}\`, { count: 1 }) })}
142
+ </div>
143
+ )}
144
+ </div>
145
+ </PopoverTrigger>
146
+ {field.value && (
147
+ <CircleX
148
+ className="text-muted hover:text-destructive ml-2 h-6 w-6 cursor-pointer"
149
+ onClick={() => set${names.pascalCase}()}
150
+ />
151
+ )}
152
+ </div>
153
+ <PopoverContent>
154
+ <Command shouldFilter={false}>
155
+ <div className="relative mb-2 w-full">
156
+ <SearchIcon className="text-muted-foreground absolute left-2.5 top-2.5 h-4 w-4" />
157
+ <Input
158
+ placeholder={t(\`generic.search.placeholder\`, { type: t(\`types.${names.pluralKebab}\`, { count: 1 }) })}
159
+ type="text"
160
+ className="w-full pl-8 pr-8"
161
+ onChange={(e) => setSearchTerm(e.target.value)}
162
+ value={searchTerm}
163
+ />
164
+ {isSearching ? (
165
+ <RefreshCwIcon className="text-muted-foreground absolute right-2.5 top-2.5 h-4 w-4 animate-spin" />
166
+ ) : searchTermRef.current ? (
167
+ <XIcon
168
+ className={\`absolute right-2.5 top-2.5 h-4 w-4 \${searchTermRef.current ? "cursor-pointer" : "text-muted-foreground"}\`}
169
+ onClick={() => {
170
+ setSearchTerm("");
171
+ search("");
172
+ }}
173
+ />
174
+ ) : (
175
+ <></>
176
+ )}
177
+ </div>
178
+ <CommandList>
179
+ {data.data &&
180
+ data.data.length > 0 &&
181
+ (data.data as ${names.pascalCase}Interface[]).map((${names.camelCase}: ${names.pascalCase}Interface) => (
182
+ <CommandItem className="cursor-pointer" key={${names.camelCase}.id} onSelect={() => set${names.pascalCase}(${names.camelCase})}>
183
+ <span className="">{${names.camelCase}.name}</span>
184
+ </CommandItem>
185
+ ))}
186
+ </CommandList>
187
+ </Command>
188
+ </PopoverContent>
189
+ </Popover>
190
+ </FormControl>
191
+ <FormMessage />
192
+ </FormItem>
193
+ )}
194
+ />
195
+ </div>
196
+ );
197
+ }
198
+ `;
199
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Context Template
3
+ *
4
+ * Generates {Module}Context.tsx for module state management.
5
+ */
6
+
7
+ import { FrontendTemplateData } from "../types/template-data.interface";
8
+
9
+ /**
10
+ * Generate the context file content
11
+ *
12
+ * @param data - Frontend template data
13
+ * @returns Generated file content
14
+ */
15
+ export function generateContextTemplate(data: FrontendTemplateData): string {
16
+ const { names, extendsContent } = data;
17
+
18
+ return `"use client";
19
+
20
+ import ${names.pascalCase}Deleter from "@/features/${data.targetDir}/${names.kebabCase}/components/forms/${names.pascalCase}Deleter";
21
+ import ${names.pascalCase}Editor from "@/features/${data.targetDir}/${names.kebabCase}/components/forms/${names.pascalCase}Editor";
22
+ import { ${names.pascalCase}Interface } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
23
+ import { ${names.pascalCase}Service } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Service";
24
+ import { SharedProvider } from "@carlonicora/nextjs-jsonapi/contexts";
25
+ import { usePageUrlGenerator } from "@carlonicora/nextjs-jsonapi/hooks";
26
+ import { BreadcrumbItemData } from "@carlonicora/nextjs-jsonapi/interfaces";
27
+
28
+ import { JsonApiHydratedDataInterface, Modules, rehydrate } from "@carlonicora/nextjs-jsonapi/core";
29
+ import { useTranslations } from "next-intl";
30
+ import { createContext, ReactNode, useContext, useState } from "react";
31
+
32
+ interface ${names.pascalCase}ContextType {
33
+ ${names.camelCase}: ${names.pascalCase}Interface | undefined;
34
+ set${names.pascalCase}: (value: ${names.pascalCase}Interface | undefined) => void;
35
+ reload${names.pascalCase}: () => Promise<void>;
36
+ }
37
+
38
+ const ${names.pascalCase}Context = createContext<${names.pascalCase}ContextType | undefined>(undefined);
39
+
40
+ type ${names.pascalCase}ProviderProps = {
41
+ children: ReactNode;
42
+ dehydrated${names.pascalCase}?: JsonApiHydratedDataInterface;
43
+ };
44
+
45
+ export const ${names.pascalCase}Provider = ({ children, dehydrated${names.pascalCase} }: ${names.pascalCase}ProviderProps) => {
46
+ const generateUrl = usePageUrlGenerator();
47
+ const t = useTranslations();
48
+
49
+ const [${names.camelCase}, set${names.pascalCase}] = useState<${names.pascalCase}Interface | undefined>(
50
+ dehydrated${names.pascalCase} ? rehydrate<${names.pascalCase}Interface>(Modules.${names.pascalCase}, dehydrated${names.pascalCase}) : undefined,
51
+ );
52
+
53
+ const reload${names.pascalCase} = async () => {
54
+ if (!${names.camelCase}) return;
55
+
56
+ const fresh${names.pascalCase} = await ${names.pascalCase}Service.findOne({ id: ${names.camelCase}.id });
57
+ set${names.pascalCase}(fresh${names.pascalCase});
58
+ };
59
+
60
+ const breadcrumb = () => {
61
+ const response: BreadcrumbItemData[] = [];
62
+
63
+ response.push({
64
+ name: t(\`types.${names.pluralCamel}\`, { count: 2 }),
65
+ href: generateUrl({ page: Modules.${names.pascalCase} }),
66
+ });
67
+
68
+ if (${names.camelCase})
69
+ response.push({
70
+ name: ${names.camelCase}.name,
71
+ href: generateUrl({ page: Modules.${names.pascalCase}, id: ${names.camelCase}.id }),
72
+ });
73
+
74
+ return response;
75
+ };
76
+
77
+ const title = () => {
78
+ const response: any = {
79
+ type: t(\`types.${names.pluralCamel}\`, { count: ${names.camelCase} ? 1 : 2 }),
80
+ };
81
+
82
+ const functions: ReactNode[] = [];
83
+
84
+ if (${names.camelCase}) {
85
+ response.element = ${names.camelCase}.name;
86
+
87
+ functions.push(<${names.pascalCase}Deleter key={\`${names.pascalCase}Deleter\`} ${names.camelCase}={${names.camelCase}} />);
88
+ functions.push(<${names.pascalCase}Editor key={\`${names.pascalCase}Editor\`} ${names.camelCase}={${names.camelCase}} propagateChanges={set${names.pascalCase}} />);
89
+ } else {
90
+ functions.push(<${names.pascalCase}Editor key={\`${names.pascalCase}Editor\`} />);
91
+ }
92
+
93
+ if (functions.length > 0) response.functions = functions;
94
+
95
+ return response;
96
+ };
97
+
98
+ return (
99
+ <SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>
100
+ <${names.pascalCase}Context.Provider
101
+ value={{
102
+ ${names.camelCase}: ${names.camelCase},
103
+ set${names.pascalCase}: set${names.pascalCase},
104
+ reload${names.pascalCase}: reload${names.pascalCase},
105
+ }}
106
+ >
107
+ {children}
108
+ </${names.pascalCase}Context.Provider>
109
+ </SharedProvider>
110
+ );
111
+ };
112
+
113
+ export const use${names.pascalCase}Context = (): ${names.pascalCase}ContextType => {
114
+ const context = useContext(${names.pascalCase}Context);
115
+ if (context === undefined) {
116
+ throw new Error("use${names.pascalCase}Context must be used within a ${names.pascalCase}Provider");
117
+ }
118
+ return context;
119
+ };
120
+ `;
121
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Fields Template
3
+ *
4
+ * Generates {Module}Fields.ts enum file for type-safe field references.
5
+ */
6
+
7
+ import { FrontendTemplateData } from "../../types/template-data.interface";
8
+ import { toCamelCase, pluralize } from "../../transformers/name-transformer";
9
+
10
+ /**
11
+ * Generate the fields enum file content
12
+ *
13
+ * @param data - Frontend template data
14
+ * @returns Generated file content
15
+ */
16
+ export function generateFieldsTemplate(data: FrontendTemplateData): string {
17
+ const { names, fields, relationships, tableFieldNames } = data;
18
+
19
+ const enumEntries = generateEnumEntries(data);
20
+
21
+ return `export enum ${names.pascalCase}Fields {
22
+ ${enumEntries}
23
+ }
24
+ `;
25
+ }
26
+
27
+ /**
28
+ * Generate enum entries
29
+ */
30
+ function generateEnumEntries(data: FrontendTemplateData): string {
31
+ const { names, fields, relationships } = data;
32
+ const entries: string[] = [];
33
+
34
+ // Add module ID field
35
+ entries.push(` ${names.camelCase}Id = "${names.camelCase}Id",`);
36
+ entries.push(``);
37
+
38
+ // Add primitive fields
39
+ const fieldNames = fields.map((f) => f.name);
40
+ fieldNames.forEach((name) => {
41
+ entries.push(` ${name} = "${name}",`);
42
+ });
43
+
44
+ if (relationships.length > 0) {
45
+ entries.push(``);
46
+ }
47
+
48
+ // Add relationship fields
49
+ relationships.forEach((rel) => {
50
+ const effectiveName = rel.variant || rel.name;
51
+ const key = rel.single
52
+ ? toCamelCase(effectiveName)
53
+ : pluralize(toCamelCase(rel.name));
54
+ entries.push(` ${key} = "${key}",`);
55
+ });
56
+
57
+ entries.push(``);
58
+
59
+ // Add standard timestamp fields
60
+ entries.push(` createdAt = "createdAt",`);
61
+ entries.push(` updatedAt = "updatedAt",`);
62
+
63
+ return entries.join("\n");
64
+ }