@memberjunction/codegen-lib 2.115.0 → 2.116.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 +448 -93
- 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.map +1 -1
- package/dist/Angular/user-view-grid-related-entity-component.js +12 -4
- 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 +358 -142
- 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 +77 -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
|
/**
|
|
@@ -198,7 +206,6 @@ import { DropDownListModule } from '@progress/kendo-angular-dropdowns';
|
|
|
198
206
|
|
|
199
207
|
// Import Generated Components
|
|
200
208
|
${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
209
|
${relatedEntityModuleImports.filter(remi => remi.library.trim().toLowerCase() !== '@memberjunction/ng-user-view-grid')
|
|
203
210
|
.map(remi => `import { ${remi.modules.map(m => m).join(', ')} } from "${remi.library}"`)
|
|
204
211
|
.join('\n')}
|
|
@@ -206,12 +213,11 @@ ${moduleCode}
|
|
|
206
213
|
|
|
207
214
|
export function Load${modulePrefix}GeneratedForms() {
|
|
208
215
|
// 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
|
|
216
|
+
// which in turn calls the sections for that generated form. Ultimately, those bits of
|
|
210
217
|
// code do NOTHING - the point is to prevent the code from being eliminated during tree shaking
|
|
211
218
|
// since it is dynamically instantiated on demand, and the Angular compiler has no way to know that,
|
|
212
219
|
// in production builds tree shaking will eliminate the code unless we do this
|
|
213
220
|
${componentNames.map(c => `Load${c.componentName}();`).join('\n ')}
|
|
214
|
-
${sections.filter(s => !s.IsRelatedEntity && s.ClassName).map(s => `Load${s.ClassName}();`).join('\n ')}
|
|
215
221
|
}
|
|
216
222
|
`;
|
|
217
223
|
}
|
|
@@ -224,11 +230,11 @@ export function Load${modulePrefix}GeneratedForms() {
|
|
|
224
230
|
* @returns Generated TypeScript code for all sub-modules and the master module
|
|
225
231
|
*/
|
|
226
232
|
generateAngularModuleCode(componentNames, sections, maxComponentsPerModule, modulePrefix) {
|
|
227
|
-
// this function breaks up the componentNames
|
|
228
|
-
//
|
|
229
|
-
//
|
|
233
|
+
// this function breaks up the componentNames into sub-modules, we only want to have a max of maxComponentsPerModule components per module
|
|
234
|
+
// Note: sections are now inline in the HTML templates, so we don't include them in the module declarations
|
|
235
|
+
// Just use the component names - sections are inline HTML now, not separate components
|
|
230
236
|
const simpleComponentNames = componentNames.map(c => c.componentName);
|
|
231
|
-
const combinedArray = simpleComponentNames
|
|
237
|
+
const combinedArray = simpleComponentNames;
|
|
232
238
|
const subModules = [];
|
|
233
239
|
let currentComponentCount = 0;
|
|
234
240
|
const subModuleStarter = `
|
|
@@ -325,7 +331,6 @@ export class ${this.SubModuleBaseName}${moduleNumber} { }
|
|
|
325
331
|
*/
|
|
326
332
|
generateSingleEntityTypeScriptForAngular(entity, additionalSections, relatedEntitySections) {
|
|
327
333
|
const entityObjectClass = entity.ClassName;
|
|
328
|
-
const sectionImports = additionalSections.length > 0 ? additionalSections.map(s => `import { Load${s.ClassName} } from "./sections/${s.FileNameWithoutExtension}"`).join('\n') : '';
|
|
329
334
|
// next, build a list of distinct imports at the library level and for components within the library
|
|
330
335
|
const libs = relatedEntitySections.length > 0 ? relatedEntitySections.filter(s => s.GeneratedOutput && s.GeneratedOutput.Component && s.GeneratedOutput.Component.ImportPath)
|
|
331
336
|
.map(s => {
|
|
@@ -350,17 +355,117 @@ export class ${this.SubModuleBaseName}${moduleNumber} { }
|
|
|
350
355
|
});
|
|
351
356
|
}
|
|
352
357
|
});
|
|
353
|
-
//
|
|
358
|
+
// now our libs array is good to go, we can generate the import statements for the libraries and the items within the libraries
|
|
354
359
|
const generationImports = distinctLibs.map(l => `import { ${l.items.join(", ")} } from "${l.lib}"`).join('\n');
|
|
355
360
|
const generationInjectedCode = relatedEntitySections.length > 0 ?
|
|
356
361
|
relatedEntitySections.filter(s => s.GeneratedOutput && s.GeneratedOutput.CodeOutput.length > 0)
|
|
357
362
|
.map(s => s.GeneratedOutput.CodeOutput.split("\n").map(l => ` ${l}`).join("\n")).join('\n') : '';
|
|
363
|
+
// Generate unique keys for all sections FIRST, then use them everywhere
|
|
364
|
+
const sectionsWithoutTop = additionalSections.filter(s => s.Type !== core_1.GeneratedFormSectionType.Top && s.Name);
|
|
365
|
+
const allSections = [...sectionsWithoutTop, ...relatedEntitySections];
|
|
366
|
+
// Assign unique keys to each section
|
|
367
|
+
const usedKeys = new Set();
|
|
368
|
+
allSections.forEach((s) => {
|
|
369
|
+
let sectionKey = this.camelCase(s.Name);
|
|
370
|
+
// Ensure unique keys by tracking used keys and adding suffix for duplicates
|
|
371
|
+
let suffix = 1;
|
|
372
|
+
while (usedKeys.has(sectionKey)) {
|
|
373
|
+
sectionKey = this.camelCase(s.Name) + suffix++;
|
|
374
|
+
}
|
|
375
|
+
usedKeys.add(sectionKey);
|
|
376
|
+
s.UniqueKey = sectionKey; // Store the unique key with the section
|
|
377
|
+
});
|
|
378
|
+
// Now update all TabCode with the correct unique keys
|
|
379
|
+
allSections.forEach(s => {
|
|
380
|
+
if (s.TabCode && s.UniqueKey) {
|
|
381
|
+
// Replace placeholder camelCase keys with actual unique keys in the HTML
|
|
382
|
+
const placeholderKey = this.camelCase(s.Name);
|
|
383
|
+
if (placeholderKey !== s.UniqueKey) {
|
|
384
|
+
// Only replace if they're different (i.e., there was a duplicate)
|
|
385
|
+
const keyRegex = new RegExp(placeholderKey, 'g');
|
|
386
|
+
s.TabCode = s.TabCode.replace(keyRegex, s.UniqueKey);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
// Now generate the sectionsExpanded object using the stored unique keys
|
|
391
|
+
const sectionsExpandedEntries = allSections.map((s, index) => {
|
|
392
|
+
// First 2 sections expanded by default, metadata and related entities collapsed
|
|
393
|
+
const isExpanded = index < 2 && !s.Name.toLowerCase().includes('metadata') && !s.IsRelatedEntity;
|
|
394
|
+
return ` ${s.UniqueKey}: ${isExpanded}`;
|
|
395
|
+
});
|
|
396
|
+
const sectionsExpandedObject = sectionsExpandedEntries.length > 0
|
|
397
|
+
? `\n\n // Collapsible section state\n public sectionsExpanded = {\n${sectionsExpandedEntries.join(',\n')}\n };`
|
|
398
|
+
: '';
|
|
399
|
+
const toggleSectionMethod = sectionsExpandedEntries.length > 0
|
|
400
|
+
? `\n\n public toggleSection(section: keyof typeof this.sectionsExpanded): void {\n this.sectionsExpanded[section] = !this.sectionsExpanded[section];\n }`
|
|
401
|
+
: '';
|
|
402
|
+
// Add expand all/collapse all/filter methods if there are 4+ sections
|
|
403
|
+
const totalSections = allSections.length;
|
|
404
|
+
// Add sectionRowCounts property for related entities
|
|
405
|
+
const hasRelatedEntities = relatedEntitySections.length > 0;
|
|
406
|
+
const sectionRowCountsProperty = hasRelatedEntities
|
|
407
|
+
? `\n\n // Row counts for related entity sections (populated after grids load)\n public sectionRowCounts: { [key: string]: number } = {};`
|
|
408
|
+
: '';
|
|
409
|
+
const sectionUtilityMethods = totalSections >= 4 ? `\n
|
|
410
|
+
public expandAllSections(): void {
|
|
411
|
+
Object.keys(this.sectionsExpanded).forEach(key => {
|
|
412
|
+
this.sectionsExpanded[key as keyof typeof this.sectionsExpanded] = true;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
public collapseAllSections(): void {
|
|
417
|
+
Object.keys(this.sectionsExpanded).forEach(key => {
|
|
418
|
+
this.sectionsExpanded[key as keyof typeof this.sectionsExpanded] = false;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
public getExpandedCount(): number {
|
|
423
|
+
return Object.values(this.sectionsExpanded).filter(v => v === true).length;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
public filterSections(event: Event): void {
|
|
427
|
+
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase();
|
|
428
|
+
const panels = document.querySelectorAll('.form-card.collapsible-card');
|
|
429
|
+
|
|
430
|
+
panels.forEach((panel: Element) => {
|
|
431
|
+
const sectionName = panel.getAttribute('data-section-name') || '';
|
|
432
|
+
const fieldNames = panel.getAttribute('data-field-names') || '';
|
|
433
|
+
|
|
434
|
+
// Show section if search term matches section name OR any field name
|
|
435
|
+
const matches = sectionName.includes(searchTerm) || fieldNames.includes(searchTerm);
|
|
436
|
+
|
|
437
|
+
if (matches) {
|
|
438
|
+
panel.classList.remove('search-hidden');
|
|
439
|
+
|
|
440
|
+
// Add highlighting to matched text in section name
|
|
441
|
+
if (searchTerm && sectionName.includes(searchTerm)) {
|
|
442
|
+
const h3 = panel.querySelector('.collapsible-title h3 .section-name');
|
|
443
|
+
if (h3) {
|
|
444
|
+
const originalText = h3.textContent || '';
|
|
445
|
+
const regex = new RegExp(\`(\${searchTerm})\`, 'gi');
|
|
446
|
+
h3.innerHTML = originalText.replace(regex, '<span class="search-highlight">$1</span>');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
panel.classList.add('search-hidden');
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Clear highlighting when search is empty
|
|
455
|
+
if (!searchTerm) {
|
|
456
|
+
panels.forEach((panel: Element) => {
|
|
457
|
+
const h3 = panel.querySelector('.collapsible-title h3 .section-name');
|
|
458
|
+
if (h3) {
|
|
459
|
+
h3.innerHTML = h3.textContent || '';
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}` : '';
|
|
358
464
|
return `import { Component } from '@angular/core';
|
|
359
465
|
import { ${entityObjectClass}Entity } from '${entity.SchemaName === config_1.mjCoreSchema ? '@memberjunction/core-entities' : 'mj_generatedentities'}';
|
|
360
466
|
import { RegisterClass } from '@memberjunction/global';
|
|
361
467
|
import { BaseFormComponent } from '@memberjunction/ng-base-forms';
|
|
362
|
-
${
|
|
363
|
-
|
|
468
|
+
${generationImports.length > 0 ? generationImports + '\n' : ''}
|
|
364
469
|
@RegisterClass(BaseFormComponent, '${entity.Name}') // Tell MemberJunction about this class
|
|
365
470
|
@Component({
|
|
366
471
|
selector: 'gen-${entity.ClassName.toLowerCase()}-form',
|
|
@@ -368,11 +473,11 @@ ${sectionImports}${generationImports.length > 0 ? '\n' + generationImports : ''}
|
|
|
368
473
|
styleUrls: ['../../../../shared/form-styles.css']
|
|
369
474
|
})
|
|
370
475
|
export class ${entity.ClassName}FormComponent extends BaseFormComponent {
|
|
371
|
-
public record!: ${entityObjectClass}Entity;${generationInjectedCode.length > 0 ? '\n' + generationInjectedCode : ''}
|
|
372
|
-
}
|
|
476
|
+
public record!: ${entityObjectClass}Entity;${generationInjectedCode.length > 0 ? '\n' + generationInjectedCode : ''}${sectionsExpandedObject}${sectionRowCountsProperty}${toggleSectionMethod}${sectionUtilityMethods}
|
|
477
|
+
}
|
|
373
478
|
|
|
374
479
|
export function Load${entity.ClassName}FormComponent() {
|
|
375
|
-
|
|
480
|
+
// does nothing, but called to prevent tree-shaking from eliminating this component from the build
|
|
376
481
|
}
|
|
377
482
|
`;
|
|
378
483
|
}
|
|
@@ -401,44 +506,71 @@ export function Load${entity.ClassName}FormComponent() {
|
|
|
401
506
|
* @param sections Array of existing sections
|
|
402
507
|
* @param type The type of section to add
|
|
403
508
|
* @param name The name of the section
|
|
509
|
+
* @param fieldSequence Optional sequence number of the field (used to track minimum sequence for sorting)
|
|
404
510
|
*/
|
|
405
|
-
AddSectionIfNeeded(entity, sections, type, name) {
|
|
511
|
+
AddSectionIfNeeded(entity, sections, type, name, fieldSequence) {
|
|
406
512
|
const section = sections.find(s => s.Name === name && s.Type === type);
|
|
407
|
-
const fName = `${this.
|
|
408
|
-
if (!section)
|
|
513
|
+
const fName = `${this.sanitizeFilename(name)}.component`;
|
|
514
|
+
if (!section) {
|
|
409
515
|
sections.push({
|
|
410
516
|
Type: type,
|
|
411
517
|
Name: name,
|
|
412
518
|
FileName: `${fName}.ts`,
|
|
413
519
|
ComponentCode: '',
|
|
414
|
-
ClassName: `${entity.ClassName}${this.
|
|
520
|
+
ClassName: `${entity.ClassName}${this.pascalCase(name)}Component`,
|
|
415
521
|
TabCode: '',
|
|
416
522
|
Fields: [],
|
|
417
523
|
EntityClassName: entity.ClassName,
|
|
418
|
-
FileNameWithoutExtension: fName
|
|
524
|
+
FileNameWithoutExtension: fName,
|
|
525
|
+
MinSequence: fieldSequence
|
|
419
526
|
});
|
|
527
|
+
}
|
|
528
|
+
else if (fieldSequence != null && (section.MinSequence == null || fieldSequence < section.MinSequence)) {
|
|
529
|
+
// Update the minimum sequence if this field has a lower sequence
|
|
530
|
+
section.MinSequence = fieldSequence;
|
|
531
|
+
}
|
|
420
532
|
}
|
|
421
533
|
/**
|
|
422
534
|
* Generates additional form sections based on entity field metadata
|
|
423
535
|
* @param entity The entity to generate sections for
|
|
424
536
|
* @param startIndex Starting index for tab ordering
|
|
537
|
+
* @param categoryIcons Optional map of category names to Font Awesome icon classes
|
|
425
538
|
* @returns Array of generated form sections
|
|
426
539
|
*/
|
|
427
|
-
generateAngularAdditionalSections(entity, startIndex) {
|
|
540
|
+
generateAngularAdditionalSections(entity, startIndex, categoryIcons) {
|
|
428
541
|
const sections = [];
|
|
429
542
|
let index = startIndex;
|
|
430
543
|
const sortedFields = (0, util_1.sortBySequenceAndCreatedAt)(entity.Fields);
|
|
431
544
|
for (const field of sortedFields) {
|
|
432
545
|
if (field.IncludeInGeneratedForm) {
|
|
433
546
|
if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Category && field.Category && field.Category !== '' && field.IncludeInGeneratedForm)
|
|
434
|
-
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Category, field.Category);
|
|
547
|
+
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Category, field.Category, field.Sequence);
|
|
435
548
|
else if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Details)
|
|
436
|
-
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Details, "Details");
|
|
549
|
+
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Details, "Details", field.Sequence);
|
|
437
550
|
else if (field.GeneratedFormSectionType === core_1.GeneratedFormSectionType.Top)
|
|
438
|
-
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Top, "Top");
|
|
551
|
+
this.AddSectionIfNeeded(entity, sections, core_1.GeneratedFormSectionType.Top, "Top", field.Sequence);
|
|
439
552
|
}
|
|
440
553
|
}
|
|
441
|
-
//
|
|
554
|
+
// Sort sections by minimum sequence (Top sections first, System sections last, then by MinSequence)
|
|
555
|
+
sections.sort((a, b) => {
|
|
556
|
+
// Top sections always first
|
|
557
|
+
if (a.Type === core_1.GeneratedFormSectionType.Top)
|
|
558
|
+
return -1;
|
|
559
|
+
if (b.Type === core_1.GeneratedFormSectionType.Top)
|
|
560
|
+
return 1;
|
|
561
|
+
// System sections always last (after related entities)
|
|
562
|
+
const aIsSystem = a.Name.toLowerCase() === 'system' || a.Name.toLowerCase() === 'system metadata';
|
|
563
|
+
const bIsSystem = b.Name.toLowerCase() === 'system' || b.Name.toLowerCase() === 'system metadata';
|
|
564
|
+
if (aIsSystem && !bIsSystem)
|
|
565
|
+
return 1;
|
|
566
|
+
if (!aIsSystem && bIsSystem)
|
|
567
|
+
return -1;
|
|
568
|
+
// Otherwise sort by sequence
|
|
569
|
+
const aSeq = a.MinSequence ?? Number.MAX_SAFE_INTEGER;
|
|
570
|
+
const bSeq = b.MinSequence ?? Number.MAX_SAFE_INTEGER;
|
|
571
|
+
return aSeq - bSeq;
|
|
572
|
+
});
|
|
573
|
+
// now we have a distinct list of section names set, generate HTML for each section
|
|
442
574
|
let sectionIndex = 0;
|
|
443
575
|
for (const section of sections) {
|
|
444
576
|
let sectionName = '';
|
|
@@ -451,44 +583,40 @@ export function Load${entity.ClassName}FormComponent() {
|
|
|
451
583
|
sectionName = this.stripWhiteSpace(section.Name.toLowerCase());
|
|
452
584
|
else if (section.Type === core_1.GeneratedFormSectionType.Details)
|
|
453
585
|
sectionName = 'details';
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
586
|
+
// Generate collapsible panel HTML inline instead of using separate components
|
|
587
|
+
const formHTML = this.generateSectionHTMLForAngular(entity, section);
|
|
588
|
+
// Use category-specific icon from LLM if available, otherwise fall back to keyword matching
|
|
589
|
+
const icon = (categoryIcons && categoryIcons[section.Name]) || this.getIconForCategory(section.Name);
|
|
590
|
+
// NOTE: We'll set the UniqueKey later in generateSingleEntityTypeScriptForAngular()
|
|
591
|
+
// For now, just use a placeholder that will be replaced
|
|
592
|
+
const sectionKey = this.camelCase(section.Name);
|
|
593
|
+
// Build field names string for search functionality (includes both CodeName and DisplayName)
|
|
594
|
+
const fieldSearchTerms = section.Fields ? section.Fields.map(f => {
|
|
595
|
+
const terms = [f.CodeName.toLowerCase()];
|
|
596
|
+
if (f.DisplayName && f.DisplayName.toLowerCase() !== f.CodeName.toLowerCase()) {
|
|
597
|
+
terms.push(f.DisplayName.toLowerCase());
|
|
598
|
+
}
|
|
599
|
+
return terms.join(' ');
|
|
600
|
+
}).join(' ') : '';
|
|
601
|
+
section.TabCode = `${sectionIndex > 0 ? '\n ' : ''}<!-- ${section.Name} Section -->
|
|
602
|
+
<div class="form-card collapsible-card" data-section-name="${section.Name.toLowerCase()}" data-field-names="${fieldSearchTerms}">
|
|
603
|
+
<div class="collapsible-header" (click)="toggleSection('${sectionKey}')" role="button" tabindex="0">
|
|
604
|
+
<div class="collapsible-title">
|
|
605
|
+
<i class="${icon}"></i>
|
|
606
|
+
<h3><span class="section-name">${section.Name}</span></h3>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="collapse-icon">
|
|
609
|
+
<i [class]="sectionsExpanded.${sectionKey} ? 'fa fa-chevron-up' : 'fa fa-chevron-down'"></i>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
<div class="collapsible-body" [class.collapsed]="!sectionsExpanded.${sectionKey}">
|
|
613
|
+
<div class="form-body">
|
|
478
614
|
${formHTML}
|
|
479
|
-
|
|
480
|
-
</div>
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
`;
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
</div>`;
|
|
618
|
+
sectionIndex++;
|
|
619
|
+
}
|
|
492
620
|
if (section.Type !== core_1.GeneratedFormSectionType.Top)
|
|
493
621
|
index++; // don't increment the tab index for TOP AREA, becuse it won't be rendered as a tab
|
|
494
622
|
}
|
|
@@ -632,39 +760,74 @@ export function Load${entity.ClassName}${this.stripWhiteSpace(section.Name)}Comp
|
|
|
632
760
|
async generateRelatedEntityTabs(entity, startIndex, contextUser) {
|
|
633
761
|
const md = new core_1.Metadata();
|
|
634
762
|
const tabs = [];
|
|
635
|
-
|
|
763
|
+
// Sort related entities by Sequence (user's explicit ordering), then by RelatedEntity name (stable tiebreaker)
|
|
764
|
+
const sortedRelatedEntities = entity.RelatedEntities
|
|
765
|
+
.filter(re => re.DisplayInForm)
|
|
766
|
+
.sort((a, b) => {
|
|
767
|
+
if (a.Sequence !== b.Sequence) {
|
|
768
|
+
return a.Sequence - b.Sequence;
|
|
769
|
+
}
|
|
770
|
+
return a.RelatedEntity.localeCompare(b.RelatedEntity);
|
|
771
|
+
});
|
|
636
772
|
let index = startIndex;
|
|
637
773
|
for (const relatedEntity of sortedRelatedEntities) {
|
|
638
774
|
const tabName = this.generateRelatedEntityTabName(relatedEntity, sortedRelatedEntities);
|
|
639
775
|
let icon = '';
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
776
|
+
let iconClass = '';
|
|
777
|
+
// First, check for custom icon
|
|
778
|
+
if (relatedEntity.DisplayIconType === 'Custom' && relatedEntity.DisplayIcon && relatedEntity.DisplayIcon.length > 0) {
|
|
779
|
+
icon = `<span class="${relatedEntity.DisplayIcon} tab-header-icon"></span>`;
|
|
780
|
+
iconClass = relatedEntity.DisplayIcon;
|
|
781
|
+
}
|
|
782
|
+
// If no custom icon, try to use the related entity's icon
|
|
783
|
+
else {
|
|
784
|
+
const re = md.Entities.find(e => e.ID === relatedEntity.RelatedEntityID);
|
|
785
|
+
if (re && re.Icon && re.Icon.length > 0) {
|
|
786
|
+
icon = `<span class="${re.Icon} tab-header-icon"></span>`;
|
|
787
|
+
iconClass = re.Icon;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
// Fall back to default table icon
|
|
791
|
+
iconClass = 'fa fa-table';
|
|
792
|
+
}
|
|
653
793
|
}
|
|
794
|
+
// Calculate section key before generation (may be replaced later if duplicate)
|
|
795
|
+
const sectionKey = this.camelCase(tabName);
|
|
654
796
|
const component = await related_entity_components_1.RelatedEntityDisplayComponentGeneratorBase.GetComponent(relatedEntity, contextUser);
|
|
655
797
|
const generateResults = await component.Generate({
|
|
656
798
|
Entity: entity,
|
|
657
799
|
RelationshipInfo: relatedEntity,
|
|
658
|
-
TabName: tabName
|
|
800
|
+
TabName: tabName,
|
|
801
|
+
SectionKey: sectionKey // Pass section key for IsCurrentSection() calls
|
|
659
802
|
});
|
|
660
|
-
//
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
${
|
|
667
|
-
|
|
803
|
+
// Add proper indentation for collapsible panel body
|
|
804
|
+
const componentCodeWithIndent = generateResults.TemplateOutput.split('\n').map(l => ` ${l}`).join('\n');
|
|
805
|
+
// For related entities, use the related entity name as searchable term
|
|
806
|
+
const relatedEntitySearchTerms = relatedEntity.RelatedEntity.toLowerCase();
|
|
807
|
+
const tabCode = `${index > 0 ? '\n ' : ''}<!-- ${tabName} Section -->
|
|
808
|
+
<div class="form-card collapsible-card related-entity" data-section-name="${tabName.toLowerCase()}" data-field-names="${relatedEntitySearchTerms}" data-section-key="${sectionKey}">
|
|
809
|
+
<div class="collapsible-header" (click)="toggleSection('${sectionKey}')" role="button" tabindex="0">
|
|
810
|
+
<div class="collapsible-title">
|
|
811
|
+
<i class="${iconClass}"></i>
|
|
812
|
+
<h3>
|
|
813
|
+
<span class="section-name">${tabName}</span>
|
|
814
|
+
<span class="row-count-badge"
|
|
815
|
+
*ngIf="sectionRowCounts?.['${sectionKey}'] !== undefined"
|
|
816
|
+
[class.zero-rows]="sectionRowCounts['${sectionKey}'] === 0">
|
|
817
|
+
{{sectionRowCounts['${sectionKey}']}}
|
|
818
|
+
</span>
|
|
819
|
+
</h3>
|
|
820
|
+
</div>
|
|
821
|
+
<div class="collapse-icon">
|
|
822
|
+
<i [class]="sectionsExpanded.${sectionKey} ? 'fa fa-chevron-up' : 'fa fa-chevron-down'"></i>
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
825
|
+
<div class="collapsible-body" [class.collapsed]="!sectionsExpanded.${sectionKey}" *ngIf="record.IsSaved">
|
|
826
|
+
<div class="form-body">
|
|
827
|
+
${componentCodeWithIndent}
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
</div>`;
|
|
668
831
|
tabs.push({
|
|
669
832
|
Type: core_1.GeneratedFormSectionType.Category,
|
|
670
833
|
IsRelatedEntity: true,
|
|
@@ -686,6 +849,129 @@ ${componentCodeWithTabs}
|
|
|
686
849
|
stripWhiteSpace(s) {
|
|
687
850
|
return s.replace(/\s/g, '');
|
|
688
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* Converts a string to camelCase and sanitizes it for use as a JavaScript identifier
|
|
854
|
+
* @param str The string to convert
|
|
855
|
+
* @returns String in camelCase format, safe for use as object key or variable name
|
|
856
|
+
*/
|
|
857
|
+
camelCase(str) {
|
|
858
|
+
// First, replace non-alphanumeric characters (except spaces) with spaces
|
|
859
|
+
let sanitized = str.replace(/[^a-zA-Z0-9\s]/g, ' ');
|
|
860
|
+
// Convert to camelCase
|
|
861
|
+
let result = sanitized
|
|
862
|
+
.replace(/\s(.)/g, (match, char) => char.toUpperCase())
|
|
863
|
+
.replace(/\s/g, '')
|
|
864
|
+
.replace(/^(.)/, (match, char) => char.toLowerCase());
|
|
865
|
+
// If starts with a digit, prefix with underscore
|
|
866
|
+
if (/^\d/.test(result)) {
|
|
867
|
+
result = '_' + result;
|
|
868
|
+
}
|
|
869
|
+
// If result is empty (all special chars), use a default
|
|
870
|
+
if (result.length === 0) {
|
|
871
|
+
result = 'section';
|
|
872
|
+
}
|
|
873
|
+
return result;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Converts a string to PascalCase and sanitizes it for use as a class name
|
|
877
|
+
* @param str The string to convert
|
|
878
|
+
* @returns String in PascalCase format, safe for use as a class name
|
|
879
|
+
*/
|
|
880
|
+
pascalCase(str) {
|
|
881
|
+
// First, replace non-alphanumeric characters (except spaces) with spaces
|
|
882
|
+
let sanitized = str.replace(/[^a-zA-Z0-9\s]/g, ' ');
|
|
883
|
+
// Convert to PascalCase (capitalize first letter of each word)
|
|
884
|
+
let result = sanitized
|
|
885
|
+
.replace(/\s(.)/g, (match, char) => char.toUpperCase())
|
|
886
|
+
.replace(/\s/g, '')
|
|
887
|
+
.replace(/^(.)/, (match, char) => char.toUpperCase());
|
|
888
|
+
// If starts with a digit, prefix with underscore
|
|
889
|
+
if (/^\d/.test(result)) {
|
|
890
|
+
result = '_' + result;
|
|
891
|
+
}
|
|
892
|
+
// If result is empty (all special chars), use a default
|
|
893
|
+
if (result.length === 0) {
|
|
894
|
+
result = 'Section';
|
|
895
|
+
}
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Sanitizes a string to create a valid filename in lowercase format.
|
|
900
|
+
* Removes all non-alphanumeric characters (except spaces) and converts to lowercase.
|
|
901
|
+
* Used for creating component filenames that are safe across all file systems.
|
|
902
|
+
*
|
|
903
|
+
* Example: "Timeline & Budget" → "timelinebudget"
|
|
904
|
+
*
|
|
905
|
+
* @param str The string to sanitize
|
|
906
|
+
* @returns A sanitized lowercase filename string
|
|
907
|
+
*/
|
|
908
|
+
sanitizeFilename(str) {
|
|
909
|
+
// Remove all non-alphanumeric characters (except spaces)
|
|
910
|
+
let sanitized = str.replace(/[^a-zA-Z0-9\s]/g, '');
|
|
911
|
+
// Convert to lowercase and remove all spaces
|
|
912
|
+
let result = sanitized.toLowerCase().replace(/\s/g, '');
|
|
913
|
+
// If result is empty (all special chars), use a default
|
|
914
|
+
if (result.length === 0) {
|
|
915
|
+
result = 'section';
|
|
916
|
+
}
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Maps category names to appropriate Font Awesome icon classes
|
|
921
|
+
* @param category The category name to map
|
|
922
|
+
* @returns Font Awesome icon class string
|
|
923
|
+
*/
|
|
924
|
+
getIconForCategory(category) {
|
|
925
|
+
const lowerCategory = category.toLowerCase();
|
|
926
|
+
// Address/Location categories
|
|
927
|
+
if (lowerCategory.includes('address') || lowerCategory.includes('location')) {
|
|
928
|
+
return 'fa fa-map-marker-alt';
|
|
929
|
+
}
|
|
930
|
+
// Contact Information
|
|
931
|
+
if (lowerCategory.includes('contact')) {
|
|
932
|
+
return 'fa fa-address-card';
|
|
933
|
+
}
|
|
934
|
+
// Financial/Pricing categories
|
|
935
|
+
if (lowerCategory.includes('pric') || lowerCategory.includes('cost') ||
|
|
936
|
+
lowerCategory.includes('payment') || lowerCategory.includes('financial') ||
|
|
937
|
+
lowerCategory.includes('billing')) {
|
|
938
|
+
return 'fa fa-dollar-sign';
|
|
939
|
+
}
|
|
940
|
+
// Date/Time categories
|
|
941
|
+
if (lowerCategory.includes('date') || lowerCategory.includes('time') ||
|
|
942
|
+
lowerCategory.includes('schedule')) {
|
|
943
|
+
return 'fa fa-calendar';
|
|
944
|
+
}
|
|
945
|
+
// Status/State categories
|
|
946
|
+
if (lowerCategory.includes('status') || lowerCategory.includes('state')) {
|
|
947
|
+
return 'fa fa-flag';
|
|
948
|
+
}
|
|
949
|
+
// Metadata/Technical categories
|
|
950
|
+
if (lowerCategory.includes('metadata') || lowerCategory.includes('technical') ||
|
|
951
|
+
lowerCategory.includes('system')) {
|
|
952
|
+
return 'fa fa-cog';
|
|
953
|
+
}
|
|
954
|
+
// Description/Details categories
|
|
955
|
+
if (lowerCategory.includes('description') || lowerCategory.includes('detail')) {
|
|
956
|
+
return 'fa fa-align-left';
|
|
957
|
+
}
|
|
958
|
+
// Settings/Configuration categories
|
|
959
|
+
if (lowerCategory.includes('setting') || lowerCategory.includes('config') ||
|
|
960
|
+
lowerCategory.includes('preference')) {
|
|
961
|
+
return 'fa fa-sliders-h';
|
|
962
|
+
}
|
|
963
|
+
// Shipping/Delivery categories
|
|
964
|
+
if (lowerCategory.includes('ship') || lowerCategory.includes('delivery')) {
|
|
965
|
+
return 'fa fa-truck';
|
|
966
|
+
}
|
|
967
|
+
// User/Person categories
|
|
968
|
+
if (lowerCategory.includes('user') || lowerCategory.includes('person') ||
|
|
969
|
+
lowerCategory.includes('customer') || lowerCategory.includes('employee')) {
|
|
970
|
+
return 'fa fa-user';
|
|
971
|
+
}
|
|
972
|
+
// Default icon for uncategorized sections
|
|
973
|
+
return 'fa fa-info-circle';
|
|
974
|
+
}
|
|
689
975
|
/**
|
|
690
976
|
* Generates the complete HTML template for a single entity form
|
|
691
977
|
* @param entity The entity to generate HTML for
|
|
@@ -693,8 +979,22 @@ ${componentCodeWithTabs}
|
|
|
693
979
|
* @returns Promise resolving to an object containing the HTML code and section information
|
|
694
980
|
*/
|
|
695
981
|
async generateSingleEntityHTMLForAngular(entity, contextUser) {
|
|
982
|
+
// Load category icons from EntitySetting if available
|
|
983
|
+
let categoryIcons;
|
|
984
|
+
const entitySettings = entity.Settings;
|
|
985
|
+
if (entitySettings) {
|
|
986
|
+
const iconSetting = entitySettings.find((s) => s.Name === 'FieldCategoryIcons');
|
|
987
|
+
if (iconSetting && iconSetting.Value) {
|
|
988
|
+
try {
|
|
989
|
+
categoryIcons = JSON.parse(iconSetting.Value);
|
|
990
|
+
}
|
|
991
|
+
catch (e) {
|
|
992
|
+
// Invalid JSON, ignore and fall back to keyword matching
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
696
996
|
const topArea = this.generateTopAreaHTMLForAngular(entity);
|
|
697
|
-
const additionalSections = this.generateAngularAdditionalSections(entity, 0);
|
|
997
|
+
const additionalSections = this.generateAngularAdditionalSections(entity, 0, categoryIcons);
|
|
698
998
|
// 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
999
|
const endingIndex = additionalSections && additionalSections.length ? (topArea && topArea.length > 0 ? additionalSections.length - 1 : additionalSections.length) : 0;
|
|
700
1000
|
const relatedEntitySections = await this.generateRelatedEntityTabs(entity, endingIndex, contextUser);
|
|
@@ -710,15 +1010,15 @@ ${componentCodeWithTabs}
|
|
|
710
1010
|
* @returns Generated HTML with splitter layout
|
|
711
1011
|
*/
|
|
712
1012
|
generateSingleEntityHTMLWithSplitterForAngular(topArea, additionalSections, relatedEntitySections) {
|
|
713
|
-
const htmlCode = `<div class="record-form-container"
|
|
714
|
-
<form *ngIf="record" class="record-form"
|
|
1013
|
+
const htmlCode = `<div class="record-form-container">
|
|
1014
|
+
<form *ngIf="record" class="record-form" #form="ngForm">
|
|
715
1015
|
<mj-form-toolbar [form]="this"></mj-form-toolbar>
|
|
716
|
-
<kendo-splitter orientation="vertical" (layoutChange)="splitterLayoutChange()"
|
|
1016
|
+
<kendo-splitter orientation="vertical" (layoutChange)="splitterLayoutChange()">
|
|
717
1017
|
<kendo-splitter-pane [collapsible]="true" [size]="TopAreaHeight">
|
|
718
1018
|
${this.innerTopAreaHTML(topArea)}
|
|
719
1019
|
</kendo-splitter-pane>
|
|
720
1020
|
<kendo-splitter-pane>
|
|
721
|
-
${this.
|
|
1021
|
+
${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
|
|
722
1022
|
</kendo-splitter-pane>
|
|
723
1023
|
</kendo-splitter>
|
|
724
1024
|
</form>
|
|
@@ -740,6 +1040,61 @@ ${this.innerTabStripHTML(additionalSections, relatedEntitySections)}
|
|
|
740
1040
|
</div>`;
|
|
741
1041
|
}
|
|
742
1042
|
/**
|
|
1043
|
+
* Generates the HTML for collapsible panels containing all form sections
|
|
1044
|
+
* @param additionalSections Array of field-based form sections
|
|
1045
|
+
* @param relatedEntitySections Array of related entity sections
|
|
1046
|
+
* @returns HTML string for all collapsible panels
|
|
1047
|
+
*/
|
|
1048
|
+
innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections) {
|
|
1049
|
+
// Filter out Top sections as they're handled separately
|
|
1050
|
+
const sectionsToRender = additionalSections.filter(s => s.Type !== core_1.GeneratedFormSectionType.Top);
|
|
1051
|
+
const totalSections = sectionsToRender.length + relatedEntitySections.length;
|
|
1052
|
+
// Only show section controls if there are 4+ sections
|
|
1053
|
+
const showControls = totalSections >= 4;
|
|
1054
|
+
const controlsHTML = showControls ? ` <div class="form-section-controls">
|
|
1055
|
+
<div class="control-group">
|
|
1056
|
+
<button (click)="expandAllSections()" title="Expand all sections">
|
|
1057
|
+
<i class="fa fa-expand-alt"></i>Expand All
|
|
1058
|
+
</button>
|
|
1059
|
+
<button (click)="collapseAllSections()" title="Collapse all sections">
|
|
1060
|
+
<i class="fa fa-compress-alt"></i>Collapse All
|
|
1061
|
+
</button>
|
|
1062
|
+
</div>
|
|
1063
|
+
<input type="text"
|
|
1064
|
+
class="section-search"
|
|
1065
|
+
placeholder="Search sections..."
|
|
1066
|
+
(input)="filterSections($event)"
|
|
1067
|
+
#sectionSearch>
|
|
1068
|
+
<span class="section-count">
|
|
1069
|
+
<span class="section-count-badge">{{getExpandedCount()}}</span> of ${totalSections} expanded
|
|
1070
|
+
</span>
|
|
1071
|
+
</div>
|
|
1072
|
+
` : '';
|
|
1073
|
+
// Combine field sections and related entity sections into panels, respecting DisplayLocation
|
|
1074
|
+
if (relatedEntitySections.length > 0) {
|
|
1075
|
+
const relatedEntityBeforePanels = relatedEntitySections.filter(s => s.RelatedEntityDisplayLocation === 'Before Field Tabs');
|
|
1076
|
+
const relatedEntityAfterPanels = relatedEntitySections.filter(s => s.RelatedEntityDisplayLocation === 'After Field Tabs');
|
|
1077
|
+
// Wrap related entity sections in grid container
|
|
1078
|
+
const beforePanelsHTML = relatedEntityBeforePanels.length > 0
|
|
1079
|
+
? ` <div class="related-entity-grid">\n${relatedEntityBeforePanels.map(s => s.TabCode).join('\n')}\n </div>`
|
|
1080
|
+
: '';
|
|
1081
|
+
const afterPanelsHTML = relatedEntityAfterPanels.length > 0
|
|
1082
|
+
? ` <div class="related-entity-grid">\n${relatedEntityAfterPanels.map(s => s.TabCode).join('\n')}\n </div>`
|
|
1083
|
+
: '';
|
|
1084
|
+
const fieldPanelsHTML = sectionsToRender.map(s => s.TabCode).join('\n');
|
|
1085
|
+
return `${controlsHTML} <div class="form-panels-container">
|
|
1086
|
+
${beforePanelsHTML}${beforePanelsHTML && fieldPanelsHTML ? '\n' : ''}${fieldPanelsHTML}${fieldPanelsHTML && afterPanelsHTML ? '\n' : ''}${afterPanelsHTML}
|
|
1087
|
+
</div>`;
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
// No related entities, just show field panels
|
|
1091
|
+
return `${controlsHTML} <div class="form-panels-container">
|
|
1092
|
+
${sectionsToRender.map(s => s.TabCode).join('\n')}
|
|
1093
|
+
</div>`;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* @deprecated Use innerCollapsiblePanelsHTML instead
|
|
743
1098
|
* Generates the HTML for the tab strip containing all form sections
|
|
744
1099
|
* @param additionalSections Array of field-based form sections
|
|
745
1100
|
* @param relatedEntitySections Array of related entity sections
|
|
@@ -764,11 +1119,11 @@ ${this.innerTabStripHTML(additionalSections, relatedEntitySections)}
|
|
|
764
1119
|
* @returns Generated HTML without splitter layout
|
|
765
1120
|
*/
|
|
766
1121
|
generateSingleEntityHTMLWithOUTSplitterForAngular(topArea, additionalSections, relatedEntitySections) {
|
|
767
|
-
const htmlCode = `<div class="record-form-container"
|
|
768
|
-
<form *ngIf="record" class="record-form"
|
|
1122
|
+
const htmlCode = `<div class="record-form-container">
|
|
1123
|
+
<form *ngIf="record" class="record-form" #form="ngForm">
|
|
769
1124
|
<mj-form-toolbar [form]="this"></mj-form-toolbar>
|
|
770
1125
|
${this.innerTopAreaHTML(topArea)}
|
|
771
|
-
${this.
|
|
1126
|
+
${this.innerCollapsiblePanelsHTML(additionalSections, relatedEntitySections)}
|
|
772
1127
|
</form>
|
|
773
1128
|
</div>
|
|
774
1129
|
`;
|