@memberjunction/codegen-lib 3.4.0 → 4.1.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 (94) hide show
  1. package/README.md +828 -630
  2. package/dist/Angular/angular-codegen.d.ts +10 -3
  3. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  4. package/dist/Angular/angular-codegen.js +164 -199
  5. package/dist/Angular/angular-codegen.js.map +1 -1
  6. package/dist/Angular/entity-data-grid-related-entity-component.d.ts +1 -1
  7. package/dist/Angular/entity-data-grid-related-entity-component.d.ts.map +1 -1
  8. package/dist/Angular/entity-data-grid-related-entity-component.js +8 -10
  9. package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
  10. package/dist/Angular/join-grid-related-entity-component.d.ts +1 -1
  11. package/dist/Angular/join-grid-related-entity-component.js +8 -36
  12. package/dist/Angular/join-grid-related-entity-component.js.map +1 -1
  13. package/dist/Angular/related-entity-components.js +13 -79
  14. package/dist/Angular/related-entity-components.js.map +1 -1
  15. package/dist/Angular/timeline-related-entity-component.d.ts +1 -1
  16. package/dist/Angular/timeline-related-entity-component.js +14 -38
  17. package/dist/Angular/timeline-related-entity-component.js.map +1 -1
  18. package/dist/Config/config.d.ts.map +1 -1
  19. package/dist/Config/config.js +171 -177
  20. package/dist/Config/config.js.map +1 -1
  21. package/dist/Config/db-connection.d.ts +1 -1
  22. package/dist/Config/db-connection.d.ts.map +1 -1
  23. package/dist/Config/db-connection.js +6 -33
  24. package/dist/Config/db-connection.js.map +1 -1
  25. package/dist/Database/dbSchema.js +28 -35
  26. package/dist/Database/dbSchema.js.map +1 -1
  27. package/dist/Database/manage-metadata.d.ts +302 -6
  28. package/dist/Database/manage-metadata.d.ts.map +1 -1
  29. package/dist/Database/manage-metadata.js +1294 -445
  30. package/dist/Database/manage-metadata.js.map +1 -1
  31. package/dist/Database/reorder-columns.d.ts +1 -1
  32. package/dist/Database/reorder-columns.d.ts.map +1 -1
  33. package/dist/Database/reorder-columns.js +1 -5
  34. package/dist/Database/reorder-columns.js.map +1 -1
  35. package/dist/Database/sql.d.ts +1 -1
  36. package/dist/Database/sql.d.ts.map +1 -1
  37. package/dist/Database/sql.js +67 -98
  38. package/dist/Database/sql.js.map +1 -1
  39. package/dist/Database/sql_codegen.d.ts +15 -2
  40. package/dist/Database/sql_codegen.d.ts.map +1 -1
  41. package/dist/Database/sql_codegen.js +282 -253
  42. package/dist/Database/sql_codegen.js.map +1 -1
  43. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +11 -0
  44. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
  45. package/dist/Manifest/GenerateClassRegistrationsManifest.js +43 -41
  46. package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
  47. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  48. package/dist/Misc/action_subclasses_codegen.js +15 -26
  49. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  50. package/dist/Misc/advanced_generation.d.ts +69 -7
  51. package/dist/Misc/advanced_generation.d.ts.map +1 -1
  52. package/dist/Misc/advanced_generation.js +114 -75
  53. package/dist/Misc/advanced_generation.js.map +1 -1
  54. package/dist/Misc/createNewUser.d.ts +1 -1
  55. package/dist/Misc/createNewUser.js +22 -26
  56. package/dist/Misc/createNewUser.js.map +1 -1
  57. package/dist/Misc/entity_subclasses_codegen.d.ts +7 -2
  58. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  59. package/dist/Misc/entity_subclasses_codegen.js +56 -45
  60. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  61. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  62. package/dist/Misc/graphql_server_codegen.js +39 -42
  63. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  64. package/dist/Misc/runCommand.d.ts +1 -1
  65. package/dist/Misc/runCommand.js +13 -20
  66. package/dist/Misc/runCommand.js.map +1 -1
  67. package/dist/Misc/sql_logging.d.ts +1 -1
  68. package/dist/Misc/sql_logging.d.ts.map +1 -1
  69. package/dist/Misc/sql_logging.js +21 -51
  70. package/dist/Misc/sql_logging.js.map +1 -1
  71. package/dist/Misc/status_logging.js +45 -60
  72. package/dist/Misc/status_logging.js.map +1 -1
  73. package/dist/Misc/system_integrity.d.ts +1 -1
  74. package/dist/Misc/system_integrity.d.ts.map +1 -1
  75. package/dist/Misc/system_integrity.js +12 -16
  76. package/dist/Misc/system_integrity.js.map +1 -1
  77. package/dist/Misc/temp_batch_file.js +15 -22
  78. package/dist/Misc/temp_batch_file.js.map +1 -1
  79. package/dist/Misc/util.d.ts.map +1 -1
  80. package/dist/Misc/util.js +17 -28
  81. package/dist/Misc/util.js.map +1 -1
  82. package/dist/__tests__/metadataConfig.test.d.ts +12 -0
  83. package/dist/__tests__/metadataConfig.test.d.ts.map +1 -0
  84. package/dist/__tests__/metadataConfig.test.js +604 -0
  85. package/dist/__tests__/metadataConfig.test.js.map +1 -0
  86. package/dist/index.d.ts +21 -21
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +21 -41
  89. package/dist/index.js.map +1 -1
  90. package/dist/runCodeGen.d.ts +1 -0
  91. package/dist/runCodeGen.d.ts.map +1 -1
  92. package/dist/runCodeGen.js +150 -178
  93. package/dist/runCodeGen.js.map +1 -1
  94. package/package.json +29 -23
