Add custom field in SuiteCRM8

Hi all,
The add fields extension page is currently empty. Does anyone has the sample code showing how to add a custom field? I would like to add a file upload field.

Thank!

This is my solution.

Assumption:

  • The field has a vardef type of “lmfile”.
  • There are 2 columns in the database table - one for the name of the document (_c), the other store the document id (_id_c).

Initially, I followed the instructions provided to create the FE extension as described in here

I created a fields folder
image

This is my detail component

// lmfile.component.html

<a href="/suitecrm/#/documents/record/{{ this.record.attributes[this.field.definition.id_name] }}">{{field.value}}</a>

// lmfile.component.ts

import {Component} from '@angular/core';
import {BaseFieldComponent} from 'core';
import {DataTypeFormatter} from 'core';
import {FieldLogicManager} from 'core';

@Component({
    selector: 'scrm-lmfile-detail',
    templateUrl: './lmfile.component.html',
    styleUrls: []
})
export class LmFileDetailFieldComponent extends BaseFieldComponent {

    constructor(protected typeFormatter: DataTypeFormatter, protected logic: FieldLogicManager) {
        super(typeFormatter, logic);
    }
}

// lmfile.module.ts

import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';

import {LmFileDetailFieldComponent} from './lmfile.component';
import {FormsModule} from '@angular/forms';

@NgModule({
    declarations: [LmFileDetailFieldComponent],
    exports: [LmFileDetailFieldComponent],
    imports: [
        CommonModule,
        FormsModule
    ]
})
export class LmFileDetailFieldModule {
}

This is my edit component
// lmfile.component.html

<input type="file" style="display: none;"
       (change)="onFileSelected($event)" #fileUpload>

<div class="file-upload">
    <button color="primary" (click)="fileUpload.click()">
        Upload File
    </button>

    <a href="/suitecrm/#/documents/record/{{ this.record.attributes[this.field.definition.id_name] }}">{{ this.field.value || "No file uploaded yet." }}</a>
</div>

lmfile.component.ts

import {Component, OnDestroy, OnInit} from '@angular/core';
import { Location } from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {BaseFieldComponent} from 'core';
import {DataTypeFormatter} from 'core';
import {FieldLogicManager} from 'core';

interface Document 
{
    module: string;
    record: string;
    isDuplicate: string;
    action: string;
    return_module: string;
    return_action: string;
    return_id: string;
    module_tab: string;
    contact_role: string;
    relate_to: string;
    relate_id: string;
    offset: string;
    old_id: string;
    contract_id: string;
    deleteAttachment: string;
    filename: string;
    doc_id: string;
    doc_url: string;
    filename_old_doctype: string;
    filename_escaped: string;
    filename_remoteName: string;
    status_id: string;
    revision: string;
    template_type: string;
    is_template: string;
    exp_date: string;
    category_id: string;
    subcategory_id: string;
    description: string;
    related_document_name: string;
    related_doc_id: string;
    assigned_user_name?: string;
    assigned_user_id?: string;
    active_date?: string;
}

@Component({
    selector: 'scrm-lmfile-edit',
    templateUrl: './lmfile.component.html',
    styleUrls: []
})
export class LmFileEditFieldComponent extends BaseFieldComponent implements OnInit, OnDestroy {

    constructor(protected typeFormatter: DataTypeFormatter, protected logic: FieldLogicManager, private http: HttpClient, private location: Location) {
        super(typeFormatter, logic);
    }

    ngOnInit(): void {
        super.ngOnInit();
        this.subscribeValueChanges();
    }

    ngOnDestroy(): void {
        this.unsubscribeAll();
    }

    onFileSelected($event: any) {

        const file: File = $event.target.files[0];

        if (file) {
            this.field.value = file.name;

            const formData = new FormData();

            const document: Document = {
                module:                   'Documents',
                record:                   '',
                isDuplicate:              'false',
                action:                   'Save',
                return_action:            'DetailView',
                return_module:            'Documents',
                return_id:                '',
                module_tab:               '',
                contact_role:             '',
                relate_to:                'Documents',
                relate_id:                '',
                offset:                   '1',
                old_id:                   '',
                contract_id:              '',
                deleteAttachment:         '0',
                filename:                 '',
                doc_id:                   '',
                doc_url:                  '',
                filename_old_doctype:     'Sugar',
                filename_escaped:         '',
                filename_remoteName:      '',
                status_id:                'Active',
                revision:                 '1',
                template_type:            '',
                is_template:              '0',
                exp_date:                 '',
                category_id:              '',
                subcategory_id:           '',
                description:              '',
                related_document_name:    '',
                related_doc_id:           '',
            };

            for (const key in document) {
                formData.append(`${key}`, `${document[key]}`);
            }

            formData.append('filename_file', file);
            formData.append('document_name', file.name);
            
            const root = window.location.href.replace(this.location.path(), '').replace('?#', '');

            this.http.post(root + "legacy/index.php", formData, {observe: 'response', responseType: 'text'})
                     .subscribe(resp => {
                        const parameters = new URLSearchParams(new URL(resp.url).search);
                        this.parent.attributes[this.field.name.replace('_c', '_id_c')] = parameters.get('record')?? '';
                    });
        }
    }
}

// lmfile.module.ts

import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';

import {LmFileEditFieldComponent} from './lmfile.component';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';

@NgModule({
    declarations: [LmFileEditFieldComponent],
    exports: [LmFileEditFieldComponent],
    imports: [
        CommonModule,
        FormsModule,
        ReactiveFormsModule
    ]
})
export class LmFileEditFieldModule {
}

I also have to register my new custom field in extension.module.ts

import {FieldRegistry} from 'core';
import {LmFileEditFieldModule} from '../fields/lmfile/templates/edit/lmfile.module';
import {LmFileDetailFieldModule} from '../fields/lmfile/templates/detail/lmfile.module';
import {LmFileEditFieldComponent} from '../fields/lmfile/templates/edit/lmfile.component';
import {LmFileDetailFieldComponent} from '../fields/lmfile/templates/detail/lmfile.component';

@NgModule({
    declarations: [],
    imports: [
        CommonModule,
        LmFileEditFieldModule,
        LmFileDetailFieldModule
    ],
})
export class ExtensionModule {
    constructor(protected fieldRegistry: FieldRegistry) {

        // Refer to baseViewFieldsMap in core/app/core/src/lib/fields/base-fields.manifest.ts
        //   mode can be detail, edit, list, filter
        fieldRegistry.register('default', 'lmfile', 'edit', LmFileEditFieldComponent);
        fieldRegistry.register('default', 'lmfile', 'detail', LmFileDetailFieldComponent);
    }

    init(): void {
    }
}