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

Creating your own data grid (table) component

The data grid of Hubleto is the React component Table from @hubleto/react-ui. You rarely write a grid yourself. You configure Table with props and the description from your model.

The simplest table

A Twig view can render a table for any model without any React code:

apps/Deals/Views/Tags.twig
<hblreact-table
  int:record-id="{{ viewParams.recordId }}"
  string:model="Hubleto/App/Community/Deals/Models/Tag"
></hblreact-table>

A table component of your app

When you need a form on click, filters from the URL or custom behavior, create a component:

apps/Customers/Components/FC/TableCustomers.tsx
import React from 'react'
import Translator from '@hubleto/react-ui/core/Translator';
import Table from '@hubleto/react-ui/components/fc/Table';
import { type TableMeta, type TableProps } from '@hubleto/react-ui/components/fc/TableInterfaces';
import FormCustomer from './FormCustomer';

interface TableCustomersProps extends TableProps {}

const componentName = 'TableCustomers'; // must be the same as the exported const
const parentApp = 'Hubleto/App/Community/Customers';
const T = new Translator(parentApp + '/Loader', 'Components/' + componentName);

const TableCustomers = (props: TableCustomersProps) => {
  return <Table
    componentName={componentName}
    parentApp={parentApp}
    model={parentApp + '/Models/Customer'}
    baseUrlSlug='customers'
    formModalProps={{type: 'right wide'}}
    renderCell={(table: TableMeta, columnName: string, column: any, data: any, options: any) => {
      if (columnName == "virt_tags") {
        return <div className='flex gap-1'>
          {data.TAGS.map((tag, key) => {
            return <div key={key} className="text-nowrap mr-2">
              <i style={{color: tag.TAG?.color}} className="fas fa-tag mr-2"></i>
              {tag.TAG?.name}
            </div>;
          })}
        </div>;
      } else {
        return table.renderDefaultCell(columnName, column, data, options);
      }
    }}
    renderForm={(table: TableMeta): React.JSX.Element => {
      return <FormCustomer {...table.getDefaultFormProps()}/>;
    }}
    {...props}
  ></Table>
}

export default TableCustomers;

Props of Table

react-ui: components/fc/TableInterfaces.tsx (excerpt)
export interface TableProps {
  ref?: any,
  componentName?: string,
  parentApp?: string,
  uid?: string,
  description?: TableDescription,
  descriptionSource?: 'props' | 'request' | 'both',
  recordId?: any,
  formDefaultValues?: any,
  formEndpoint?: FormEndpoint,
  formEndpointParams?: any,
  formModalProps?: ModalProps,
  formProps?: FormProps,
  formActiveTabUid?: any,
  formReactComponent?: string,
  endpoint?: TableEndpoint,
  endpointParams?: any,
  model?: string,
  parentForm?: any,
  tag?: string,
  context?: string,
  where?: Array<any>,
  params?: any,
  page?: number,
  itemsPerPage?: number,
  inlineEditingEnabled?: boolean,
  isInlineEditing?: boolean,
  isUsedAsInput?: boolean,
  selectionMode?: TableSelectionMode,
  selection?: Array<any>,
  data?: TableData,
  async?: boolean,
  readonly?: boolean,
  title?: string,
  closeFormAfterSave?: boolean,
  className?: string,
  fulltextSearch?: string,
  columnSearch?: any,
  filters?: any,
  view?: string,
  invalidInputs?: Array<TableInvalidInput>,
  crudController?: string,
  junctionTitle?: string,
  junctionModel?: string,
  junctionSourceColumn?: string,
  junctionDestinationColumn?: string,
  junctionSourceRecordId?: number,
  junctionSaveEndpoint?: string,
  baseUrlSlug?: string,

  getEndpointParams?: (table: TableMeta) => object,
  getEndpointUrl?: (table: TableMeta, action: string) => string,
  getCsvImportEndpointParams?: (table: TableMeta) => any,
  getSelectionMode?: (table: TableMeta) => any,
  getRecordsToDisplay?: (table: TableMeta) => any,
  getFormProps?: (table: TableMeta) => any,
  getFormModalProps?: (table: TableMeta) => any,
  getCellClassName?: (table: TableMeta, columnName: string, column: any, rowData: any) => any,
  getCellCssStyle?: (table: TableMeta, columnName: string, column: any, rowData: any) => any,
  getMinColumnValue?: (table: TableMeta, columnName: string) => any,
  getMaxColumnValue?: (table: TableMeta, columnName: string) => any,
  getRowClassName?: (table: TableMeta, rowData: any) => any,

