Skip to content
Developerhome
X3

Customizing a TableField Component

  Less than to read

This page summarizes how to provide customization on a TableField, by overriding default properties in an extension page. We will demonstrate using the MobileInternalStockChange page Override and customize the Stock TableField.

This customization is an extension of the standard mobile application, see also Extending a standard mobile application.

To begin, in the development environment, within the directory /xtrem-dev/xtrem/x3-services/application/x3-stock/lib/, create a directory called page-extensions if it is not present. Create a new TypeScript file within it named mobile-internal-stock-change-extension.ts.

New Folder and File

Now use the following examples to customize any of:

Customize the title and helper text

standard TableField customized TableField
Title= Stock
No helper text
Title= Stock lines
Helper text= Stock lines for the Internal stock change
title-and-helper1 title-and-helper2
    @ui.decorators.tableFieldOverride<MobileInternalStockChangeExtension, Stock>({

        // 🟢 Customize the title and helper text
        title: 'Stock lines',
        helperText: 'Stock lines for the internal stock change',

    })
    stock: ui.fields.Table<Stock>;

Customize the sort order field

standard TableField
Sorted by stockID
customized TableField
Sorted by location
  location values: CAP18, INT008, STO50
orderby-1 orderby-2
    @ui.decorators.tableFieldOverride<MobileInternalStockChangeExtension, Stock>({

        // 🟢  Customize orderBy with non-transient fields only
        orderBy: { location: { code: 1 } },

    })
    stock: ui.fields.Table<Stock>;

Customize the mobileCard display

standard TableField display   customized TableField display  
quantityInPackingUnit product product code quantityInPackingUnit
       
location lot product description product UPC code
sublot status location code  
identifier1 identifier2 lot sublot
    status identifier1
mobilecard-1   mobilecard-2  
    @ui.decorators.tableFieldO|verride<MobileInternalStockChangeExtension, Stock>({

        // 🟢 Add a customized mobileCard
        mobileCard: {
            title: ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, ProductSite>({
                bind: 'product',
                valueField: { product: { code: true } },
            }),

            titleRight: ui.nestedFields.numeric<MobileInternalStockChangeExtension>({
                bind: 'quantityInPackingUnit',
                title: 'Quantity Unit',
            }),

            line2: ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, ProductSite>({
                bind: 'product',
                valueField: { product: { localizedDescription1: true } },
            }),

            line2Right: ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, ProductSite>({
                bind: 'product',
                valueField: { product: { upc: true } },
            }),

            line3: ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, Location>({
                bind: 'location',
                valueField: 'code',
            }),
            line4: ui.nestedFields.text<MobileInternalStockChangeExtension>({
                bind: 'lot',
            }),
            line4Right: ui.nestedFields.text<MobileInternalStockChangeExtension>({
                bind: 'sublot',
            }),
            line3Right: ui.nestedFields.text<MobileInternalStockChangeExtension>({
                bind: 'serialNumber',
            }),
            line5: ui.nestedFields.reference<MobileInternalStockChangeExtension>({
                bind: 'status',
                valueField: { code: true },
            }),
            line5Right: ui.nestedFields.text<MobileInternalStockChangeExtension>({
                bind: 'identifier1',
            }),
        },

    })
    stock: ui.fields.Table<Stock>;

Customize the onRowClick event

