www.hubleto.eu Reddit User Guide Stack Overflow LinkedIn Report an issue

Customizing forms

A form is built from three things: the description from the model's describeForm(), the tabs and inputs written in the form component, and the customizations that other apps add with FormCustomizer.

Props of a form

Form is configured with props. The most useful ones:

react-ui: components/fc/FormInterfaces.ts (excerpt)
export interface FormProps {
  activeTabUid?: string,
  children?: any,
  componentName?: string,
  context?: string,
  description?: FormDescription,
  descriptionSource?: FormDescriptionSource,
  endpoint?: FormEndpoint,
  endpointParams?: any,
  folderUrl?: string,
  showInModal?: boolean,
  getContentClassName?: (form: FormMeta) => string,
  getEndpointParams?: (form: FormMeta) => object,
  getEndpointUrl?: (form: FormMeta) => string,
  getRecordFormUrl?: (form: FormMeta) => string,
  getTabs?: (form: FormMeta) => FormTabs,
  id?: any,
  isFullscreen?: boolean,
  isInitialized?: boolean,
  junctionDestinationColumn?: string,
  junctionModel?: string,
  junctionSaveEndpoint?: string,
  junctionSourceColumn?: string,
  junctionSourceRecordId?: number,
  junctionTitle?: string,
  modal?: any,
  model?: string,
  showHeader?: boolean,
  showFooter?: boolean,
  nextId?: any,
  onAfterCopyRecord?: (form: FormMeta, record: FormRecord) => void,
  onAfterDeleteRecord?: (form: FormMeta, saveResponse: any) => void,
  onAfterFormInitialized?: (form: FormMeta) => void,
  onAfterRecordLoaded?: (form: FormMeta, record: FormRecord) => void,
  onAfterSaveRecord?: (form: FormMeta, saveResponse: any, customSaveOptions?: any) => void,
  onBeforeCopyRecord?: (form: FormMeta, record: FormRecord) => FormRecord,
  onBeforeSaveRecord?: (form: FormMeta, record: FormRecord) => FormRecord,
  onChange?: (form: FormMeta, changedRecord: FormRecord) => void,
  onClose?: (form: FormMeta) => void,
  onTabChange?: (form: FormMeta) => void,
  parentApp?: any,
  parentTable?: any,
  permissions?: FormPermissions,
  prevId?: any,
  readonly?: boolean,
  record?: any,
  saveRecordWhenInitialized?: any,
  showOwnerManagerSelector?: boolean,
  showOwnerManagerUi?: boolean,
  showWorkflowUi?: boolean,
  showPreviewUi?: boolean,
  tabs?: FormTabs,
  tag?: string,
  timeline?: Array<any>,
  translationContext?: string,
  translationContextInner?: string,
  uid?: string,
  urlSlug?: string,
  title?: any,

  renderTopMenuButton?: (form: FormMeta, tabUid: string) => React.JSX.Element,
  renderTopInputs?: (form: FormMeta) => React.JSX.Element,
  renderTopMenu?: (form: FormMeta) => React.JSX.Element,
  renderTimeline?: (form: FormMeta, timelineConfig: any) => React.JSX.Element,
  renderTab?: (form: FormMeta, tab: string) => React.JSX.Element,
  renderContent?: (form: FormMeta) => React.JSX.Element,
  renderPrintPreviewUi?: (form: FormMeta) => React.JSX.Element,
  renderHeaderExtraButtons?: (form: FormMeta) => React.JSX.Element,
  renderFooterExtraButtons?: (form: FormMeta) => React.JSX.Element,
  renderSaveButton?: (form: FormMeta) => React.JSX.Element,
  renderCopyButton?: (form: FormMeta) => React.JSX.Element,
  renderDeleteButton?: (form: FormMeta) => React.JSX.Element,
  renderPrevRecordButton?: (form: FormMeta) => React.JSX.Element,
  renderNextRecordButton?: (form: FormMeta) => React.JSX.Element,
  renderFullscreenButton?: (form: FormMeta) => React.JSX.Element,
  renderCloseButton?: (form: FormMeta) => React.JSX.Element,
  renderPrintPreviewUiButton?: (form: FormMeta) => React.JSX.Element,
  renderHeader?: (form: FormMeta) => React.JSX.Element,
  renderHeaderLeft?: (form: FormMeta) => React.JSX.Element,
  renderHeaderRight?: (form: FormMeta) => React.JSX.Element,
  renderFooter?: (form: FormMeta) => React.JSX.Element,
  renderTitle?: (form: FormMeta) => React.JSX.Element,
  renderWarningsOrErrors?: (form: FormMeta) => React.JSX.Element,
  renderSaveErrorMessage?: (form: FormMeta) => React.JSX.Element,
}
Prop Purpose
model, endpoint Which model to show and which API to call.
getTabs Returns the tabs of the form.
onBeforeSaveRecord Change the record before it is saved. Return the record.
onAfterSaveRecord React when the record was saved (e.g. reload a table).
onAfterRecordLoaded Prepare the form after the record was loaded.
onChange Called when any input changes.
readonly, permissions Turn the form read-only or hide the buttons.