  renderAddButton?: (table: TableMeta) => any,
  renderMoreActionsButton?: (table: TableMeta) => any,
  renderHeaderButtons?: (table: TableMeta) => any,
  renderReloadButton?: (table: TableMeta) => any,
  renderHeaderLeft?: (table: TableMeta) => any,
  renderFulltextSearch?: (table: TableMeta) => any,
  renderTitle?: (table: TableMeta) => any,
  renderHeaderRight?: (table: TableMeta) => any,
  renderHeader?: (table: TableMeta) => any,
  renderFilter?: (table: TableMeta) => any,
  renderSidebarFilter?: (table: TableMeta) => any,
  renderFooter?: (table: TableMeta) => any,
  renderDeleteConfirmModal?: (table: TableMeta) => any,
  renderFormModal?: (table: TableMeta) => any,
  renderForm?: (table: TableMeta) => any,
  renderCell?: (table: TableMeta, columnName: string, column: any, data: any, options: any) => any,
  renderInsertButton?: (table: TableMeta, row: any) => any,
  renderDeleteButton?: (table: TableMeta, row: any) => any,
  renderActionsColumn?: (table: TableMeta, row: any) => any,
  renderRecordsAsTree?: (table: TableMeta, nodes: any, idParent: number, level: number) => React.JSX.Element,
  renderRecords?: (table: TableMeta) => any,
  renderContent?: (table: TableMeta) => any,
  renderCsvExportForm?: (table: TableMeta) => any,

  onAddClick?: (table: TableMeta) => void,
  onRowEdited?: (table: TableMeta, input: any, value: any) => void,
  onRowClick?: (table: TableMeta, row: any) => void,
  onPaginationChange?: (table: TableMeta, page: number, itemsPerPage: number) => void,
  onFilterChange?: (table: TableMeta, filterBy: any) => void,
  onOrderByChange?: (table: TableMeta, orderBy: TableOrderBy) => void,
  onAfterLoadData?: (table: TableMeta) => void,
  onAfterLoadDescription?: (table: TableMeta) => void,
}
Prop Purpose
model The model that provides the data and the description.
tag A unique name of the table in the page.
uid A unique ID of the component instance. Build it from the uid of the parent.
parentForm The form in which the table is embedded (for related records).
endpointParams Extra parameters for the data endpoint.
formDefaultValues, formEndpointParams, formReactComponent The form that opens when a row is clicked.
junctionModel, junctionSourceColumn, junctionDestinationColumn, junctionSourceRecordId Many-to-many tables.
filters, where Fixed filters of the table.
view The way of rendering rows, e.g. cards.
readonly, selectionMode, isUsedAsInput Behavior.
onRowClick, onAddClick, onAfterLoadData, onAfterLoadDescription Callbacks.

Frequently used props. The visibility of the header, the footer and the buttons comes from describeTable() ($description->show() and hide()).

Where the data comes from

The table calls generic API controllers:

Endpoint Does
api/table/describe Returns describeTable() of the model together with its columns.
api/record/load-table-data Returns the records: loadTableData() of the record manager.
api/table-describe-and-load Both in one request (used by default).
api/record/save, api/record/delete Create, update and delete a record.
api/record/save-junction Save a row of a many-to-many table.
api/table-export-csv CSV export of the table.

Generic endpoints of the table.

You change what is shown on the server (describeTable(), prepareReadQuery(), addUrlFiltersToQuery()), not in the component:

apps/Deals/Models/RecordManagers/Deal.php (excerpt)
  public function addUrlFiltersToQuery(mixed $query): mixed
  {
    $query = parent::addUrlFiltersToQuery($query);

    $hubleto = \Hubleto\Erp\Loader::getGlobalApp();

    if ($hubleto->router()->urlParamAsInteger("idCustomer") > 0) {
      $query = $query->where("deals.id_customer", $hubleto->router()->urlParamAsInteger("idCustomer"));
    }

    $filters = $hubleto->router()->urlParamAsArray("filters");

    $query = Workflow::applyWorkflowStepFilter(
      $this->model,
      $query,
      (array) ($filters['fDealWorkflowStep'] ?? [])
    );

    if (isset($filters["fDealSourceChannel"]) && $filters["fDealSourceChannel"] > 0) {
      $query = $query->where("deals.source_channel", $filters["fDealSourceChannel"]);
    }
    if (isset($filters["fDealResult"]) && $filters["fDealResult"] > 0) {
      $query = $query->where("deals.deal_result", $filters["fDealResult"]);
    }
    if (isset($filters["fDealBusinessType"]) && $filters["fDealBusinessType"] > 0) {
      $query = $query->where("deals.business_type", $filters["fDealBusinessType"]);
    }
    if (isset($filters["fDealBusinessType"]) && $filters["fDealBusinessType"] > 0) {
      $query = $query->where("deals.business_type", $filters["fDealBusinessType"]);
    }

    if (isset($filters["fDealOwnership"])) {
      switch ($filters["fDealOwnership"]) {
        case 1: $query = $query->where("deals.id_owner", $hubleto->authProvider()->getUserId());
          break;
        case 2: $query = $query->where("deals.id_manager", $hubleto->authProvider()->getUserId());
          break;
      }
    }

    $fDealClosed = $filters['fDealClosed'] ?? 1;
    if ($fDealClosed == 1) $query = $query->where("deals.is_closed", false);
    if ($fDealClosed == 2) $query = $query->where("deals.is_closed", true);

Filtering a related table

To show the contacts of the customer that is open in the form, embed the table of contacts, pass the form as parentForm and the ID as a prop (idCustomer). The table sends it to the API, and the record manager of the contacts applies it in addUrlFiltersToQuery():

apps/Customers/Components/FC/FormCustomer.tsx (excerpt)
      <TableContacts
        uid={props.uid + "_table_contacts"}
        parentForm={form}
        view='cards'
        idCustomer={form.id}
      ></TableContacts>

Related pages