@@ -1,84 +1,38 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AngularClientGeneratorBase = exports.AngularFormSectionInfo = void 0;
7
- const core_1 = require("@memberjunction/core");
8
- const status_logging_1 = require("../Misc/status_logging");
9
- const fs_1 = __importDefault(require("fs"));
10
- const path_1 = __importDefault(require("path"));
11
- const config_1 = require("../Config/config");
12
- const related_entity_components_1 = require("./related-entity-components");
13
- const util_1 = require("../Misc/util");
1
+ import { GeneratedFormSectionType, EntityFieldTSType, EntityFieldValueListType, Metadata } from '@memberjunction/core';
2
+ import { logError, logStatus } from '../Misc/status_logging.js';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { mjCoreSchema, outputOptionValue, configInfo } from '../Config/config.js';
6
+ import { RelatedEntityDisplayComponentGeneratorBase } from './related-entity-components.js';
7
+ import { sortBySequenceAndCreatedAt } from '../Misc/util.js';
14
8
  /**
15
9
  * Represents metadata about an Angular form section that is generated for an entity
16
10
  */
17
- class AngularFormSectionInfo {
18
- /**
19
- * The type of form section (e.g., Top, Category, Details)
20
- */
21
- Type;
22
- /**
23
- * The display name of the section
24
- */
25
- Name;
26
- /**
27
- * The generated HTML code for the section (panel or tab)
28
- */
29
- TabCode;
30
- /**
31
- * The TypeScript class name for the section component
32
- */
33
- ClassName;
34
- /**
35
- * The filename where the section component will be saved
36
- */
37
- FileName;
38
- /**
39
- * The complete TypeScript component code for the section
40
- */
41
- ComponentCode;
42
- /**
43
- * Array of entity fields that belong to this section
44
- */
45
- Fields;
46
- /**
47
- * The filename without the .ts extension
48
- */
49
- FileNameWithoutExtension;
50
- /**
51
- * The class name of the entity this section belongs to
52
- */
53
- EntityClassName;
54
- /**
55
- * Indicates if this section represents a related entity tab
56
- */
57
- IsRelatedEntity = false;
58
- /**
59
- * Specifies where related entity tabs should be displayed relative to field tabs
60
- */
61
- RelatedEntityDisplayLocation = 'After Field Tabs';
62
- /**
63
- * The generation result for related entity components
64
- */
65
- GeneratedOutput;
66
- /**
67
- * The minimum sequence number from fields in this section (used for sorting)
68
- */
69
- MinSequence;
70
- /**
71
- * The unique camelCase key used for this section in the sectionsExpanded object
72
- */
73
- UniqueKey;
11
+ export class AngularFormSectionInfo {
12
+ constructor() {
13
+ /**
14
+ * Indicates if this section represents a related entity tab
15
+ */
16
+ this.IsRelatedEntity = false;
17
+ /**
18
+ * Specifies where related entity tabs should be displayed relative to field tabs
19
+ */
20
+ this.RelatedEntityDisplayLocation = 'After Field Tabs';
21
+ }
74
22
  }
75
- exports.AngularFormSectionInfo = AngularFormSectionInfo;
76
23
  /**
77
24
  * Base class for generating Angular client code for MemberJunction entities.
78
25
  * This class handles the generation of Angular components, forms, and modules based on entity metadata.
79
26
  * You can sub-class this class to create your own Angular client code generator logic.
80
27
  */
81
- class AngularClientGeneratorBase {
28
+ export class AngularClientGeneratorBase {
29
+ constructor() {
30
+ /**
31
+ * Base name used for generating sub-module names
32
+ * @protected
33
+ */
34
+ this.subModule_BaseName = 'GeneratedForms_SubModule_';
35
+ }
82
36
  /**
83
37
  * Main entry point for generating Angular code for a collection of entities
84
38
  * @param entities Array of EntityInfo objects to generate Angular code for
@@ -89,24 +43,24 @@ class AngularClientGeneratorBase {
89
43
  */
90
44
  async generateAngularCode(entities, directory, modulePrefix, contextUser) {
91
45
  try {
92
- const entityPath = path_1.default.join(directory, 'Entities');
46
+ const entityPath = path.join(directory, 'Entities');
93
47
  //const classMapEntries: string[] = [];
94
48
  const componentImports = [];
95
49
  const relatedEntityModuleImports = [];
96
50
  const componentNames = [];
97
51
  const sections = [];
98
- if (!fs_1.default.existsSync(entityPath))
99
- fs_1.default.mkdirSync(entityPath, { recursive: true }); // create the directory if it doesn't exist
52
+ if (!fs.existsSync(entityPath))
53
+ fs.mkdirSync(entityPath, { recursive: true }); // create the directory if it doesn't exist
100
54
  for (let i = 0; i < entities.length; ++i) {
101
55
  const entity = entities[i];
102
56
  if (entity.PrimaryKeys && entity.PrimaryKeys.length > 0 && entity.IncludeInAPI) {
103
- const thisEntityPath = path_1.default.join(entityPath, entity.ClassName);
104
- if (!fs_1.default.existsSync(thisEntityPath))
105
- fs_1.default.mkdirSync(thisEntityPath, { recursive: true }); // create the directory if it doesn't exist
57
+ const thisEntityPath = path.join(entityPath, entity.ClassName);
58
+ if (!fs.existsSync(thisEntityPath))
59
+ fs.mkdirSync(thisEntityPath, { recursive: true }); // create the directory if it doesn't exist
106
60
  const { htmlCode, additionalSections, relatedEntitySections } = await this.generateSingleEntityHTMLForAngular(entity, contextUser);
107
61
  const tsCode = this.generateSingleEntityTypeScriptForAngular(entity, additionalSections, relatedEntitySections);
108
- fs_1.default.writeFileSync(path_1.default.join(thisEntityPath, `${entity.ClassName.toLowerCase()}.form.component.ts`), tsCode);
109
- fs_1.default.writeFileSync(path_1.default.join(thisEntityPath, `${entity.ClassName.toLowerCase()}.form.component.html`), htmlCode);
62
+ fs.writeFileSync(path.join(thisEntityPath, `${entity.ClassName.toLowerCase()}.form.component.ts`), tsCode);
63
+ fs.writeFileSync(path.join(thisEntityPath, `${entity.ClassName.toLowerCase()}.form.component.html`), htmlCode);
110
64
  // Sections are now inline in HTML templates, no separate files needed
111
65
  // Just track them for module generation purposes
112
66
  if (additionalSections.length > 0) {
@@ -115,7 +69,7 @@ class AngularClientGeneratorBase {
115
69
  });
116
70
  }
117
71
  const componentName = `${entity.ClassName}FormComponent`;
118
- componentImports.push(`import { ${componentName}, Load${componentName} } from "./Entities/${entity.ClassName}/${entity.ClassName.toLowerCase()}.form.component";`);
72
+ componentImports.push(`import { ${componentName} } from "./Entities/${entity.ClassName}/${entity.ClassName.toLowerCase()}.form.component";`);
119
73
  const currentComponentDistinctRelatedEntityClassNames = [];
120
74
  relatedEntitySections.forEach(s => s.GeneratedOutput.Component.ImportItems.forEach(i => {
121
75
  if (!currentComponentDistinctRelatedEntityClassNames.find(ii => ii.itemClassName === i.ClassName))
@@ -140,16 +94,16 @@ class AngularClientGeneratorBase {
140
94
  // now the imports are good
141
95
  }
142
96
  else {
143
- (0, status_logging_1.logStatus)(` Entity ${entity.Name} does not have a primary key or is not included in the API, skipping code generation for this entity`);
97
+ logStatus(` Entity ${entity.Name} does not have a primary key or is not included in the API, skipping code generation for this entity`);
144
98
  }
145
99
  }
146
- const maxComponentsPerModule = (0, config_1.outputOptionValue)('Angular', 'maxComponentsPerModule', 25);
100
+ const maxComponentsPerModule = outputOptionValue('Angular', 'maxComponentsPerModule', 25);
147
101
  const moduleCode = this.generateAngularModule(componentImports, componentNames, relatedEntityModuleImports, sections, modulePrefix, maxComponentsPerModule);
148
- fs_1.default.writeFileSync(path_1.default.join(directory, 'generated-forms.module.ts'), moduleCode);
102
+ fs.writeFileSync(path.join(directory, 'generated-forms.module.ts'), moduleCode);
149
103
  return true;
150
104
  }
151
105
  catch (err) {
152
- (0, status_logging_1.logError)(err);
106
+ logError(err);
153
107
  return false;
154
108
  }
155
109
  }
@@ -184,19 +138,11 @@ import { FormsModule } from '@angular/forms';
184
138
 
185
139
  // MemberJunction Imports
186
140
  import { BaseFormsModule } from '@memberjunction/ng-base-forms';
187
- import { FormToolbarModule } from '@memberjunction/ng-form-toolbar';
188
141
  import { EntityViewerModule } from '@memberjunction/ng-entity-viewer';
189
142
  import { LinkDirectivesModule } from '@memberjunction/ng-link-directives';
190
143
  import { MJTabStripModule } from "@memberjunction/ng-tabstrip";
191
144
  import { ContainerDirectivesModule } from "@memberjunction/ng-container-directives";
192
-
193
- // Kendo Imports
194
- import { InputsModule } from '@progress/kendo-angular-inputs';
195
- import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
196
- import { ButtonsModule } from '@progress/kendo-angular-buttons';
197
145
  import { LayoutModule } from '@progress/kendo-angular-layout';
198
- import { ComboBoxModule } from '@progress/kendo-angular-dropdowns';
199
- import { DropDownListModule } from '@progress/kendo-angular-dropdowns';
200
146
 
201
147
  // Import Generated Components
202
148
  ${componentImports.join('\n')}
@@ -205,14 +151,9 @@ ${relatedEntityModuleImports.filter(remi => remi.library.trim().toLowerCase() !=
205
151
  .join('\n')}
206
152
  ${moduleCode}
207
153
 
208
- export function Load${modulePrefix}GeneratedForms() {
209
- // This function doesn't do much, but it calls each generated form's loader function
210
- // which in turn calls the sections for that generated form. Ultimately, those bits of
211
- // code do NOTHING - the point is to prevent the code from being eliminated during tree shaking
212
- // since it is dynamically instantiated on demand, and the Angular compiler has no way to know that,
213
- // in production builds tree shaking will eliminate the code unless we do this
214
- ${componentNames.map(c => `Load${c.componentName}();`).join('\n ')}
215
- }
154
+ // Note: LoadXXXGeneratedForms() functions have been removed. Tree-shaking prevention
155
+ // is now handled by the pre-built class registration manifest system.
156
+ // See packages/CodeGenLib/CLASS_MANIFEST_GUIDE.md for details.
216
157
  `;
217
158
  }
218
159
  /**
@@ -274,11 +215,6 @@ export class ${modulePrefix}GeneratedFormsModule { }`;
274
215
  // so we need to combine the two into a single return value and send back to the caller
275
216
  return subModules.join('\n\n') + '\n\n' + masterModuleCode;
276
217
  }
277
- /**
278
- * Base name used for generating sub-module names
279
- * @protected
280
- */
281
- subModule_BaseName = 'GeneratedForms_SubModule_';
282
218
  /**
283
219
  * Get the base name for the sub-modules. Override this method to change the base name.
284
220
  * @returns The base name for sub-modules (default: 'GeneratedForms_SubModule_')
@@ -298,17 +234,11 @@ imports: [
298
234
  CommonModule,
299
235
  FormsModule,
300
236
  LayoutModule,
301
- InputsModule,
302
- ButtonsModule,
303
- DateInputsModule,
237
+ BaseFormsModule,
304
238
  EntityViewerModule,
305
239
  LinkDirectivesModule,
306
- BaseFormsModule,
307
- FormToolbarModule,
308
240
  MJTabStripModule,
309
- ContainerDirectivesModule,
310
- DropDownListModule,
311
- ComboBoxModule${additionalModulesToImport.length > 0 ? ',\n ' + additionalModulesToImport.join(',\n ') : ''}
241
+ ContainerDirectivesModule${additionalModulesToImport.length > 0 ? ',\n ' + additionalModulesToImport.join(',\n ') : ''}
312
242
  ],
313
243
  exports: [
314
244
  ]
@@ -355,7 +285,7 @@ export class ${this.SubModuleBaseName}${moduleNumber} { }
355
285
  relatedEntitySections.filter(s => s.GeneratedOutput && s.GeneratedOutput.CodeOutput.length > 0)
356
286
  .map(s => s.GeneratedOutput.CodeOutput.split("\n").map(l => ` ${l}`).join("\n")).join('\n') : '';
357
287
  // Generate unique keys for all sections FIRST, then use them everywhere
358
- const sectionsWithoutTop = additionalSections.filter(s => s.Type !== core_1.GeneratedFormSectionType.Top && s.Name);
288
+ const sectionsWithoutTop = additionalSections.filter(s => s.Type !== GeneratedFormSectionType.Top && s.Name);
359
289
  const allSections = [...sectionsWithoutTop, ...relatedEntitySections];
360
290
  // Assign unique keys to each section
361
291
  const usedKeys = new Set();
@@ -390,14 +320,15 @@ export class ${this.SubModuleBaseName}${moduleNumber} { }
390
320
  const sectionInitCode = sectionInitEntries.length > 0
391
321
  ? `\n\n override async ngOnInit() {\n await super.ngOnInit();\n this.initSections([\n${sectionInitEntries.join(',\n')}\n ]);\n }`
392
322
  : '';
393
- const entityPackageName = config_1.configInfo.entityPackageName || 'mj_generatedentities';
323
+ const entityPackageName = configInfo.entityPackageName || 'mj_generatedentities';
394
324
  return `import { Component } from '@angular/core';
395
- import { ${entityObjectClass}Entity } from '${entity.SchemaName === config_1.mjCoreSchema ? '@memberjunction/core-entities' : entityPackageName}';
325
+ import { ${entityObjectClass}Entity } from '${entity.SchemaName === mjCoreSchema ? '@memberjunction/core-entities' : entityPackageName}';
396
326
  import { RegisterClass } from '@memberjunction/global';
397
327
  import { BaseFormComponent } from '@memberjunction/ng-base-forms';
398
328
  ${generationImports.length > 0 ? generationImports + '\n' : ''}
399
329
  @RegisterClass(BaseFormComponent, '${entity.Name}') // Tell MemberJunction about this class
400
330
  @Component({
331
+ standalone: false,
401
332
  selector: 'gen-${entity.ClassName.toLowerCase()}-form',
402
333
  templateUrl: './${entity.ClassName.toLowerCase()}.form.component.html'
403
334
  })
@@ -405,9 +336,6 @@ export class ${entity.ClassName}FormComponent extends BaseFormComponent {
405
336
  public record!: ${entityObjectClass}Entity;${generationInjectedCode.length > 0 ? '\n' + generationInjectedCode : ''}${sectionInitCode}
406
337
  }
407
338
 
408
- export function Load${entity.ClassName}FormComponent() {
409
- // does nothing, but called to prevent tree-shaking from eliminating this component from the build
410
- }
411
339
  `;
412
340
  }
413
341
  /**
@@ -416,7 +344,7 @@ export function Load${entity.ClassName}FormComponent() {
416
344
  * @returns True if the entity has top area fields, false otherwise
417
345
  */
418
346
  entityHasTopArea(entity) {
419
- return entity.Fields.some(f => f.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Top);
347
+ return entity.Fields.some(f => f.GeneratedFormSectionType === GeneratedFormSectionType.Top);
420
348
  }
421
349
  /**
422
350
  * Generates HTML for the top area section of an entity form
@@ -466,26 +394,27 @@ export function Load${entity.ClassName}FormComponent() {
466
394
  * @param categoryIcons Optional map of category names to Font Awesome icon classes
467
395
  * @returns Array of generated form sections
468
396
  */
469
- generateAngularAdditionalSections(entity, startIndex, categoryIcons) {
397
+ generateAngularAdditionalSections(entity, startIndex, fieldCategories) {
470
398
  const sections = [];
471
399
  let index = startIndex;
472
- const sortedFields = (0, util_1.sortBySequenceAndCreatedAt)(entity.Fields);
400
+ const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
473
401
  for (const field of sortedFields) {
474
402
  if (field.IncludeInGeneratedForm) {
475
- if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Category && field.Category && field.Category !== '' && field.IncludeInGeneratedForm)
476
- this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Category, field.Category, field.Sequence);
477
- else if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Details)
478
- this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Details, "Details", field.Sequence);
479
- else if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Top)
480
- this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Top, "Top", field.Sequence);
403
+ if (field.GeneratedFormSectionType === GeneratedFormSectionType.Category && field.Category && field.Category !== '' && field.IncludeInGeneratedForm)
404
+ this.AddSectionIfNeeded(entity, sections, GeneratedFormSectionType.Category, field.Category, field.Sequence);
405
+ else if (field.GeneratedFormSectionType === GeneratedFormSectionType.Details)
406
+ this.AddSectionIfNeeded(entity, sections, GeneratedFormSectionType.Details, "Details", field.Sequence);
407
+ else if (field.GeneratedFormSectionType === GeneratedFormSectionType.Top)
408
+ this.AddSectionIfNeeded(entity, sections, GeneratedFormSectionType.Top, "Top", field.Sequence);
481
409
  }
482
410
  }
483
- // Sort sections by minimum sequence (Top sections first, System sections last, then by MinSequence)
411
+ // Sort sections: Top first, then own categories, then inherited categories
412
+ // (nearest parent first), then System Metadata last
484
413
  sections.sort((a, b) => {
485
414
  // Top sections always first
486
- if (a.Type === core_1.GeneratedFormSectionType.Top)
415
+ if (a.Type === GeneratedFormSectionType.Top)
487
416
  return -1;
488
- if (b.Type === core_1.GeneratedFormSectionType.Top)
417
+ if (b.Type === GeneratedFormSectionType.Top)
489
418
  return 1;
490
419
  // System sections always last (after related entities)
491
420
  const aIsSystem = a.Name.toLowerCase() === 'system' || a.Name.toLowerCase() === 'system metadata';
@@ -494,6 +423,13 @@ export function Load${entity.ClassName}FormComponent() {
494
423
  return 1;
495
424
  if (!aIsSystem && bIsSystem)
496
425
  return -1;
426
+ // Inherited sections come after own sections (but before System)
427
+ const aIsInherited = fieldCategories?.[a.Name]?.inheritedFromEntityName != null;
428
+ const bIsInherited = fieldCategories?.[b.Name]?.inheritedFromEntityName != null;
429
+ if (aIsInherited && !bIsInherited)
430
+ return 1;
431
+ if (!aIsInherited && bIsInherited)
432
+ return -1;
497
433
  // Otherwise sort by sequence
498
434
  const aSeq = a.MinSequence ?? Number.MAX_SAFE_INTEGER;
499
435
  const bSeq = b.MinSequence ?? Number.MAX_SAFE_INTEGER;
@@ -503,19 +439,20 @@ export function Load${entity.ClassName}FormComponent() {
503
439
  let sectionIndex = 0;
504
440
  for (const section of sections) {
505
441
  let sectionName = '';
506
- if (section.Type === core_1.GeneratedFormSectionType.Top) {
442
+ if (section.Type === GeneratedFormSectionType.Top) {
507
443
  section.TabCode = this.generateTopAreaHTMLForAngular(entity);
508
444
  sectionName = 'top-area';
509
445
  }
510
446
  else {
511
- if (section.Type === core_1.GeneratedFormSectionType.Category)
447
+ if (section.Type === GeneratedFormSectionType.Category)
512
448
  sectionName = this.stripWhiteSpace(section.Name.toLowerCase());
513
- else if (section.Type === core_1.GeneratedFormSectionType.Details)
449
+ else if (section.Type === GeneratedFormSectionType.Details)
514
450
  sectionName = 'details';
515
451
  // Generate collapsible panel HTML inline instead of using separate components
516
452
  const formHTML = this.generateSectionHTMLForAngular(entity, section);
517
- // Use category-specific icon from LLM if available, otherwise fall back to keyword matching
518
- const icon = (categoryIcons && categoryIcons[section.Name]) || this.getIconForCategory(section.Name);
453
+ // Use category-specific icon from metadata if available, otherwise fall back to keyword matching
454
+ const categoryInfo = fieldCategories ? fieldCategories[section.Name] : undefined;
455
+ const icon = categoryInfo?.icon || this.getIconForCategory(section.Name);
519
456
  // NOTE: We'll set the UniqueKey later in generateSingleEntityTypeScriptForAngular()
520
457
  // For now, just use a placeholder that will be replaced
521
458
  const sectionKey = this.camelCase(section.Name);
@@ -529,18 +466,22 @@ export function Load${entity.ClassName}FormComponent() {
529
466
  }).join(' ') : '';
530
467
  // No additional indentation needed - formHTML is already properly indented
531
468
  const indentedFormHTML = formHTML;
469
+ // Build inherited variant attributes if this category comes from a parent entity
470
+ const inheritedAttrs = categoryInfo?.inheritedFromEntityName
471
+ ? `\n Variant="inherited"\n InheritedFromEntity="${categoryInfo.inheritedFromEntityName}"`
472
+ : '';
532
473
  section.TabCode = `${sectionIndex > 0 ? '\n' : ''} <!-- ${section.Name} Section -->
533
- <mj-collapsible-panel slot="field-panels"
534
- sectionKey="${sectionKey}"
535
- sectionName="${section.Name}"
536
- icon="${icon}"
537
- [form]="this"
538
- [formContext]="formContext">
474
+ <mj-collapsible-panel field-panels
475
+ SectionKey="${sectionKey}"
476
+ SectionName="${section.Name}"
477
+ Icon="${icon}"${inheritedAttrs}
478
+ [Form]="this"
479
+ [FormContext]="formContext">
539
480
  ${indentedFormHTML}
540
481
  </mj-collapsible-panel>`;
541
482
  sectionIndex++;
542
483
  }
543
- if (section.Type !== core_1.GeneratedFormSectionType.Top)
484
+ if (section.Type !== GeneratedFormSectionType.Top)
544
485
  index++; // don't increment the tab index for TOP AREA, becuse it won't be rendered as a tab
545
486
  }
546
487
  return sections;
@@ -555,23 +496,29 @@ ${indentedFormHTML}
555
496
  let html = '';
556
497
  // figure out which fields will be in this section first
557
498
  section.Fields = [];
558
- const sortedFields = (0, util_1.sortBySequenceAndCreatedAt)(entity.Fields);
499
+ const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
559
500
  for (const field of sortedFields) {
560
501
  if (field.IncludeInGeneratedForm) {
561
502
  let bMatch = false;
562
- if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Top && section.Type === core_1.GeneratedFormSectionType.Top) {
503
+ if (field.GeneratedFormSectionType === GeneratedFormSectionType.Top && section.Type === GeneratedFormSectionType.Top) {
563
504
  // match, include the field in the output
564
505
  bMatch = true;
565
506
  }
566
- else if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Category && field.Category && section.Name && field.Category.trim().toLowerCase() === section.Name.trim().toLowerCase()) {
507
+ else if (field.GeneratedFormSectionType === GeneratedFormSectionType.Category && field.Category && section.Name && field.Category.trim().toLowerCase() === section.Name.trim().toLowerCase()) {
567
508
  // match, include the field in the output
568
509
  bMatch = true;
569
510
  }
570
- else if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Details && section.Type === core_1.GeneratedFormSectionType.Details) {
511
+ else if (field.GeneratedFormSectionType === GeneratedFormSectionType.Details && section.Type === GeneratedFormSectionType.Details) {
571
512
  // match, include the field in the output
572
513
  bMatch = true;
573
514
  }
574
515
  if (bMatch && field.Name.toLowerCase() !== 'id') {
516
+ // Skip virtual fields that are the name-field-map of an FK field.
517
+ // The FK field itself will display the name via RelatedEntityNameFieldMap
518
+ // at runtime, so emitting the virtual field would be redundant.
519
+ if (field.IsVirtual && this.isVirtualNameFieldForFK(entity, field)) {
520
+ continue;
521
+ }
575
522
  section.Fields.push(field); // add the field to the section fields array
576
523
  }
577
524
  }
@@ -582,27 +529,27 @@ ${indentedFormHTML}
582
529
  if (!field.ReadOnly) {
583
530
  // first, check to see if we have a ValueListType != None, if so, generate a dropdown.
584
531
  // If value list type is ListOrUserEntry, then generate a combobox, if ValueListType = List, then generate a dropdown
585
- if (field.ValueListTypeEnum !== core_1.EntityFieldValueListType.None) {
532
+ if (field.ValueListTypeEnum !== EntityFieldValueListType.None) {
586
533
  // build the possible values list
587
- if (field.ValueListTypeEnum === core_1.EntityFieldValueListType.ListOrUserEntry) {
588
- // combo box
589
- editControl = `combobox`;
534
+ if (field.ValueListTypeEnum === EntityFieldValueListType.ListOrUserEntry) {
535
+ // autocomplete (allows user-entered values)
536
+ editControl = `autocomplete`;
590
537
  }
591
- else if (field.ValueListTypeEnum === core_1.EntityFieldValueListType.List) {
592
- // dropdown
593
- editControl = `dropdownlist`;
538
+ else if (field.ValueListTypeEnum === EntityFieldValueListType.List) {
539
+ // select (fixed list)
540
+ editControl = `select`;
594
541
  }
595
542
  }
596
543
  else {
597
544
  // no value list, generate a text box, checkbox, or date picker
598
- if (field.TSType === core_1.EntityFieldTSType.Boolean)
545
+ if (field.TSType === EntityFieldTSType.Boolean)
599
546
  editControl = `checkbox`;
600
- else if (field.TSType === core_1.EntityFieldTSType.Date)
547
+ else if (field.TSType === EntityFieldTSType.Date)
601
548
  editControl = `datepicker`;
602
- else if (field.TSType === core_1.EntityFieldTSType.Number)
603
- editControl = `numerictextbox`;
604
- else if (field.TSType === core_1.EntityFieldTSType.String) {
605
- if (field.Length < 0 || field.MaxLength > 100) // length < 0 means nvarchar(max) or similar, so use textarea
549
+ else if (field.TSType === EntityFieldTSType.Number)
550
+ editControl = `number`;
551
+ else if (field.TSType === EntityFieldTSType.String) {
552
+ if (field.Length < 0 || field.MaxLength > 1000) // length < 0 means nvarchar(max) or similar, so use textarea; nvarchar(1000) and below get single-line textbox
606
553
  editControl = `textarea`;
607
554
  else
608
555
  editControl = `textbox`;
@@ -613,10 +560,8 @@ ${indentedFormHTML}
613
560
  editControl = 'code';
614
561
  }
615
562
  let linkType = null;
616
- let linkComponentType = null;
617
563
  if (field.RelatedEntity && field.RelatedEntity.length > 0) {
618
564
  linkType = 'Record';
619
- linkComponentType = `\n LinkComponentType="${field.RelatedEntityDisplayType}"`;
620
565
  }
621
566
  else if (field.ExtendedType && field.ExtendedType.length > 0) {
622
567
  switch (field.ExtendedType.trim().toLowerCase()) {
@@ -630,17 +575,38 @@ ${indentedFormHTML}
630
575
  }
631
576
  // next, generate HTML for the field, use fillContainer if we have just one field
632
577
  html += ` <mj-form-field ${section.Fields.length === 1 ? '' : ''}
633
- [record]="record"
578
+ [Record]="record"
634
579
  [ShowLabel]="${section.Fields.length > 1 ? 'true' : 'false'}"
635
580
  FieldName="${field.CodeName}"
636
581
  Type="${editControl}"
637
582
  [EditMode]="EditMode"
638
- [formContext]="formContext"${linkType ? `\n LinkType="${linkType}"` : ''}${linkComponentType ? linkComponentType : ''}
583
+ [FormContext]="formContext"${linkType ? `\n LinkType="${linkType}"` : ''}
639
584
  ></mj-form-field>
640
585
  `;
641
586
  }
642
587
  return html;
643
588
  }
589
+ /**
590
+ * Checks whether a virtual field is the name-field-map target of an FK field on the same entity.
591
+ * For example, if entity has `ParentID` (FK) with `RelatedEntityNameFieldMap = 'Parent'`,
592
+ * then the virtual `Parent` field is redundant on the form since the FK field will display
593
+ * the name at runtime.
594
+ */
595
+ isVirtualNameFieldForFK(entity, virtualField) {
596
+ const vNameLower = virtualField.Name.toLowerCase();
597
+ return entity.Fields.some(f => {
598
+ if (f === virtualField || !f.RelatedEntity)
599
+ return false;
600
+ // 1. Exact match via RelatedEntityNameFieldMap
601
+ if (f.RelatedEntityNameFieldMap === virtualField.Name)
602
+ return true;
603
+ // 2. Case-insensitive convention: virtual "Foo" matches FK "FooID"
604
+ const fkLower = f.Name.toLowerCase();
605
+ if (fkLower === vNameLower + 'id')
606
+ return true;
607
+ return false;
608
+ });
609
+ }
644
610
  /**
645
611
  * Generates the tab name for a related entity tab. Appends the field's display name to the tab name
646
612
  * if there are multiple tabs for the same related entity to differentiate them.
@@ -664,7 +630,7 @@ ${indentedFormHTML}
664
630
  // if the fkeyField has wrapping [] then remove them
665
631
  fkeyField = fkeyField.trim().replace('[', '').replace(']', '');
666
632
  // let's get the actual entityInfo for the related entity so we can get the field and see if it has a display name
667
- const md = new core_1.Metadata();
633
+ const md = new Metadata();
668
634
  const re = md.EntityByID(relatedEntity.RelatedEntityID);
669
635
  const f = re.Fields.find(f => f.Name.trim().toLowerCase() === fkeyField.trim().toLowerCase());
670
636
  if (f)
@@ -682,7 +648,7 @@ ${indentedFormHTML}
682
648
  * @returns Promise resolving to array of related entity tab sections
683
649
  */
684
650
  async generateRelatedEntityTabs(entity, startIndex, contextUser) {
685
- const md = new core_1.Metadata();
651
+ const md = new Metadata();
686
652
  const tabs = [];
687
653
  // Sort related entities by Sequence (user's explicit ordering), then by RelatedEntity name (stable tiebreaker)
688
654
  const sortedRelatedEntities = entity.RelatedEntities
@@ -717,7 +683,7 @@ ${indentedFormHTML}
717
683
  }
718
684
  // Calculate section key before generation (may be replaced later if duplicate)
719
685
  const sectionKey = this.camelCase(tabName);
720
- const component = await related_entity_components_1.RelatedEntityDisplayComponentGeneratorBase.GetComponent(relatedEntity, contextUser);
686
+ const component = await RelatedEntityDisplayComponentGeneratorBase.GetComponent(relatedEntity, contextUser);
721
687
  const generateResults = await component.Generate({
722
688
  Entity: entity,
723
689
  RelationshipInfo: relatedEntity,
@@ -731,21 +697,23 @@ ${indentedFormHTML}
731
697
  // Determine slot based on DisplayLocation
732
698
  const slot = relatedEntity.DisplayLocation === 'Before Field Tabs' ? 'before-panels' : 'after-panels';
733
699
  const tabCode = `${index > 0 ? '\n' : ''} <!-- ${tabName} Section -->
734
- <mj-collapsible-panel slot="${slot}"
735
- sectionKey="${sectionKey}"
736
- sectionName="${tabName}"
737
- icon="${iconClass}"
738
- variant="related-entity"
739
- [form]="this"
740
- [formContext]="formContext"
741
- [badgeCount]="GetSectionRowCount('${sectionKey}')"
742
- [defaultExpanded]="false">
743
- <div *ngIf="record.IsSaved">
700
+ <mj-collapsible-panel ${slot}
701
+ SectionKey="${sectionKey}"
702
+ SectionName="${tabName}"
703
+ Icon="${iconClass}"
704
+ Variant="related-entity"
705
+ [Form]="this"
706
+ [FormContext]="formContext"
707
+ [BadgeCount]="GetSectionRowCount('${sectionKey}')"
708
+ [DefaultExpanded]="false">
709
+ @if (record.IsSaved) {
710
+ <div>
744
711
  ${componentCodeWithIndent}
745
712
  </div>
713
+ }
746
714
  </mj-collapsible-panel>`;
747
715
  tabs.push({
748
- Type: core_1.GeneratedFormSectionType.Category,
716
+ Type: GeneratedFormSectionType.Category,
749
717
  IsRelatedEntity: true,
750
718
  RelatedEntityDisplayLocation: relatedEntity.DisplayLocation,
751
719
  Name: tabName,
@@ -895,22 +863,10 @@ ${componentCodeWithIndent}
895
863
  * @returns Promise resolving to an object containing the HTML code and section information
896
864
  */
897
865
  async generateSingleEntityHTMLForAngular(entity, contextUser) {
898
- // Load category icons from EntitySetting if available
899
- let categoryIcons;
900
- const entitySettings = entity.Settings;
901
- if (entitySettings) {
902
- const iconSetting = entitySettings.find((s) => s.Name === 'FieldCategoryIcons');
903
- if (iconSetting && iconSetting.Value) {
904
- try {
905
- categoryIcons = JSON.parse(iconSetting.Value);
906
- }
907
- catch (e) {
908
- // Invalid JSON, ignore and fall back to keyword matching
909
- }
910
- }
911
- }
866
+ // Load category metadata (icons + inheritance info) from the typed FieldCategories property
867
+ const fieldCategories = entity.FieldCategories;
912
868
  const topArea = this.generateTopAreaHTMLForAngular(entity);
913
- const additionalSections = this.generateAngularAdditionalSections(entity, 0, categoryIcons);
869
+ const additionalSections = this.generateAngularAdditionalSections(entity, 0, fieldCategories);
914
870
  // calc ending index for additional sections so we can pass taht into the related entity tabs because they need to start incrementally up from there...
915
871
  const endingIndex = additionalSections && additionalSections.length ? (topArea && topArea.length > 0 ? additionalSections.length - 1 : additionalSections.length) : 0;
916
872
  const relatedEntitySections = await this.generateRelatedEntityTabs(entity, endingIndex, contextUser);
@@ -926,7 +882,12 @@ ${componentCodeWithIndent}
926
882
  * @returns Generated HTML with splitter layout
927
883
  */
928
884
  generateSingleEntityHTMLWithSplitterForAngular(topArea, additionalSections, relatedEntitySections) {
929
- const htmlCode = `<mj-record-form-container [record]="record" [formComponent]="this">
885
+ const htmlCode = `<mj-record-form-container [Record]="record" [FormComponent]="this"
886
+ (Navigate)="OnFormNavigate($event)"
887
+ (DeleteRequested)="OnDeleteRequested()"
888
+ (FavoriteToggled)="OnFavoriteToggled()"
889
+ (HistoryRequested)="OnHistoryRequested()"
890
+ (ListManagementRequested)="OnListManagementRequested()">
930
891
  <kendo-splitter orientation="vertical" (layoutChange)="splitterLayoutChange()">
931
892
  <kendo-splitter-pane [collapsible]="true" [size]="TopAreaHeight">
932
893
  ${this.innerTopAreaHTML(topArea)}
@@ -960,7 +921,7 @@ ${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
960
921
  */
961
922
  innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections) {
962
923
  // Filter out Top sections as they're handled separately
963
- const sectionsToRender = additionalSections.filter(s => s.Type !== core_1.GeneratedFormSectionType.Top);
924
+ const sectionsToRender = additionalSections.filter(s => s.Type !== GeneratedFormSectionType.Top);
964
925
  // Order: before-panels, field-panels, after-panels
965
926
  // The RecordFormContainer handles the related-entity-grid wrapper via named slots
966
927
  const beforePanels = relatedEntitySections.filter(s => s.RelatedEntityDisplayLocation === 'Before Field Tabs');
@@ -1003,7 +964,7 @@ ${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
1003
964
  const relatedEntityAfterFieldTabs = relatedEntitySections.filter(s => s.RelatedEntityDisplayLocation === 'After Field Tabs');
1004
965
  return ` <mj-tabstrip (TabSelected)="onTabSelect($event)" (ResizeContainer)="InvokeManualResize()">
1005
966
  ${relatedEntityBeforeFieldTabs ? relatedEntityBeforeFieldTabs.map(s => s.TabCode).join('\n') : ''}
1006
- ${additionalSections ? additionalSections.filter(s => s.Type !== core_1.GeneratedFormSectionType.Top).map(s => s.TabCode).join('\n ') : ''}
967
+ ${additionalSections ? additionalSections.filter(s => s.Type !== GeneratedFormSectionType.Top).map(s => s.TabCode).join('\n ') : ''}
1007
968
  ${relatedEntityAfterFieldTabs ? relatedEntityAfterFieldTabs.map(s => s.TabCode).join('\n') : ''}
1008
969
  </mj-tabstrip>`;
1009
970
  }
@@ -1015,7 +976,12 @@ ${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
1015
976
  * @returns Generated HTML without splitter layout
1016
977
  */
1017
978
  generateSingleEntityHTMLWithOUTSplitterForAngular(topArea, additionalSections, relatedEntitySections) {
1018
- const htmlCode = `<mj-record-form-container [record]="record" [formComponent]="this">
979
+ const htmlCode = `<mj-record-form-container [Record]="record" [FormComponent]="this"
980
+ (Navigate)="OnFormNavigate($event)"
981
+ (DeleteRequested)="OnDeleteRequested()"
982
+ (FavoriteToggled)="OnFavoriteToggled()"
983
+ (HistoryRequested)="OnHistoryRequested()"
984
+ (ListManagementRequested)="OnListManagementRequested()">
1019
985
  ${this.innerTopAreaHTML(topArea)}
1020
986
  ${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
1021
987
  </mj-record-form-container>
@@ -1023,5 +989,4 @@ ${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
1023
989
  return htmlCode;
1024
990
  }
1025
991
  }
1026
- exports.AngularClientGeneratorBase = AngularClientGeneratorBase;
1027
992
  //# sourceMappingURL=angular-codegen.js.map