@memberjunction/codegen-lib 2.115.0 → 2.117.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.
- package/dist/Angular/angular-codegen.d.ts +50 -3
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +364 -118
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/related-entity-components.d.ts +6 -0
- package/dist/Angular/related-entity-components.d.ts.map +1 -1
- package/dist/Angular/related-entity-components.js +7 -0
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Angular/user-view-grid-related-entity-component.d.ts +1 -1
- package/dist/Angular/user-view-grid-related-entity-component.d.ts.map +1 -1
- package/dist/Angular/user-view-grid-related-entity-component.js +14 -12
- package/dist/Angular/user-view-grid-related-entity-component.js.map +1 -1
- package/dist/Config/config.d.ts +0 -18
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +2 -2
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +48 -7
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +383 -146
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +134 -28
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +3 -2
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +5 -2
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.d.ts +80 -22
- package/dist/Misc/advanced_generation.d.ts.map +1 -1
- package/dist/Misc/advanced_generation.js +248 -142
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +6 -1
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +7 -0
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +10 -10
|
@@ -24,7 +24,7 @@ class AngularFormSectionInfo {
|
|
|
24
24
|
*/
|
|
25
25
|
Name;
|
|
26
26
|
/**
|
|
27
|
-
* The generated HTML code for the tab
|
|
27
|
+
* The generated HTML code for the section (panel or tab)
|
|
28
28
|
*/
|
|
29
29
|
TabCode;
|
|
30
30
|
/**
|
|
@@ -63,6 +63,14 @@ class AngularFormSectionInfo {
|
|
|
63
63
|
* The generation result for related entity components
|
|
64
64
|
*/
|
|
65
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;
|
|
66
74
|
}
|
|
67
75
|
exports.AngularFormSectionInfo = AngularFormSectionInfo;
|
|
68
76
|
/**
|
|
@@ -99,18 +107,12 @@ class AngularClientGeneratorBase {
|
|
|
99
107
|
const tsCode = this.generateSingleEntityTypeScriptForAngular(entity, additionalSections, relatedEntitySections);
|
|
100
108
|
fs_1.default.writeFileSync(path_1.default.join(thisEntityPath, `${entity.ClassName.toLowerCase()}.form.component.ts`), tsCode);
|
|
101
109
|
fs_1.default.writeFileSync(path_1.default.join(thisEntityPath, `${entity.ClassName.toLowerCase()}.form.component.html`), htmlCode);
|
|
110
|
+
// Sections are now inline in HTML templates, no separate files needed
|
|
111
|
+
// Just track them for module generation purposes
|
|
102
112
|
if (additionalSections.length > 0) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
for (let j = 0; j < additionalSections.length; ++j) {
|
|
107
|
-
const section = additionalSections[j];
|
|
108
|
-
// Only write file if FileName and ComponentCode are populated
|
|
109
|
-
if (section.FileName && section.ComponentCode) {
|
|
110
|
-
fs_1.default.writeFileSync(path_1.default.join(sectionPath, section.FileName), section.ComponentCode);
|
|
111
|
-
}
|
|
112
|
-
sections.push(section); // add the entity's secitons one by one to the master/global list of sections
|
|
113
|
-
}
|
|
113
|
+
additionalSections.forEach(section => {
|
|
114
|
+
sections.push(section);
|
|
115
|
+
});
|
|
114
116
|
}
|
|
115
117
|
const componentName = `${entity.ClassName}FormComponent`;
|
|
116
118
|
componentImports.push(`import { ${componentName}, Load${componentName} } from "./Entities/${entity.ClassName}/${entity.ClassName.toLowerCase()}.form.component";`);
|
|
@@ -198,7 +200,6 @@ import { DropDownListModule } from '@progress/kendo-angular-dropdowns';
|
|
|
198
200
|
|
|
199
201
|
// Import Generated Components
|
|
200
202
|
${componentImports.join('\n')}
|
|
201
|
-
${sections.filter(s => !s.IsRelatedEntity && s.ClassName && s.EntityClassName && s.FileNameWithoutExtension).map(s => `import { ${s.ClassName}, Load${s.ClassName} } from "./Entities/${s.EntityClassName}/sections/${s.FileNameWithoutExtension}"`).join('\n')}
|
|
202
203
|
${relatedEntityModuleImports.filter(remi => remi.library.trim().toLowerCase() !== '@memberjunction/ng-user-view-grid')
|
|
203
204
|
.map(remi => `import { ${remi.modules.map(m => m).join(', ')} } from "${remi.library}"`)
|
|
204
205
|
.join('\n')}
|
|
@@ -206,12 +207,11 @@ ${moduleCode}
|
|
|
206
207
|
|
|
207
208
|
export function Load${modulePrefix}GeneratedForms() {
|
|
208
209
|
// This function doesn't do much, but it calls each generated form's loader function
|
|
209
|
-
// which in turn calls the sections for that generated form. Ultimately, those bits of
|
|
210
|
+
// which in turn calls the sections for that generated form. Ultimately, those bits of
|
|
210
211
|
// code do NOTHING - the point is to prevent the code from being eliminated during tree shaking
|
|
211
212
|
// since it is dynamically instantiated on demand, and the Angular compiler has no way to know that,
|
|
212
213
|
// in production builds tree shaking will eliminate the code unless we do this
|
|
213
214
|
${componentNames.map(c => `Load${c.componentName}();`).join('\n ')}
|
|
214
|
-
${sections.filter(s => !s.IsRelatedEntity && s.ClassName).map(s => `Load${s.ClassName}();`).join('\n ')}
|
|
215
215
|
}
|
|
216
216
|
`;
|
|
217
217
|
}
|
|
@@ -224,11 +224,11 @@ export function Load${modulePrefix}GeneratedForms() {
|
|
|
224
224
|
* @returns Generated TypeScript code for all sub-modules and the master module
|
|
225
225
|
*/
|
|
226
226
|
generateAngularModuleCode(componentNames, sections, maxComponentsPerModule, modulePrefix) {
|
|
227
|
-
// this function breaks up the componentNames
|
|
228
|
-
//
|
|
229
|
-
//
|
|
227
|
+
// this function breaks up the componentNames into sub-modules, we only want to have a max of maxComponentsPerModule components per module
|
|
228
|
+
// Note: sections are now inline in the HTML templates, so we don't include them in the module declarations
|
|
229
|
+
// Just use the component names - sections are inline HTML now, not separate components
|
|
230
230
|
const simpleComponentNames = componentNames.map(c => c.componentName);
|
|
231
|
-
const combinedArray = simpleComponentNames
|
|
231
|
+
const combinedArray = simpleComponentNames;
|
|
232
232
|
const subModules = [];
|
|
233
233
|
let currentComponentCount = 0;
|
|
234
234
|
const subModuleStarter = `
|
|
@@ -325,7 +325,6 @@ export class ${this.SubModuleBaseName}${moduleNumber} { }
|
|
|
325
325
|
*/
|
|
326
326
|
generateSingleEntityTypeScriptForAngular(entity, additionalSections, relatedEntitySections) {
|
|
327
327
|
const entityObjectClass = entity.ClassName;
|
|
328
|
-
const sectionImports = additionalSections.length > 0 ? additionalSections.map(s => `import { Load${s.ClassName} } from "./sections/${s.FileNameWithoutExtension}"`).join('\n') : '';
|
|
329
328
|
// next, build a list of distinct imports at the library level and for components within the library
|
|
330
329
|
const libs = relatedEntitySections.length > 0 ? relatedEntitySections.filter(s => s.GeneratedOutput && s.GeneratedOutput.Component && s.GeneratedOutput.Component.ImportPath)
|
|
331
330
|
.map(s => {
|
|
@@ -350,29 +349,63 @@ export class ${this.SubModuleBaseName}${moduleNumber} { }
|
|
|
350
349
|
});
|
|
351
350
|
}
|
|
352
351
|
});
|
|
353
|
-
//
|
|
352
|
+
// now our libs array is good to go, we can generate the import statements for the libraries and the items within the libraries
|
|
354
353
|
const generationImports = distinctLibs.map(l => `import { ${l.items.join(", ")} } from "${l.lib}"`).join('\n');
|
|
355
354
|
const generationInjectedCode = relatedEntitySections.length > 0 ?
|
|
356
355
|
relatedEntitySections.filter(s => s.GeneratedOutput && s.GeneratedOutput.CodeOutput.length > 0)
|
|
357
356
|
.map(s => s.GeneratedOutput.CodeOutput.split("\n").map(l => ` ${l}`).join("\n")).join('\n') : '';
|
|
357
|
+
// 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);
|
|
359
|
+
const allSections = [...sectionsWithoutTop, ...relatedEntitySections];
|
|
360
|
+
// Assign unique keys to each section
|
|
361
|
+
const usedKeys = new Set();
|
|
362
|
+
allSections.forEach((s) => {
|
|
363
|
+
let sectionKey = this.camelCase(s.Name);
|
|
364
|
+
// Ensure unique keys by tracking used keys and adding suffix for duplicates
|
|
365
|
+
let suffix = 1;
|
|
366
|
+
while (usedKeys.has(sectionKey)) {
|
|
367
|
+
sectionKey = this.camelCase(s.Name) + suffix++;
|
|
368
|
+
}
|
|
369
|
+
usedKeys.add(sectionKey);
|
|
370
|
+
s.UniqueKey = sectionKey; // Store the unique key with the section
|
|
371
|
+
});
|
|
372
|
+
// Now update all TabCode with the correct unique keys
|
|
373
|
+
allSections.forEach(s => {
|
|
374
|
+
if (s.TabCode && s.UniqueKey) {
|
|
375
|
+
// Replace placeholder camelCase keys with actual unique keys in the HTML
|
|
376
|
+
const placeholderKey = this.camelCase(s.Name);
|
|
377
|
+
if (placeholderKey !== s.UniqueKey) {
|
|
378
|
+
// Only replace if they're different (i.e., there was a duplicate)
|
|
379
|
+
const keyRegex = new RegExp(placeholderKey, 'g');
|
|
380
|
+
s.TabCode = s.TabCode.replace(keyRegex, s.UniqueKey);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Generate plain objects for section initialization
|
|
385
|
+
const sectionInitEntries = allSections.map((s, index) => {
|
|
386
|
+
// First 2 sections expanded by default, metadata and related entities collapsed
|
|
387
|
+
const isExpanded = index < 2 && !s.Name.toLowerCase().includes('metadata') && !s.IsRelatedEntity;
|
|
388
|
+
return ` { sectionKey: '${s.UniqueKey}', sectionName: '${s.Name}', isExpanded: ${isExpanded} }`;
|
|
389
|
+
});
|
|
390
|
+
const sectionInitCode = sectionInitEntries.length > 0
|
|
391
|
+
? `\n\n override async ngOnInit() {\n await super.ngOnInit();\n this.initSections([\n${sectionInitEntries.join(',\n')}\n ]);\n }`
|
|
392
|
+
: '';
|
|
358
393
|
return `import { Component } from '@angular/core';
|
|
359
394
|
import { ${entityObjectClass}Entity } from '${entity.SchemaName === config_1.mjCoreSchema ? '@memberjunction/core-entities' : 'mj_generatedentities'}';
|
|
360
395
|
import { RegisterClass } from '@memberjunction/global';
|
|
361
396
|
import { BaseFormComponent } from '@memberjunction/ng-base-forms';
|
|
362
|
-
${
|
|
363
|
-
|
|
397
|
+
${generationImports.length > 0 ? generationImports + '\n' : ''}
|
|
364
398
|
@RegisterClass(BaseFormComponent, '${entity.Name}') // Tell MemberJunction about this class
|
|
365
399
|
@Component({
|
|
366
400
|
selector: 'gen-${entity.ClassName.toLowerCase()}-form',
|
|
367
|
-
templateUrl: './${entity.ClassName.toLowerCase()}.form.component.html'
|
|
368
|
-
styleUrls: ['../../../../shared/form-styles.css']
|
|
401
|
+
templateUrl: './${entity.ClassName.toLowerCase()}.form.component.html'
|
|
369
402
|
})
|
|
370
403
|
export class ${entity.ClassName}FormComponent extends BaseFormComponent {
|
|
371
|
-
public record!: ${entityObjectClass}Entity;${generationInjectedCode.length > 0 ? '\n' + generationInjectedCode : ''}
|
|
372
|
-
}
|
|
404
|
+
public record!: ${entityObjectClass}Entity;${generationInjectedCode.length > 0 ? '\n' + generationInjectedCode : ''}${sectionInitCode}
|
|
405
|
+
}
|
|
373
406
|
|
|
374
407
|
export function Load${entity.ClassName}FormComponent() {
|
|
375
|
-
|
|
408
|
+
// does nothing, but called to prevent tree-shaking from eliminating this component from the build
|
|
376
409
|
}
|
|
377
410
|
`;
|
|
378
411
|
}
|
|
@@ -401,44 +434,71 @@ export function Load${entity.ClassName}FormComponent() {
|
|
|
401
434
|
* @param sections Array of existing sections
|
|
402
435
|
* @param type The type of section to add
|
|
403
436
|
* @param name The name of the section
|
|
437
|
+
* @param fieldSequence Optional sequence number of the field (used to track minimum sequence for sorting)
|
|
404
438
|
*/
|
|
405
|
-
AddSectionIfNeeded(entity, sections, type, name) {
|
|
439
|
+
AddSectionIfNeeded(entity, sections, type, name, fieldSequence) {
|
|
406
440
|
const section = sections.find(s => s.Name === name && s.Type === type);
|
|
407
|
-
const fName = `${this.
|
|
408
|
-
if (!section)
|
|
441
|
+
const fName = `${this.sanitizeFilename(name)}.component`;
|
|
442
|
+
if (!section) {
|
|
409
443
|
sections.push({
|
|
410
444
|
Type: type,
|
|
411
445
|
Name: name,
|
|
412
446
|
FileName: `${fName}.ts`,
|
|
413
447
|
ComponentCode: '',
|
|
414
|
-
ClassName: `${entity.ClassName}${this.
|
|
448
|
+
ClassName: `${entity.ClassName}${this.pascalCase(name)}Component`,
|
|
415
449
|
TabCode: '',
|
|
416
450
|
Fields: [],
|
|
417
451
|
EntityClassName: entity.ClassName,
|
|
418
|
-
FileNameWithoutExtension: fName
|
|
452
|
+
FileNameWithoutExtension: fName,
|
|
453
|
+
MinSequence: fieldSequence
|
|
419
454
|
});
|
|
455
|
+
}
|
|
456
|
+
else if (fieldSequence != null && (section.MinSequence == null || fieldSequence < section.MinSequence)) {
|
|
457
|
+
// Update the minimum sequence if this field has a lower sequence
|
|
458
|
+
section.MinSequence = fieldSequence;
|
|
459
|
+
}
|
|
420
460
|
}
|
|
421
461
|
/**
|
|
422
462
|
* Generates additional form sections based on entity field metadata
|
|
423
463
|
* @param entity The entity to generate sections for
|
|
424
464
|
* @param startIndex Starting index for tab ordering
|
|
465
|
+
* @param categoryIcons Optional map of category names to Font Awesome icon classes
|
|
425
466
|
* @returns Array of generated form sections
|
|
426
467
|
*/
|
|
427
|
-
generateAngularAdditionalSections(entity, startIndex) {
|
|
468
|
+
generateAngularAdditionalSections(entity, startIndex, categoryIcons) {
|
|
428
469
|
const sections = [];
|
|
429
470
|
let index = startIndex;
|
|
430
471
|
const sortedFields = (0, util_1.sortBySequenceAndCreatedAt)(entity.Fields);
|
|
431
472
|
for (const field of sortedFields) {
|
|
432
473
|
if (field.IncludeInGeneratedForm) {
|
|
433
474
|
if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Category && field.Category && field.Category !== '' && field.IncludeInGeneratedForm)
|
|
434
|
-
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Category, field.Category);
|
|
475
|
+
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Category, field.Category, field.Sequence);
|
|
435
476
|
else if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Details)
|
|
436
|
-
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Details, "Details");
|
|
477
|
+
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Details, "Details", field.Sequence);
|
|
437
478
|
else if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Top)
|
|
438
|
-
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Top, "Top");
|
|
479
|
+
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Top, "Top", field.Sequence);
|
|
439
480
|
}
|
|
440
481
|
}
|
|
441
|
-
//
|
|
482
|
+
// Sort sections by minimum sequence (Top sections first, System sections last, then by MinSequence)
|
|
483
|
+
sections.sort((a, b) => {
|
|
484
|
+
// Top sections always first
|
|
485
|
+
if (a.Type === core_1.GeneratedFormSectionType.Top)
|
|
486
|
+
return -1;
|
|
487
|
+
if (b.Type === core_1.GeneratedFormSectionType.Top)
|
|
488
|
+
return 1;
|
|
489
|
+
// System sections always last (after related entities)
|
|
490
|
+
const aIsSystem = a.Name.toLowerCase() === 'system' || a.Name.toLowerCase() === 'system metadata';
|
|
491
|
+
const bIsSystem = b.Name.toLowerCase() === 'system' || b.Name.toLowerCase() === 'system metadata';
|
|
492
|
+
if (aIsSystem && !bIsSystem)
|
|
493
|
+
return 1;
|
|
494
|
+
if (!aIsSystem && bIsSystem)
|
|
495
|
+
return -1;
|
|
496
|
+
// Otherwise sort by sequence
|
|
497
|
+
const aSeq = a.MinSequence ?? Number.MAX_SAFE_INTEGER;
|
|
498
|
+
const bSeq = b.MinSequence ?? Number.MAX_SAFE_INTEGER;
|
|
499
|
+
return aSeq - bSeq;
|
|
500
|
+
});
|
|
501
|
+
// now we have a distinct list of section names set, generate HTML for each section
|
|
442
502
|
let sectionIndex = 0;
|
|
443
503
|
for (const section of sections) {
|
|
444
504
|
let sectionName = '';
|
|
@@ -451,44 +511,34 @@ export function Load${entity.ClassName}FormComponent() {
|
|
|
451
511
|
sectionName = this.stripWhiteSpace(section.Name.toLowerCase());
|
|
452
512
|
else if (section.Type === core_1.GeneratedFormSectionType.Details)
|
|
453
513
|
sectionName = 'details';
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
514
|
+
// Generate collapsible panel HTML inline instead of using separate components
|
|
515
|
+
const formHTML = this.generateSectionHTMLForAngular(entity, section);
|
|
516
|
+
// Use category-specific icon from LLM if available, otherwise fall back to keyword matching
|
|
517
|
+
const icon = (categoryIcons && categoryIcons[section.Name]) || this.getIconForCategory(section.Name);
|
|
518
|
+
// NOTE: We'll set the UniqueKey later in generateSingleEntityTypeScriptForAngular()
|
|
519
|
+
// For now, just use a placeholder that will be replaced
|
|
520
|
+
const sectionKey = this.camelCase(section.Name);
|
|
521
|
+
// Build field names string for search functionality (includes both CodeName and DisplayName)
|
|
522
|
+
const fieldSearchTerms = section.Fields ? section.Fields.map(f => {
|
|
523
|
+
const terms = [f.CodeName.toLowerCase()];
|
|
524
|
+
if (f.DisplayName && f.DisplayName.toLowerCase() !== f.CodeName.toLowerCase()) {
|
|
525
|
+
terms.push(f.DisplayName.toLowerCase());
|
|
526
|
+
}
|
|
527
|
+
return terms.join(' ');
|
|
528
|
+
}).join(' ') : '';
|
|
529
|
+
// No additional indentation needed - formHTML is already properly indented
|
|
530
|
+
const indentedFormHTML = formHTML;
|
|
531
|
+
section.TabCode = `${sectionIndex > 0 ? '\n' : ''} <!-- ${section.Name} Section -->
|
|
532
|
+
<mj-collapsible-panel slot="field-panels"
|
|
533
|
+
sectionKey="${sectionKey}"
|
|
534
|
+
sectionName="${section.Name}"
|
|
535
|
+
icon="${icon}"
|
|
536
|
+
[form]="this"
|
|
537
|
+
[formContext]="formContext">
|
|
538
|
+
${indentedFormHTML}
|
|
539
|
+
</mj-collapsible-panel>`;
|
|
540
|
+
sectionIndex++;
|
|
465
541
|
}
|
|
466
|
-
const formHTML = this.generateSectionHTMLForAngular(entity, section);
|
|
467
|
-
section.ComponentCode = `import { Component, Input } from '@angular/core';
|
|
468
|
-
import { RegisterClass } from '@memberjunction/global';
|
|
469
|
-
import { BaseFormSectionComponent } from '@memberjunction/ng-base-forms';
|
|
470
|
-
import { ${entity.ClassName}Entity } from '${entity.SchemaName === config_1.mjCoreSchema ? '@memberjunction/core-entities' : 'mj_generatedentities'}';
|
|
471
|
-
|
|
472
|
-
@RegisterClass(BaseFormSectionComponent, '${entity.Name}.${sectionName}') // Tell MemberJunction about this class
|
|
473
|
-
@Component({
|
|
474
|
-
selector: 'gen-${entity.ClassName.toLowerCase()}-form-${sectionName}',
|
|
475
|
-
styleUrls: ['../../../../../shared/form-styles.css'],
|
|
476
|
-
template: \`<div *ngIf="this.record">
|
|
477
|
-
<div class="record-form">
|
|
478
|
-
${formHTML}
|
|
479
|
-
</div>
|
|
480
|
-
</div>
|
|
481
|
-
\`
|
|
482
|
-
})
|
|
483
|
-
export class ${entity.ClassName}${this.stripWhiteSpace(section.Name)}Component extends BaseFormSectionComponent {
|
|
484
|
-
@Input() override record!: ${entity.ClassName}Entity;
|
|
485
|
-
@Input() override EditMode: boolean = false;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
export function Load${entity.ClassName}${this.stripWhiteSpace(section.Name)}Component() {
|
|
489
|
-
// does nothing, but called in order to prevent tree-shaking from eliminating this component from the build
|
|
490
|
-
}
|
|
491
|
-
`;
|
|
492
542
|
if (section.Type !== core_1.GeneratedFormSectionType.Top)
|
|
493
543
|
index++; // don't increment the tab index for TOP AREA, becuse it won't be rendered as a tab
|
|
494
544
|
}
|
|
@@ -583,7 +633,8 @@ export function Load${entity.ClassName}${this.stripWhiteSpace(section.Name)}Comp
|
|
|
583
633
|
[ShowLabel]="${section.Fields.length > 1 ? 'true' : 'false'}"
|
|
584
634
|
FieldName="${field.CodeName}"
|
|
585
635
|
Type="${editControl}"
|
|
586
|
-
[EditMode]="EditMode"
|
|
636
|
+
[EditMode]="EditMode"
|
|
637
|
+
[formContext]="formContext"${linkType ? `\n LinkType="${linkType}"` : ''}${linkComponentType ? linkComponentType : ''}
|
|
587
638
|
></mj-form-field>
|
|
588
639
|
`;
|
|
589
640
|
}
|
|
@@ -632,39 +683,65 @@ export function Load${entity.ClassName}${this.stripWhiteSpace(section.Name)}Comp
|
|
|
632
683
|
async generateRelatedEntityTabs(entity, startIndex, contextUser) {
|
|
633
684
|
const md = new core_1.Metadata();
|
|
634
685
|
const tabs = [];
|
|
635
|
-
|
|
686
|
+
// Sort related entities by Sequence (user's explicit ordering), then by RelatedEntity name (stable tiebreaker)
|
|
687
|
+
const sortedRelatedEntities = entity.RelatedEntities
|
|
688
|
+
.filter(re => re.DisplayInForm)
|
|
689
|
+
.sort((a, b) => {
|
|
690
|
+
if (a.Sequence !== b.Sequence) {
|
|
691
|
+
return a.Sequence - b.Sequence;
|
|
692
|
+
}
|
|
693
|
+
return a.RelatedEntity.localeCompare(b.RelatedEntity);
|
|
694
|
+
});
|
|
636
695
|
let index = startIndex;
|
|
637
696
|
for (const relatedEntity of sortedRelatedEntities) {
|
|
638
697
|
const tabName = this.generateRelatedEntityTabName(relatedEntity, sortedRelatedEntities);
|
|
639
698
|
let icon = '';
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
case 'Related Entity Icon':
|
|
646
|
-
const re = md.Entities.find(e => e.ID === relatedEntity.RelatedEntityID);
|
|
647
|
-
if (re && re.Icon && re.Icon.length > 0)
|
|
648
|
-
icon = `<span class="${re.Icon} tab-header-icon"></span>`;
|
|
649
|
-
break;
|
|
650
|
-
default:
|
|
651
|
-
// none
|
|
652
|
-
break;
|
|
699
|
+
let iconClass = '';
|
|
700
|
+
// First, check for custom icon
|
|
701
|
+
if (relatedEntity.DisplayIconType === 'Custom' && relatedEntity.DisplayIcon && relatedEntity.DisplayIcon.length > 0) {
|
|
702
|
+
icon = `<span class="${relatedEntity.DisplayIcon} tab-header-icon"></span>`;
|
|
703
|
+
iconClass = relatedEntity.DisplayIcon;
|
|
653
704
|
}
|
|
705
|
+
// If no custom icon, try to use the related entity's icon
|
|
706
|
+
else {
|
|
707
|
+
const re = md.Entities.find(e => e.ID === relatedEntity.RelatedEntityID);
|
|
708
|
+
if (re && re.Icon && re.Icon.length > 0) {
|
|
709
|
+
icon = `<span class="${re.Icon} tab-header-icon"></span>`;
|
|
710
|
+
iconClass = re.Icon;
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
// Fall back to default table icon
|
|
714
|
+
iconClass = 'fa fa-table';
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Calculate section key before generation (may be replaced later if duplicate)
|
|
718
|
+
const sectionKey = this.camelCase(tabName);
|
|
654
719
|
const component = await related_entity_components_1.RelatedEntityDisplayComponentGeneratorBase.GetComponent(relatedEntity, contextUser);
|
|
655
720
|
const generateResults = await component.Generate({
|
|
656
721
|
Entity: entity,
|
|
657
722
|
RelationshipInfo: relatedEntity,
|
|
658
|
-
TabName: tabName
|
|
723
|
+
TabName: tabName,
|
|
724
|
+
SectionKey: sectionKey // Pass section key for IsSectionExpanded() calls
|
|
659
725
|
});
|
|
660
|
-
//
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
${
|
|
667
|
-
|
|
726
|
+
// Add proper indentation for collapsible panel body (12 spaces for div content)
|
|
727
|
+
const componentCodeWithIndent = generateResults.TemplateOutput.split('\n').map(l => ` ${l}`).join('\n');
|
|
728
|
+
// For related entities, use the related entity name as searchable term
|
|
729
|
+
const relatedEntitySearchTerms = relatedEntity.RelatedEntity.toLowerCase();
|
|
730
|
+
// Determine slot based on DisplayLocation
|
|
731
|
+
const slot = relatedEntity.DisplayLocation === 'Before Field Tabs' ? 'before-panels' : 'after-panels';
|
|
732
|
+
const tabCode = `${index > 0 ? '\n' : ''} <!-- ${tabName} Section -->
|
|
733
|
+
<mj-collapsible-panel slot="${slot}"
|
|
734
|
+
sectionKey="${sectionKey}"
|
|
735
|
+
sectionName="${tabName}"
|
|
736
|
+
icon="${iconClass}"
|
|
737
|
+
variant="related-entity"
|
|
738
|
+
[form]="this"
|
|
739
|
+
[formContext]="formContext"
|
|
740
|
+
[badgeCount]="GetSectionRowCount('${sectionKey}')">
|
|
741
|
+
<div *ngIf="record.IsSaved">
|
|
742
|
+
${componentCodeWithIndent}
|
|
743
|
+
</div>
|
|
744
|
+
</mj-collapsible-panel>`;
|
|
668
745
|
tabs.push({
|
|
669
746
|
Type: core_1.GeneratedFormSectionType.Category,
|
|
670
747
|
IsRelatedEntity: true,
|
|
@@ -686,6 +763,129 @@ ${componentCodeWithTabs}
|
|
|
686
763
|
stripWhiteSpace(s) {
|
|
687
764
|
return s.replace(/\s/g, '');
|
|
688
765
|
}
|
|
766
|
+
/**
|
|
767
|
+
* Converts a string to camelCase and sanitizes it for use as a JavaScript identifier
|
|
768
|
+
* @param str The string to convert
|
|
769
|
+
* @returns String in camelCase format, safe for use as object key or variable name
|
|
770
|
+
*/
|
|
771
|
+
camelCase(str) {
|
|
772
|
+
// First, replace non-alphanumeric characters (except spaces) with spaces
|
|
773
|
+
let sanitized = str.replace(/[^a-zA-Z0-9\s]/g, ' ');
|
|
774
|
+
// Convert to camelCase
|
|
775
|
+
let result = sanitized
|
|
776
|
+
.replace(/\s(.)/g, (match, char) => char.toUpperCase())
|
|
777
|
+
.replace(/\s/g, '')
|
|
778
|
+
.replace(/^(.)/, (match, char) => char.toLowerCase());
|
|
779
|
+
// If starts with a digit, prefix with underscore
|
|
780
|
+
if (/^\d/.test(result)) {
|
|
781
|
+
result = '_' + result;
|
|
782
|
+
}
|
|
783
|
+
// If result is empty (all special chars), use a default
|
|
784
|
+
if (result.length === 0) {
|
|
785
|
+
result = 'section';
|
|
786
|
+
}
|
|
787
|
+
return result;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Converts a string to PascalCase and sanitizes it for use as a class name
|
|
791
|
+
* @param str The string to convert
|
|
792
|
+
* @returns String in PascalCase format, safe for use as a class name
|
|
793
|
+
*/
|
|
794
|
+
pascalCase(str) {
|
|
795
|
+
// First, replace non-alphanumeric characters (except spaces) with spaces
|
|
796
|
+
let sanitized = str.replace(/[^a-zA-Z0-9\s]/g, ' ');
|
|
797
|
+
// Convert to PascalCase (capitalize first letter of each word)
|
|
798
|
+
let result = sanitized
|
|
799
|
+
.replace(/\s(.)/g, (match, char) => char.toUpperCase())
|
|
800
|
+
.replace(/\s/g, '')
|
|
801
|
+
.replace(/^(.)/, (match, char) => char.toUpperCase());
|
|
802
|
+
// If starts with a digit, prefix with underscore
|
|
803
|
+
if (/^\d/.test(result)) {
|
|
804
|
+
result = '_' + result;
|
|
805
|
+
}
|
|
806
|
+
// If result is empty (all special chars), use a default
|
|
807
|
+
if (result.length === 0) {
|
|
808
|
+
result = 'Section';
|
|
809
|
+
}
|
|
810
|
+
return result;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Sanitizes a string to create a valid filename in lowercase format.
|
|
814
|
+
* Removes all non-alphanumeric characters (except spaces) and converts to lowercase.
|
|
815
|
+
* Used for creating component filenames that are safe across all file systems.
|
|
816
|
+
*
|
|
817
|
+
* Example: "Timeline & Budget" → "timelinebudget"
|
|
818
|
+
*
|
|
819
|
+
* @param str The string to sanitize
|
|
820
|
+
* @returns A sanitized lowercase filename string
|
|
821
|
+
*/
|
|
822
|
+
sanitizeFilename(str) {
|
|
823
|
+
// Remove all non-alphanumeric characters (except spaces)
|
|
824
|
+
let sanitized = str.replace(/[^a-zA-Z0-9\s]/g, '');
|
|
825
|
+
// Convert to lowercase and remove all spaces
|
|
826
|
+
let result = sanitized.toLowerCase().replace(/\s/g, '');
|
|
827
|
+
// If result is empty (all special chars), use a default
|
|
828
|
+
if (result.length === 0) {
|
|
829
|
+
result = 'section';
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Maps category names to appropriate Font Awesome icon classes
|
|
835
|
+
* @param category The category name to map
|
|
836
|
+
* @returns Font Awesome icon class string
|
|
837
|
+
*/
|
|
838
|
+
getIconForCategory(category) {
|
|
839
|
+
const lowerCategory = category.toLowerCase();
|
|
840
|
+
// Address/Location categories
|
|
841
|
+
if (lowerCategory.includes('address') || lowerCategory.includes('location')) {
|
|
842
|
+
return 'fa fa-map-marker-alt';
|
|
843
|
+
}
|
|
844
|
+
// Contact Information
|
|
845
|
+
if (lowerCategory.includes('contact')) {
|
|
846
|
+
return 'fa fa-address-card';
|
|
847
|
+
}
|
|
848
|
+
// Financial/Pricing categories
|
|
849
|
+
if (lowerCategory.includes('pric') || lowerCategory.includes('cost') ||
|
|
850
|
+
lowerCategory.includes('payment') || lowerCategory.includes('financial') ||
|
|
851
|
+
lowerCategory.includes('billing')) {
|
|
852
|
+
return 'fa fa-dollar-sign';
|
|
853
|
+
}
|
|
854
|
+
// Date/Time categories
|
|
855
|
+
if (lowerCategory.includes('date') || lowerCategory.includes('time') ||
|
|
856
|
+
lowerCategory.includes('schedule')) {
|
|
857
|
+
return 'fa fa-calendar';
|
|
858
|
+
}
|
|
859
|
+
// Status/State categories
|
|
860
|
+
if (lowerCategory.includes('status') || lowerCategory.includes('state')) {
|
|
861
|
+
return 'fa fa-flag';
|
|
862
|
+
}
|
|
863
|
+
// Metadata/Technical categories
|
|
864
|
+
if (lowerCategory.includes('metadata') || lowerCategory.includes('technical') ||
|
|
865
|
+
lowerCategory.includes('system')) {
|
|
866
|
+
return 'fa fa-cog';
|
|
867
|
+
}
|
|
868
|
+
// Description/Details categories
|
|
869
|
+
if (lowerCategory.includes('description') || lowerCategory.includes('detail')) {
|
|
870
|
+
return 'fa fa-align-left';
|
|
871
|
+
}
|
|
872
|
+
// Settings/Configuration categories
|
|
873
|
+
if (lowerCategory.includes('setting') || lowerCategory.includes('config') ||
|
|
874
|
+
lowerCategory.includes('preference')) {
|
|
875
|
+
return 'fa fa-sliders-h';
|
|
876
|
+
}
|
|
877
|
+
// Shipping/Delivery categories
|
|
878
|
+
if (lowerCategory.includes('ship') || lowerCategory.includes('delivery')) {
|
|
879
|
+
return 'fa fa-truck';
|
|
880
|
+
}
|
|
881
|
+
// User/Person categories
|
|
882
|
+
if (lowerCategory.includes('user') || lowerCategory.includes('person') ||
|
|
883
|
+
lowerCategory.includes('customer') || lowerCategory.includes('employee')) {
|
|
884
|
+
return 'fa fa-user';
|
|
885
|
+
}
|
|
886
|
+
// Default icon for uncategorized sections
|
|
887
|
+
return 'fa fa-info-circle';
|
|
888
|
+
}
|
|
689
889
|
/**
|
|
690
890
|
* Generates the complete HTML template for a single entity form
|
|
691
891
|
* @param entity The entity to generate HTML for
|
|
@@ -693,8 +893,22 @@ ${componentCodeWithTabs}
|
|
|
693
893
|
* @returns Promise resolving to an object containing the HTML code and section information
|
|
694
894
|
*/
|
|
695
895
|
async generateSingleEntityHTMLForAngular(entity, contextUser) {
|
|
896
|
+
// Load category icons from EntitySetting if available
|
|
897
|
+
let categoryIcons;
|
|
898
|
+
const entitySettings = entity.Settings;
|
|
899
|
+
if (entitySettings) {
|
|
900
|
+
const iconSetting = entitySettings.find((s) => s.Name === 'FieldCategoryIcons');
|
|
901
|
+
if (iconSetting && iconSetting.Value) {
|
|
902
|
+
try {
|
|
903
|
+
categoryIcons = JSON.parse(iconSetting.Value);
|
|
904
|
+
}
|
|
905
|
+
catch (e) {
|
|
906
|
+
// Invalid JSON, ignore and fall back to keyword matching
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
696
910
|
const topArea = this.generateTopAreaHTMLForAngular(entity);
|
|
697
|
-
const additionalSections = this.generateAngularAdditionalSections(entity, 0);
|
|
911
|
+
const additionalSections = this.generateAngularAdditionalSections(entity, 0, categoryIcons);
|
|
698
912
|
// 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...
|
|
699
913
|
const endingIndex = additionalSections && additionalSections.length ? (topArea && topArea.length > 0 ? additionalSections.length - 1 : additionalSections.length) : 0;
|
|
700
914
|
const relatedEntitySections = await this.generateRelatedEntityTabs(entity, endingIndex, contextUser);
|
|
@@ -710,19 +924,16 @@ ${componentCodeWithTabs}
|
|
|
710
924
|
* @returns Generated HTML with splitter layout
|
|
711
925
|
*/
|
|
712
926
|
generateSingleEntityHTMLWithSplitterForAngular(topArea, additionalSections, relatedEntitySections) {
|
|
713
|
-
const htmlCode = `<
|
|
714
|
-
<
|
|
715
|
-
<
|
|
716
|
-
<kendo-splitter orientation="vertical" (layoutChange)="splitterLayoutChange()" >
|
|
717
|
-
<kendo-splitter-pane [collapsible]="true" [size]="TopAreaHeight">
|
|
927
|
+
const htmlCode = `<mj-record-form-container [record]="record" [formComponent]="this">
|
|
928
|
+
<kendo-splitter orientation="vertical" (layoutChange)="splitterLayoutChange()">
|
|
929
|
+
<kendo-splitter-pane [collapsible]="true" [size]="TopAreaHeight">
|
|
718
930
|
${this.innerTopAreaHTML(topArea)}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
${this.
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
</div>
|
|
931
|
+
</kendo-splitter-pane>
|
|
932
|
+
<kendo-splitter-pane>
|
|
933
|
+
${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
|
|
934
|
+
</kendo-splitter-pane>
|
|
935
|
+
</kendo-splitter>
|
|
936
|
+
</mj-record-form-container>
|
|
726
937
|
`;
|
|
727
938
|
return htmlCode;
|
|
728
939
|
}
|
|
@@ -740,6 +951,44 @@ ${this.innerTabStripHTML(additionalSections, relatedEntitySections)}
|
|
|
740
951
|
</div>`;
|
|
741
952
|
}
|
|
742
953
|
/**
|
|
954
|
+
* Generates the HTML for collapsible panels containing all form sections
|
|
955
|
+
* @param additionalSections Array of field-based form sections
|
|
956
|
+
* @param relatedEntitySections Array of related entity sections
|
|
957
|
+
* @returns HTML string for all collapsible panels
|
|
958
|
+
*/
|
|
959
|
+
innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections) {
|
|
960
|
+
// Filter out Top sections as they're handled separately
|
|
961
|
+
const sectionsToRender = additionalSections.filter(s => s.Type !== core_1.GeneratedFormSectionType.Top);
|
|
962
|
+
// Order: before-panels, field-panels, after-panels
|
|
963
|
+
// The RecordFormContainer handles the related-entity-grid wrapper via named slots
|
|
964
|
+
const beforePanels = relatedEntitySections.filter(s => s.RelatedEntityDisplayLocation === 'Before Field Tabs');
|
|
965
|
+
const afterPanels = relatedEntitySections.filter(s => s.RelatedEntityDisplayLocation === 'After Field Tabs');
|
|
966
|
+
const parts = [];
|
|
967
|
+
// Add before panels if any
|
|
968
|
+
if (beforePanels.length > 0) {
|
|
969
|
+
parts.push(' <!-- ========================================');
|
|
970
|
+
parts.push(' RELATED ENTITY PANELS - BEFORE');
|
|
971
|
+
parts.push(' ======================================== -->');
|
|
972
|
+
parts.push(beforePanels.map(s => s.TabCode).join('\n'));
|
|
973
|
+
}
|
|
974
|
+
// Add field panels with header comment
|
|
975
|
+
if (sectionsToRender.length > 0) {
|
|
976
|
+
parts.push(' <!-- ========================================');
|
|
977
|
+
parts.push(' FIELD PANELS');
|
|
978
|
+
parts.push(' ======================================== -->');
|
|
979
|
+
parts.push(sectionsToRender.map(s => s.TabCode).join('\n'));
|
|
980
|
+
}
|
|
981
|
+
// Add after panels if any
|
|
982
|
+
if (afterPanels.length > 0) {
|
|
983
|
+
parts.push(' <!-- ========================================');
|
|
984
|
+
parts.push(' RELATED ENTITY PANELS - AFTER');
|
|
985
|
+
parts.push(' ======================================== -->');
|
|
986
|
+
parts.push(afterPanels.map(s => s.TabCode).join('\n'));
|
|
987
|
+
}
|
|
988
|
+
return parts.join('\n');
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* @deprecated Use innerCollapsiblePanelsHTML instead
|
|
743
992
|
* Generates the HTML for the tab strip containing all form sections
|
|
744
993
|
* @param additionalSections Array of field-based form sections
|
|
745
994
|
* @param relatedEntitySections Array of related entity sections
|
|
@@ -764,13 +1013,10 @@ ${this.innerTabStripHTML(additionalSections, relatedEntitySections)}
|
|
|
764
1013
|
* @returns Generated HTML without splitter layout
|
|
765
1014
|
*/
|
|
766
1015
|
generateSingleEntityHTMLWithOUTSplitterForAngular(topArea, additionalSections, relatedEntitySections) {
|
|
767
|
-
const htmlCode = `<
|
|
768
|
-
<form *ngIf="record" class="record-form" #form="ngForm" >
|
|
769
|
-
<mj-form-toolbar [form]="this"></mj-form-toolbar>
|
|
1016
|
+
const htmlCode = `<mj-record-form-container [record]="record" [formComponent]="this">
|
|
770
1017
|
${this.innerTopAreaHTML(topArea)}
|
|
771
|
-
${this.
|
|
772
|
-
|
|
773
|
-
</div>
|
|
1018
|
+
${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
|
|
1019
|
+
</mj-record-form-container>
|
|
774
1020
|
`;
|
|
775
1021
|
return htmlCode;
|
|
776
1022
|
}
|