standard TableField custom TableField
Values of the stock detail display: Values of the stock detail display:
- product
- licencePlateNumber
- location
- location
- Lot
- Sublot
- lotCustomField1
- lotCustomField2
- quantityInPackingUnit
- packingUnitToStockUnitConversionFactor
- quantityInStockUnit
- allocatedQuantity
- status
- identifer1
- identifier2
- stockId
- licensePlateNumber
- serialNumber
onRowClick-1 onRowClick-2
    @ui.decorators.tableFieldOverride<MobileInternalStockChangeExtension, Stock>({

        // 🟢 Add a custom onRowClick event to display new stock details
        onRowClick(recordId, rowItem) {
            this.stockInfo.value = [rowItem]; // populate details list
            this.gridBlock.selectedRecordId = recordId; // populate grid row block
            this.detailPanelSection.isHidden = false; // show details panel
            this.stockPanelSection.isHidden = true; // show stock panel
            this.$.showToast('onRowClick TableField overrides', { type: 'success' });
        },

    })
    stock: ui.fields.Table<Stock>;

    @ui.decorators.detailListField<MobileInternalStockChangeExtension, Stock>({
        parent() {
            return this.detailsBlock;
        },
        node: '@sage/x3-stock-data/Stock',
        isTransient: true,
        isFullWidth: true,
        isTitleHidden: false,
        title: 'Stock information',
        helperText: 'Stock information for the selected stock line',
        fields: [
            ui.nestedFields.text({
                bind: 'stockId',
                title: 'Stock ID',
                isReadOnly: true,
            }),
            ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, LicensePlateNumber>({
                bind: 'licensePlateNumber',
                title: 'License plate number',
                valueField: 'code',
                node: '@sage/x3-stock-data/LicensePlateNumber',
                isReadOnly: true,
                columns: [
                    ui.nestedFields.text({
                        bind: 'code',
                    }),
                    ui.nestedFields.text({
                        bind: '_id',
                        isHidden: true,
                    }),
                ],
            }),
            ui.nestedFields.text({
                bind: 'serialNumber',
                title: 'Serial no.',
                isReadOnly: true,
            }),
            ui.nestedFields.text({
                bind: 'owner',
                title: 'Owner',
                isReadOnly: true,
                isHidden: true,
            }),
        ],
    })
    stockInfo: ui.fields.DetailList<Stock>;

Hide a standard value

standard TableField customized TableField
  Product code value is hidden
hide-standard-value 1 hide-standard-value 2
    @ui.decorators.tableFieldOverride<MobileInternalStockChangeExtension, Stock>({

        // 🟢 Be able to hide product
        columnOverrides: [
            ui.nestedFieldOverrides.reference({
                bind: 'product',
                isHidden: true,
                valueField: { product: { code: true } },
                isMandatory: true,
            }),
        ],

    })
    stock: ui.fields.Table<Stock>;
Note: Some properties cannot be overridden with nestedFieldOverrides. They include:
- isTransient
- isTransientInput
- node
- columns

Add standard or custom fields to TableField

standard TableField customized TableField
  values added:
- location type
- location category
add-a-field 1 add-a-field 2
    @ui.decorators.tableFieldOverride<MobileInternalStockChangeExtension, Stock>({

        // 🟢 Add new fields to TableField: Location type and location category
        columns: [
            ui.nestedFieldExtensions.reference<MobileInternalStockChangeExtension, Stock, Location>({
                title: 'Location type',
                bind: 'location',
                node: '@sage/x3-stock-data/Location',
                valueField: { type: true },
                isReadOnly: true,
                insertBefore: 'quantityInPackingUnit',
            }),
            ui.nestedFieldExtensions.reference<MobileInternalStockChangeExtension, Stock, Location>({
                title: 'Location category',
                bind: 'location',
                valueField: { category: true },
                insertBefore: 'quantityInPackingUnit',
                insertAfter: 'locationType',
            }),
        ],

    })
    stock: ui.fields.Table<Stock>;

Note:

  • The TableField of the standard page is populated by a query that does not include all the fields from the Stock table.
  • To add columns to the TableField that are not present in the query, a method must be added to the page extension to query the Stock table and retrieve the data for the new columns.

The following code example shows how to add columns to the TableField that are not present in the query:

import { ProductSite } from '@sage/x3-master-data-api';
import { GraphApi } from '@sage/x3-stock-api';
import { LicensePlateNumber, Location, Stock } from '@sage/x3-stock-data-api';
import { Dict } from '@sage/xtrem-shared';
import * as ui from '@sage/xtrem-ui';
import type { MobileInternalStockChange as MobileInternalStockChangePage } from '../pages/mobile-internal-stock-change';

@ui.decorators.pageExtension<MobileInternalStockChangeExtension>({
    extends: '@sage/x3-stock/MobileInternalStockChange',
})
export class MobileInternalStockChangeExtension extends ui.PageExtension<MobileInternalStockChangePage, GraphApi> {

    // 🟢 Override the 'Next' button.
    @ui.decorators.pageActionOverride<MobileInternalStockChangeExtension>({
        async onClickAfter() {
            setTimeout(
                async () =>
                    await this._prepareStockWithCustomFields(),
                1000,
            );
        },
    })
    nextButton: ui.PageAction;

    @ui.decorators.tableFieldOverride<MobileInternalStockChangeExtension, Stock>({

        // 🟢 Customize the title and helper text
        title: 'Stock lines',
        helperText: 'Stock lines for the internal stock change',

        // 🟢  Customize orderBy with non-transient fields only
        orderBy: { location: { code: 1 } },

        // 🟢 Add a customized mobileCard
        mobileCard: {
            title: ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, ProductSite>({
                bind: 'product',
                valueField: { product: { code: true } },
            }),

            titleRight: ui.nestedFields.numeric<MobileInternalStockChangeExtension>({
                bind: 'quantityInPackingUnit',
                title: 'Quantity Unit',
            }),

            line2: ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, ProductSite>({
                bind: 'product',
                valueField: { product: { localizedDescription1: true } },
            }),

            line2Right: ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, ProductSite>({
                bind: 'product',
                valueField: { product: { upc: true } },
            }),

            line3: ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, Location>({
                bind: 'location',
                valueField: 'code',
            }),
            line4: ui.nestedFields.text<MobileInternalStockChangeExtension>({
                bind: 'lot',
            }),
            line4Right: ui.nestedFields.text<MobileInternalStockChangeExtension>({
                bind: 'sublot',
            }),
            line3Right: ui.nestedFields.text<MobileInternalStockChangeExtension>({
                bind: 'serialNumber',
            }),
            line5: ui.nestedFields.reference<MobileInternalStockChangeExtension>({
                bind: 'status',
                valueField: { code: true },
            }),
            line5Right: ui.nestedFields.text<MobileInternalStockChangeExtension>({
                bind: 'identifier1',
            }),
        },

        // 🟢 Customize event for TableField event
        onRowSelected(recordId, rowItem) {
            this.$.showToast('onRowSelected TableField overrides', { type: 'success' });
        },
        onRowSelectedAfter(recordId, rowItem) {
            this.$.showToast('onRowSelected TableField overrides', { type: 'success' });
        },
        onRowUnselected(recordId, rowItem) {
            this.$.showToast('onRowSelected TableField overrides', { type: 'success' });
        },
        onRowUnselectedAfter(recordId, rowItem) {
            this.$.showToast('onRowSelected TableField overrides', { type: 'success' });
        },

        // 🟢 Add a custom onRowClick event to display new stock details
        onRowClick(recordId, rowItem) {
            this.stockInfo.value = [rowItem]; // populate details list
            this.gridBlock.selectedRecordId = recordId; // populate grid row block
            this.detailPanelSection.isHidden = false; // show details panel
            this.stockPanelSection.isHidden = true; // show stock panel
            this.$.showToast('onRowClick TableField overrides', { type: 'success' });
        },

        // 🟢 Add a customized dropdownActions
        dropdownActions: [
            {
                icon: 'bin',
                title: 'Delete',
                isDestructive: true,
                async onClick(recordId) {
                    const row = this.stock.getRecordValue(recordId);
                    if (row) {
                        await this.stock.removeRecord(recordId);
                    }
                },
            },
        ],

        // 🟢 Make quantityInPackingUnit visible and hide product
        columnOverrides: [
            ui.nestedFieldOverrides.numeric({
                bind: 'quantityInPackingUnit',
                isHidden: false,
            }),
            ui.nestedFieldOverrides.reference({
                bind: 'product',
                isHidden: true,
                valueField: { product: { code: true } },
                isMandatory: true,
            }),
        ],

        // 🟢 Add new fields to TableField: Location type and location category
        columns: [
            ui.nestedFieldExtensions.reference<MobileInternalStockChangeExtension, Stock, Location>({
                title: 'Location type',
                bind: 'location',
                node: '@sage/x3-stock-data/Location',
                valueField: { type: true },
                isReadOnly: true,
                insertBefore: 'quantityInPackingUnit',
            }),
            ui.nestedFieldExtensions.reference<MobileInternalStockChangeExtension, Stock, Location>({
                title: 'Location category',
                bind: 'location',
                valueField: { category: true },
                insertBefore: 'quantityInPackingUnit',
                insertAfter: 'locationType',
            }),
        ],

    })
    stock: ui.fields.Table<Stock>;

