UiPath/angular-components

View on GitHub
projects/angular/components/ui-grid/src/body/ui-grid-column.directive.ts

Summary

Maintainability
B
4 hrs
Test Coverage
A
98%
import {
    BehaviorSubject,
    Subject,
} from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

import {
    ContentChild,
    Directive,
    inject,
    Input,
    isDevMode,
    OnChanges,
    OnDestroy,
    SimpleChange,
    SimpleChanges,
    TemplateRef,
} from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { identifier } from '@uipath/angular/utilities';

import { UiGridDropdownFilterDirective } from '../filters/ui-grid-dropdown-filter.directive';
import { UiGridSearchFilterDirective } from '../filters/ui-grid-search-filter.directive';
import {
    ResizeStrategy,
    UI_GRID_RESIZE_STRATEGY_STREAM,
} from '../managers';

/**
 * @ignore
 */
const ARIA_SORT_MAP: Record<SortDirection, string> = {
    // eslint-disable-next-line @typescript-eslint/naming-convention
    '': 'none',
    asc: 'ascending',
    desc: 'descending',
};

/**
 * @ignore
 */
const REACTIVE_INPUT_LIST: (keyof UiGridColumnDirective<Record<string, unknown>>)[]
    = ['sort', 'visible', 'title', 'primary'];

/**
 * The grid column definition directive.
 *
 * @export
 */
@Directive({
    selector: '[uiGridColumn], ui-grid-column',
})
export class UiGridColumnDirective<T> implements OnChanges, OnDestroy {
    /**
     * Set the column width, in `%`.
     *
     */
    @Input()
    set width(value: number | string) {
        this._validateUnits(value);

        const width = typeof value === 'string' ? Number(parseFloat(value).toFixed(1)) : value;
        if (isNaN(width)) { return; }
        if (this._resizeStrategyStream$.value === ResizeStrategy.ScrollableGrid) {
            // preserving compatibility with previous implementation where width was expressed in %
            this._width = typeof value === 'string' ? width * 10 : width;
            this.widthPx$.next(this._width);
            return;
        }

        this._width = width * 10;
    }

    /**
     * Returns the column width, in `%`.
     *
     */
    get width() {
        return this._width;
    }

    /**
     * Returns the `aria-sort` associated to the current sort.
     *
     */
    get ariaSort() {
        return ARIA_SORT_MAP[this.sort];
    }

    /**
     * The string identifier for the column.
     *
     * (used for resize identification)
     *
     */
    identifier = identifier();

    /**
     * Configure if the column is sortable.
     *
     */
    @Input()
    sortable = false;

    /**
     * Configure if the column should be included in the search.
     *
     */
    @Input()
    searchable = false;

    /**
     * Configure if the column is resizeable or not.
     *
     */
    @Input()
    resizeable = true;

    /**
     * The column title.
     *
     */
    @Input()
    title?: string;

    /**
     * The property that should be loaded in the associated row cell.
     *
     */
    @Input()
    property?: keyof T | string; // nested property

    /**
     * If defined, this will be used for sorting and filtering
     *
     */
    @Input()
    queryProperty?: keyof T | string; // nested property

    /**
     * The method metadata used for searches.
     *
     */
    @Input()
    method?: string;

    /**
     * The current sort of the column.
     *
     */
    @Input()
    sort: SortDirection = '';

    /**
     * If true and ui-grid has scrollable resize strategy, then the column will be placed in sticky mode
     *
     */
    @Input()
    set isSticky(value: boolean) {
        this._isSticky = value;
        this.disableToggle = this.disableToggle || value;
        if (value) {
            this.visible = true;
        }
    }
    get isSticky() {
        return this._isSticky;
    }

    /**
     * If the column should be styled as primary.
     *
     */
    @Input()
    get primary() {
        return this._primary;
    }
    set primary(primary: boolean) {
        if (primary === this._primary) { return; }
        this._primary = !!primary;

        this.change$.next({
            primary: new SimpleChange(!primary, primary, false),
        });
    }

    /**
     * If the column can have visibility toggled.
     *
     */
    @Input()
    set disableToggle(value: boolean) {
        this._disableToggle = value;
    }
    get disableToggle() {
        return this._disableToggle;
    }

    /**
     * If the column should be rendered, used as default state if toggle columns is turned on.
     *
     */
    @Input()
    get visible() {
        return this._visible;
    }
    set visible(visible: boolean) {
        if (visible === this._visible) { return; }
        this._visible = !!visible;

        this.change$.next({
            visible: new SimpleChange(!visible, visible, false),
        });
    }

    /**
     * The minimum width percentage that the column should have when resizing.
     *
     */
    @Input()
    minWidth = 30;

    /**
     * If the searchable dropdown associated to the column should trigger a data fetch when opened.
     *
     */
    @Input()
    refetch = false;

    /**
     * Determines the message which appears in the tooltip of an info icon inside the column header.
     *
     */
    @Input()
    description = '';

    /**
     * The searchable dropdown directive reference.
     *
     * @ignore
     */
    @ContentChild(UiGridSearchFilterDirective, {
        static: true,
    })
    searchableDropdown?: UiGridSearchFilterDirective<T>;

    /**
     * The dropdown directive reference.
     *
     * @ignore
     */
    @ContentChild(UiGridDropdownFilterDirective, {
        static: true,
    })
    dropdown?: UiGridDropdownFilterDirective<T>;

    /**
     * The view template associated to the row cell.
     *
     * @ignore
     */
    @ContentChild(TemplateRef, {
        static: true,
    })
    html?: TemplateRef<any>;

    /**
     * Emits when reactive properties change.
     *
     */
    change$ = new Subject<SimpleChanges>();

    /**
     * Source of truth for column's width expressed in pixels when grid has Scrollable resize strategy.
     *
     */
    widthPx$ = new BehaviorSubject(0);

    private _width = NaN;
    private _visible = true;
    private _primary = false;
    private _isSticky = false;
    private _disableToggle = false;

    private _resizeStrategyStream$ = inject(UI_GRID_RESIZE_STRATEGY_STREAM, { host: false });
    constructor() {
        this._resizeStrategyStream$.pipe(
            distinctUntilChanged(),
        ).subscribe(resizeStrategy => {
            this.width = this._width / 10;
            if (resizeStrategy !== ResizeStrategy.ScrollableGrid) {
                this.widthPx$.next(0);
            }
        });
    }
    /**
     * @ignore
     */
    ngOnChanges(changes: SimpleChanges) {
        const isAnyPropertyChanged = Object.keys(changes)
            .filter(property => REACTIVE_INPUT_LIST.includes(property as keyof UiGridColumnDirective<T>))
            .map(property => changes[property])
            .some(change => !change.firstChange &&
                change.currentValue !== change.previousValue,
            );

        if (!isAnyPropertyChanged) { return; }

        this.change$.next(changes);
    }

    /**
     * @ignore
     */
    ngOnDestroy() {
        this.change$.complete();
    }

    private _validateUnits(value: string | number) {
        if (
            isDevMode() &&
            typeof value === 'string' &&
            !value.endsWith('%')
        ) {
            console.error(`Width should be percentual for '${this.title}' column.`);
        }
    }
}