Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ module.exports = {
rules: {
'prettier/prettier': ['error'],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn'
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}
]
},
env: {
browser: true,
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/cps-shared-ui-checkers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ jobs:
- name: Lint app
run: npm run lint

typecheck:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm

- name: Install dependencies
run: npm ci

- name: Typecheck app
run: npm run typecheck

playwright:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test:coverage:cps-ui-kit": "ng test --project=cps-ui-kit --coverage --coverage-reporters text-summary --coverage-reporters lcov --json --output-file=./coverage/report.json",
"test:coverage:composition": "ng test --project=composition --coverage --coverage-reporters text-summary --coverage-reporters lcov --json --output-file=./coverage/report.json",
"lint": "eslint \"**/*.ts\"",
"typecheck": "tsc --noEmit",
"generate-json-api": "node ./api-generator/api-generator.js",
"test:a11y": "pa11y-ci --threshold 1000",
"test:a11y:summary": "pa11y-ci --threshold 1000 --reporter=json > .pa11y-temp.json 2>/dev/null && jq -C '{\"Total URLs tested\": .total, \"Passed\": \"\\(.passes)/\\(.total)\", \"Total errors found\": .errors, \"Standard\": \"WCAG 2.0 AA\", \"Test engine\": \"axe-core via pa11y-ci\", \"Top 10 components with errors\": (.results | to_entries | map({component: (.key | split(\"/\") | .[-2]), errors: .value | length}) | sort_by(-.errors) | .[0:10])}' .pa11y-temp.json && rm -f .pa11y-temp.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};

// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};
Expand Down Expand Up @@ -702,7 +702,7 @@
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

Check warning on line 705 in projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function

onBlur() {
this.isActive = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,11 @@
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};

Check warning on line 211 in projects/cps-ui-kit/src/lib/components/cps-button-toggle/cps-button-toggle.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

registerOnChange(fn: any) {
this.onChange = fn;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class CpsCheckboxComponent
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};

// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};
Expand Down Expand Up @@ -166,7 +166,7 @@ export class CpsCheckboxComponent
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

focus() {
this._elementRef?.nativeElement?.querySelector('input')?.focus();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,177 +43,177 @@
templateUrl: './cps-datepicker.component.html',
styleUrls: ['./cps-datepicker.component.scss']
})
export class CpsDatepickerComponent
implements ControlValueAccessor, OnInit, OnDestroy
{
/**
* Label of the datepicker element.
* @group Props
*/
@Input() label = '';

/**
* Determines whether datepicker is disabled.
* @group Props
*/
@Input() disabled = false;

/**
* Width of the datepicker of type number denoting pixels or string.
* @group Props
*/
@Input() width: number | string = '100%';

/**
* Placeholder text.
* @group Props
*/
@Input() placeholder = 'MM/DD/YYYY';

/**
* Bottom hint text for the input field.
* @group Props
*/
@Input() hint = '';

/**
* When enabled, a clear icon is displayed to clear the value.
* @group Props
*/
@Input() clearable = false;

/**
* Hides hint and validation errors.
* @group Props
*/
@Input() hideDetails = false;

/**
* Determines whether the component should have persistent clear icon.
* @group Props
*/
@Input() persistentClear = false;

/**
* Determines whether to show button to be able to select today's date.
* @group Props
*/
@Input() showTodayButton = true;

/**
* Determines whether the datepicker dropdown should open on input focus.
* @group Props
*/
@Input() openOnInputFocus = false;

/**
* When it is not an empty string, an info icon is displayed to show text for more info.
* @group Props
*/
@Input() infoTooltip = '';

/**
* InfoTooltip class for styling.
* @group Props
*/
@Input() infoTooltipClass = 'cps-tooltip-content';

/**
* Size of infoTooltip, of type number denoting pixels or string.
* @group Props
*/
@Input() infoTooltipMaxWidth: number | string = '100%';

/**
* Determines whether the infoTooltip is persistent.
* @group Props
*/
@Input() infoTooltipPersistent = false;

/**
* Position of infoTooltip, it can be "top", "bottom", "left" or "right".
* @group Props
*/
@Input() infoTooltipPosition: CpsTooltipPosition = 'top';

/**
* Styling appearance of datepicker input, it can be 'outlined', 'underlined' or 'borderless.
* @group Props
*/
@Input() appearance: CpsDatepickerAppearanceType = 'outlined';

/**
* Minimal date availalbe for selection.
* @group Props
*/
@Input()
minDate!: Date;

/**
* Maximal date availalbe for selection.
* @group Props
*/
@Input()
maxDate!: Date;

/**
* Value of the datepicker.
* @default null
* @group Props
*/
@Input() set value(value: Date | null) {
this._value = value;
this.stringDate = this._dateToString(value);
this.onChange(value);
}

get value(): Date | null {
return this._value;
}

/**
* Callback to invoke on value change.
* @param {Date | null} value - value change.
* @group Emits
*/
@Output() valueChanged = new EventEmitter<Date | null>();

@ViewChild('datepickerInput')
datepickerInput!: CpsInputComponent;

@ViewChild('calendarMenu')
calendarMenu!: CpsMenuComponent;

stringDate = '';
isOpened = false;
error = '';
cvtWidth = '';

private _statusChangesSubscription?: Subscription;
private _value: Date | null = null;

constructor(@Self() @Optional() private _control: NgControl) {
if (this._control) {
this._control.valueAccessor = this;
}
}

ngOnInit(): void {
this.cvtWidth = convertSize(this.width);

this._statusChangesSubscription = this._control?.statusChanges?.subscribe(
() => {
this._checkErrors();
}
);
}

ngOnDestroy() {
this._statusChangesSubscription?.unsubscribe();
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};

Check warning on line 216 in projects/cps-ui-kit/src/lib/components/cps-datepicker/cps-datepicker.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 216 in projects/cps-ui-kit/src/lib/components/cps-datepicker/cps-datepicker.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function

// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ export class CpsInputComponent
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};