Frequently used props.

Tabs and inputs

Each tab is a small function component that renders inputs of the form:

apps/Customers/Components/FC/FormCustomer.tsx (excerpt)
    tabs={{
      default: {title: <b>{T.translate('Customer')}</b>, content: () => <TabDefault {...props} />},
      calendar: {title: T.translate('Calendar'), content: () => <TabCalendar {...props} />},
    }}

Use <Input field='name' /> for an input defined by the model. The input takes the type and title from describeColumns(). Format a single input with customInputProps and cssClass:

apps/Customers/Components/FC/FormCustomer.tsx (excerpt)
      <Input field='name' renderOnlyInputField customInputProps={{cssClass: 'text-[2em] border border-primary p-1 shadow rounded'}} />

Workflow selector

A form that has the columns id_workflow and id_workflow_step shows the workflow selector automatically. See Workflow.

FormCustomizer: extend forms of other apps

FormCustomizer is how an app adds something to a form that belongs to a different app, without editing it:

react-ui: core/FormCustomizer.tsx
import { type FormMeta, type FormTab } from "../components/fc/FormInterfaces";

export interface FormExtraButton {
  title: string,
  icon: string,
  onClick: any,
}

export default class FormCustomizer {

  static tabs: any = {};
  static headerExtraButtons: any = {};
  static footerExtraButtons: any = {};

  static addTab(
    componentName: string,
    tabUid: string,
    mount: (form: FormMeta) => boolean|FormTab,
  ) {
    if (!this.tabs[componentName]) {
      this.tabs[componentName] = {};
    }
    this.tabs[componentName][tabUid] = {mount: mount};
  }

  static getTabs(componentName: string) {
    return this.tabs[componentName] ?? [];
  }

  static addFormHeaderExtraButton(
    componentName: string,
    mount: (form: FormMeta) => boolean|FormExtraButton,
  ) {
    if (!this.headerExtraButtons[componentName]) {
      this.headerExtraButtons[componentName] = [];
    }
    this.headerExtraButtons[componentName].push({mount: mount});
  }

  static getFormHeaderExtraButtons(componentName: string) {
    return this.headerExtraButtons[componentName] ?? [];
  }

  static addFormFooterExtraButton(
    componentName: string,
    mount: (form: FormMeta) => boolean|FormExtraButton,
  ) {
    if (!this.footerExtraButtons[componentName]) {
      this.footerExtraButtons[componentName] = [];
    }
    this.footerExtraButtons[componentName].push({mount: mount});
  }

  static getFormFooterExtraButtons(componentName: string) {
    return this.footerExtraButtons[componentName] ?? [];
  }

}

Deals adds a button to the header of the form of an order, Projects adds a tab:

apps/Deals/Loader.tsx (excerpt)
    FormCustomizer.addFormHeaderExtraButton(
      'FormLead',
      (form: FormMeta) => { return form.id <= 0 ? false : {
        title: T.translate('Create deal'),
        icon: 'fas fa-diagram-project',
        onClick: (form: FormMeta) => {
          request.get(
            'deals/api/create-from-lead',
            {idLead: form.id},
            (data: any) => {
                if (data.status == "success") {
                globalThis.window.open(globalThis.hubleto.config.projectUrl + `/deals/${data.idDeal}`)
                }
            }
          );
        }
      }}
    )
apps/Projects/Loader.tsx (excerpt)
    FormCustomizer.addTab(
      'FormOrder',
      'projects',
      (form: FormMeta) => { return form.id <= 0 ? false : {
        title: T.translate('Projects'),
        content: (form: FormMeta) => {
          return <TableProjects
            tag={"table_project_order"}
            parentForm={form}
            //@ts-ignore
            description={{ui: {showHeader:false}}}
            descriptionSource='both'
            uid={form.uid + "_table_project_order"}
            junctionTitle={T.translate('Order')}
            junctionModel='Hubleto/App/Community/Projects/Models/ProjectOrder'
            junctionSourceColumn='id_order'
            junctionSourceRecordId={form.id}
            junctionDestinationColumn='id_project'
          />;
        }
      }}
    );

NOTE The first parameter is the registered name of the form component of the other app (FormOrder). Return false from mount to add nothing, e.g. for a record that is not saved yet.

Server-side customization

apps/Deals/Models/Deal.php (excerpt)
  public function describeForm(): \Hubleto\Framework\Description\Form
  {
    $description = parent::describeForm();

    /** @var Workflow */
    $mWorkflow = $this->getModel(Workflow::class);
    $description->defaultValues = $mWorkflow->applyDefaultWorkflow([], 'deals');

    /** @var CurrencyService */
    $currencyService = $this->getService(CurrencyService::class);
    $description->defaultValues["id_currency"] = $currencyService->getDefaultCurrencyId();

    return $description;
  }