    @ui.decorators.detailListField<MobileInternalStockChangeExtension, Stock>({
        parent() {
            return this.detailsBlock;
        },
        node: '@sage/x3-stock-data/Stock',
        isTransient: true,
        isFullWidth: true,
        isTitleHidden: false,
        title: 'Stock information',
        helperText: 'Stock information for the selected stock line',
        fields: [
            ui.nestedFields.text({
                bind: 'stockId',
                title: 'Stock ID',
                isReadOnly: true,
            }),
            ui.nestedFields.reference<MobileInternalStockChangeExtension, Stock, LicensePlateNumber>({
                bind: 'licensePlateNumber',
                title: 'License plate number',
                valueField: 'code',
                node: '@sage/x3-stock-data/LicensePlateNumber',
                isReadOnly: true,
                columns: [
                    ui.nestedFields.text({
                        bind: 'code',
                    }),
                    ui.nestedFields.text({
                        bind: '_id',
                        isHidden: true,
                    }),
                ],
            }),
            ui.nestedFields.text({
                bind: 'serialNumber',
                title: 'Serial no.',
                isReadOnly: true,
            }),
            ui.nestedFields.text({
                bind: 'owner',
                title: 'Owner',
                isReadOnly: true,
                isHidden: true,
            }),
        ],
    })
    stockInfo: ui.fields.DetailList<Stock>;

    public async _prepareStockWithCustomFields() {
        const stockIds = this.stock?.value?.map(stockLine => stockLine.stockId) ?? [];
        const response = await this.$.graph
            .node('@sage/x3-stock-data/Stock')
            .query(
                ui.queryUtils.edgesSelector(
                    {
                        _id: true,
                        product: {
                            product: {
                                code: true,
                                localizedDescription1: true,
                                upc: true,
                                _id: true,
                            },
                            _id: true,
                        },
                        location: {
                            type: true,
                            category: true,
                            _id: true,
                        },
                        stockId: true,
                    },
                    {
                        filter: {
                            stockId: { _in: stockIds },
                        },
                        first: 1000,
                    },
                ),
            )
            .execute();
        this.stock.value = this.stock.value.map((stockLine) => {
            const stockItem = response.edges?.find(edge => String(edge.node.stockId) === String(stockLine.stockId));
            if (!stockItem) {
                return stockLine;
            }
            return {
                ...stockLine,
                product: {
                    product:
                    {
                        ...stockLine.product?.product,
                        localizedDescription1: stockItem.node.product?.product?.localizedDescription1,
                        upc: stockItem.node.product?.product?.upc,
                    },
                },
                location: {
                    ...stockLine.location,
                    type: stockItem.node.location?.type,
                    category: stockItem.node.location?.category,
                },
            };
        });
    };

}