Expand Down Expand Up @@ -383,7 +383,7 @@ export class CpsInputComponent
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

onClickPrefixIcon() {
if (!this.prefixIconClickable || this.readonly || this.disabled) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export class CpsRadioGroupComponent
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};

Expand Down Expand Up @@ -225,7 +225,7 @@ export class CpsRadioGroupComponent
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

onBlur() {
this._checkErrors();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ export class CpsSelectComponent
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};

Expand Down Expand Up @@ -648,7 +648,7 @@ export class CpsSelectComponent
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

onBlur() {
this._checkErrors();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,15 @@ describe('CpsSidebarMenuComponent', () => {
it('should not toggle when target element has disabled class', () => {
const el = document.createElement('button');
el.classList.add('disabled');
const event = { currentTarget: el } as MouseEvent;
const event = { currentTarget: el } as unknown as MouseEvent;
component.toggleMenu(event, mockMenu as CpsMenuComponent, item);
expect(mockMenu.show).not.toHaveBeenCalled();
expect(mockMenu.hide).not.toHaveBeenCalled();
});

it('should hide menu when it is visible', () => {
const el = document.createElement('button');
const event = { currentTarget: el } as MouseEvent;
const event = { currentTarget: el } as unknown as MouseEvent;
(mockMenu.isVisible as jest.Mock).mockReturnValue(true);
component.toggleMenu(event, mockMenu as CpsMenuComponent, item);
expect(mockMenu.hide).toHaveBeenCalled();
Expand All @@ -248,7 +248,7 @@ describe('CpsSidebarMenuComponent', () => {

it('should hide all other menus and show this one when not visible', () => {
const el = document.createElement('button');
const event = { currentTarget: el } as MouseEvent;
const event = { currentTarget: el } as unknown as MouseEvent;
(mockMenu.isVisible as jest.Mock).mockReturnValue(false);
component.toggleMenu(event, mockMenu as CpsMenuComponent, item);
expect((component.allMenus as any).forEach).toHaveBeenCalled();
Expand All @@ -257,7 +257,7 @@ describe('CpsSidebarMenuComponent', () => {

it('should always set focusedItemWithMenu to the given item', () => {
const el = document.createElement('button');
const event = { currentTarget: el } as MouseEvent;
const event = { currentTarget: el } as unknown as MouseEvent;
component.toggleMenu(event, mockMenu as CpsMenuComponent, item);
expect(component.focusedItemWithMenu).toBe(item);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class CpsSwitchComponent implements ControlValueAccessor {
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};

// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};
Expand Down Expand Up @@ -128,7 +128,7 @@ export class CpsSwitchComponent implements ControlValueAccessor {
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

focus() {
this._elementRef?.nativeElement?.querySelector('input')?.focus();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class CpsTagComponent implements ControlValueAccessor, OnChanges {
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};

// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};

// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};
Expand Down Expand Up @@ -284,7 +284,7 @@
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

Check warning on line 287 in projects/cps-ui-kit/src/lib/components/cps-textarea/cps-textarea.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function

onBlur() {
this._checkErrors();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};

Expand All @@ -237,7 +237,7 @@
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

Check warning on line 240 in projects/cps-ui-kit/src/lib/components/cps-timepicker/cps-timepicker.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function

writeValue(value: CpsTime | undefined) {
this.value = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,256 +53,256 @@
templateUrl: './cps-tree-autocomplete.component.html',
styleUrls: ['./cps-tree-autocomplete.component.scss']
})
export class CpsTreeAutocompleteComponent
extends CpsBaseTreeDropdownComponent
implements OnInit, AfterViewInit, OnDestroy
{
/**
* Message if array of items is empty.
* @group Props
*/
@Input() emptyMessage = 'No results found';

/**
* Styling appearance of tree autocomplete, it can be 'outlined', 'underlined' or 'borderless'.
* @group Props
*/
@Input() appearance: CpsTreeAutocompleteAppearanceType = 'outlined';

/**
* Placeholder text.
* @group Props
*/
@Input() placeholder = 'Please enter';

@ViewChild('treeAutocompleteInput')
treeAutocompleteInput!: ElementRef;

inputText = '';
backspaceClickedOnce = false;
activeSingle = false;

constructor(
@Optional() public override control: NgControl,
@Inject(DOCUMENT) private document: Document,
public override cdRef: ChangeDetectorRef
) {
super(control, cdRef);
}

override ngOnInit() {
super.ngOnInit();
}

override ngAfterViewInit() {
this.isAutocomplete = true;
super.ngAfterViewInit();
}

override ngOnDestroy() {
super.ngOnDestroy();
}

override onSelectNode() {
this.backspaceClickedOnce = false;
this._clearInput();
super.onSelectNode();
}

override onBlur() {
if (!this.isOpened) {
this._closeAndClear();
}
super.onBlur();
}

onBeforeOptionsHidden(reason: CpsMenuHideReason) {
if ([CpsMenuHideReason.SCROLL, CpsMenuHideReason.RESIZE].includes(reason)) {
this.toggleOptions(false);
return;
}
this._closeAndClear();
}

onBoxClick() {
if (!this.multiple) {
this.activeSingle = true;
if (!this.inputText) this.inputText = this._getValueLabel();
if (!this.isOpened) this.treeList.resetFilter();
}
this.focus();
this.optionFocused = false;
}

onContainerKeyDown(event: any) {
const code = event.keyCode;
// escape
if (code === 27) {
this._closeAndClear();
}
// click down arrow
else if (code === 40) {
this.initArrowsNavigaton();
}
}

onInputKeyDown(event: any) {
const code = event.keyCode;
// backspace
if (code === 8) {
this._removeLastValue();
event.stopPropagation();
}
// enter
else if (code === 13) {
if (!this.optionFocused) {
this._confirmInput(event?.target?.value || '');
event.stopPropagation();
}
}
}

onChevronClick(event: any) {
event.stopPropagation();

if (this.isOpened) {
this._closeAndClear();
} else {
this.onBoxClick();
}
}

isActive() {
return (
this.isOpened ||
this.document.activeElement === this.treeAutocompleteInput?.nativeElement
);
}

override remove(option: TreeNode): void {
super.remove(option);

this._clearInput();
setTimeout(() => {
this.focusInput();
}, 0);
}

override clear(event?: any): void {
super.clear(event);

this._clearInput();
setTimeout(() => {
this.focusInput();
}, 0);
}

focusInput() {
this.componentContainer?.nativeElement?.querySelector('input')?.focus();
}

override focus() {
super.focus();
this.focusInput();
}

onFilterOptions() {
this.recalcVirtualListHeight();
}

filterOptions(event: any) {
if (!this.isOpened) {
this.toggleOptions(true);
}
this.backspaceClickedOnce = false;
const searchVal = (event?.target?.value || '').toLowerCase();

if (!searchVal) this.treeList.resetFilter();
else this.treeList._filter(searchVal);
}

private _select(option: TreeNode): void {
function includes(array: any[], val: any): boolean {
return array?.some((item) => isEqual(item, val)) || false;
}

this.backspaceClickedOnce = false;

if (this.multiple) {
if (includes(this.treeSelection, option)) {
this.treeSelection = this.treeSelection.filter(
(v: TreeNode) => !isEqual(v, option)
);
} else {
this.treeSelection.push(option);
}
} else {
this.treeSelection = option;
}
this.updateValue(this.treeSelectionToValue(this.treeSelection));

this._clearInput();
setTimeout(() => {
this.focusInput();
}, 0);
}

private _getValueLabel() {
return this.treeSelection?.label || '';
}

private _clearInput() {
this.treeList.resetFilter();
this.inputText = '';
this.activeSingle = false;
this.updateOptions();
setTimeout(() => {
this.recalcVirtualListHeight();
});
}

private _closeAndClear() {
this._clearInput();
this.toggleOptions(false);
}

private _confirmInput(searchVal: string) {
if (!this.isOpened) return;
searchVal = searchVal.toLowerCase();
if (!searchVal) {
if (this.multiple) return;
this.treeSelection = undefined;
this.updateValue(undefined);
this.cdRef.detectChanges();
this._closeAndClear();
return;
}

// https://github.com/primefaces/primeng/blob/v16.4.3/src/app/components/tree/tree.ts#L1125
const found: any = this.treeList?.serializedValue?.find(
(sv: any) => sv?.node?.label?.toLowerCase() === searchVal
);
if (found) {
this._select(found.node);
this.toggleOptions(this.multiple);
} else {
if (!this.multiple) {
this.inputText = this._getValueLabel();
this.treeList.resetFilter();
return;
}
}

this._clearInput();
}

private _removeLastValue() {
if (!this.multiple || this.inputText) return;

if (this.treeSelection?.length) {
if (this.backspaceClickedOnce) {
this.treeSelection = this.treeSelection.filter(
(v: TreeNode, index: number) =>
(_v: TreeNode, index: number) =>

Check warning on line 305 in projects/cps-ui-kit/src/lib/components/cps-tree-autocomplete/cps-tree-autocomplete.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
index !== this.treeSelection.length - 1
);

Check warning on line 307 in projects/cps-ui-kit/src/lib/components/cps-tree-autocomplete/cps-tree-autocomplete.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
this.updateValue(this.treeSelectionToValue(this.treeSelection));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,309 +32,309 @@
template: '',
standalone: false
})
export class CpsBaseTreeDropdownComponent
implements ControlValueAccessor, OnInit, OnChanges, AfterViewInit, OnDestroy
{
/**
* Label of the component.
* @group Props
*/
@Input() label = '';

/**
* Bottom hint text.
* @group Props
*/
@Input() hint = '';

/**
* Specifies if multiple values can be selected.
* @group Props
*/
@Input() multiple = false;

/**
* Determines whether the component is disabled.
* @group Props
*/
@Input() disabled = false;

/**
* Width of the component, of type number denoting pixels or string.
* @group Props
*/
@Input() width: number | string = '100%';

/**
* When selecting an element, it will appear in a form of a chip.
* @group Props
*/
@Input() chips = true;

/**
* Determines whether the chips can be directly removed.
* @group Props
*/
@Input() closableChips = true;

/**
* When enabled, a clear icon is displayed to clear the value.
* @group Props
*/
@Input() clearable = false;

/**
* Determines whether the dropdown should open on clear.
* @group Props
*/
@Input() openOnClear = true;

/**
* Name of the label field of an option.
* @group Props
*/
@Input() optionLabel = 'label';

/**
* Name of the info field of an option, shows the additional information text.
* @group Props
*/
@Input() optionInfo = 'info';

/**
* Options for hiding details.
* @group Props
*/
@Input() hideDetails = false;

/**
* Determines whether the component should have persistent clear icon.
* @group Props
*/
@Input() persistentClear = false;

/**
* Icon before input value.
* @group Props
*/
@Input() prefixIcon: IconType = '';

/**
* Size of icon before input value, of type number, string, 'fill', 'xsmall', 'small', 'normal' or 'large'.
* @group Props
*/
@Input() prefixIconSize: iconSizeType = '18px';

/**
* When enabled, a loading bar is displayed.
* @group Props
*/
@Input() loading = false;

/**
* Determines whether only the elements within scrollable area should be added into the DOM.
* @group Props
*/
@Input() virtualScroll = false;

/**
* Determines how many additional elements to add to the DOM outside of the view.
* @group Props
*/
@Input() numToleratedItems = 10;

/**
* When it is not an empty string, an info icon is displayed to show text for more info.
* @group Props
*/
@Input() infoTooltip = '';

/**
* Info tooltip class for styling.
* @group Props
*/
@Input() infoTooltipClass = 'cps-tooltip-content';

/**
* Max width of infoTooltip of type number denoting pixels or string.
* @group Props
*/
@Input() infoTooltipMaxWidth: number | string = '100%';

/**
* Determines whether the infoTooltip is persistent.
* @group Props
*/
@Input() infoTooltipPersistent = false;

/**
* Position of infoTooltip, it can be 'top', 'bottom', 'left' or 'right'.
* @group Props
*/
@Input() infoTooltipPosition: CpsTooltipPosition = 'top';

/**
* When set, it expands all directiories initially.
* @group Props
*/
@Input() initialExpandDirectories = false;

/**
* When set, it expands all options initially.
* @group Props
*/
@Input() initialExpandAll = false;

/**
* Determines whether the chevron icon should be displayed.
* @group Props
*/
@Input() showChevron = true;

/**
* An array of objects to display as the available options.
* @group Props
*/
@Input() options: any[] = [];

/**
* Value of the component.
* @group Props
*/
@Input('value') _value: any = undefined;

set value(value: any) {
this._value = value;
this.onChange(value);
}

get value(): any {
return this._value;
}

/**
* Callback to invoke on value change.
* @param {any} any - value changed.
* @group Emits
*/
@Output() valueChanged = new EventEmitter<any>();

/**
* Callback to invoke when the component receives focus.
* @param {any}
* @group Emits
*/
@Output() focused = new EventEmitter();

/**
* Callback to invoke when the component loses focus.
* @param {any}
* @group Emits
*/
@Output() blurred = new EventEmitter();

@ViewChild('componentContainer')
componentContainer!: ElementRef;

@ViewChild('optionsMenu')
optionsMenu!: CpsMenuComponent;

@ViewChild('treeList') treeList!: Tree;

@ViewChild('boxEl')
boxEl!: ElementRef;

private _statusChangesSubscription?: Subscription;

innerOptions: TreeNode[] = [];
optionsMap = new Map<string, TreeNode>();
originalOptionsMap = new Map<string, any>();

virtualListHeight = 240;
virtualScrollItemSize = 40;

error = '';
cvtWidth = '';
isOpened = false;
optionFocused = false;
isAutocomplete = false;

treeContainerElement!: HTMLElement;
treeSelection: any;

boxWidth = 0;
resizeObserver: ResizeObserver;

constructor(
@Self() @Optional() public control: NgControl,
public cdRef: ChangeDetectorRef
) {
if (this.control) {
this.control.valueAccessor = this;
}
this.resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
if (entry?.target) this.boxWidth = (entry.target as any).offsetWidth;
});
});
}

ngOnInit() {
this.cvtWidth = convertSize(this.width);
if (!this._value) {
if (this.multiple) {
this._value = [];
this.treeSelection = [];
}
} else {
this.treeSelection = this._valueToTreeSelection(this.value);
}

this._statusChangesSubscription = this.control?.statusChanges?.subscribe(
() => {
this._checkErrors();
}
);
}

ngOnChanges(changes: SimpleChanges) {
if (changes.options) {
this.innerOptions = this._toInnerOptions(this.options);
}
}

ngAfterViewInit() {
this._initContainerClickListener();
this.recalcVirtualListHeight();
this.resizeObserver.observe(this.boxEl.nativeElement);
this.cdRef.detectChanges();
}

ngOnDestroy() {
this._statusChangesSubscription?.unsubscribe();

this.treeContainerElement?.removeEventListener(
'click',
this._handleOnContainerClick.bind(this)
);

this.resizeObserver?.disconnect();
}

expandAll() {
this.optionsMap?.forEach((value) => {
if (value.children) value.expanded = true;
});
}

collapseAll() {
this.optionsMap?.forEach((value) => {
if (value.children) value.expanded = false;
});
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = (event: any) => {};
onChange = (_event: any) => {};

Check warning on line 337 in projects/cps-ui-kit/src/lib/components/internal/cps-base-tree-dropdown/cps-base-tree-dropdown.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 337 in projects/cps-ui-kit/src/lib/components/internal/cps-base-tree-dropdown/cps-base-tree-dropdown.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function

// eslint-disable-next-line @typescript-eslint/no-empty-function
onTouched = () => {};
Expand All @@ -355,7 +355,7 @@
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
setDisabledState(disabled: boolean) {}
setDisabledState(_disabled: boolean) {}

Check warning on line 358 in projects/cps-ui-kit/src/lib/components/internal/cps-base-tree-dropdown/cps-base-tree-dropdown.component.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function

onBlur() {
this._checkErrors();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,15 @@ describe('CpsCronValidationService', () => {
]
});
const tokenService = TestBed.inject(CPS_CRON_VALIDATION_SERVICE);
expect(tokenService.isValidCron('')).toBe(true);
expect(tokenService?.isValidCron('')).toBe(true);
});

it('should delegate isValidCron to the underlying service', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({});
const tokenService = TestBed.inject(CPS_CRON_VALIDATION_SERVICE);
expect(tokenService.isValidCron('0 12 * * ? *')).toBe(true);
expect(tokenService.isValidCron('invalid')).toBe(false);
expect(tokenService?.isValidCron('0 12 * * ? *')).toBe(true);
expect(tokenService?.isValidCron('invalid')).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('CpsDialogService', () => {
function setupAppendSpy() {
jest
.spyOn(service as any, 'appendDialogComponentToBody')
.mockImplementation((config: CpsDialogConfig) => {
.mockImplementation(() => {
const dialogRef = new CpsDialogRef();
const mockRef = makeMockComponentRef();
lastCreatedMockRef = mockRef;
Expand Down
Loading
Loading