UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

849 lines (842 loc) 56.5 kB
import * as i0 from '@angular/core'; import { inject, Injectable, InjectionToken, EventEmitter, Output, Input, Component, ViewChild, NgModule } from '@angular/core'; import { QueriesUtil, SmartGroupsService } from '@c8y/client'; import * as i2 from '@c8y/ngx-components'; import { FeatureCacheService, ContextRouteService, WILDCARD_SEARCH_FEATURE_KEY, SearchFilters, ViewContext, TabsOutletComponent, TabComponent, C8yTranslatePipe, RouterService, SearchInputComponent, getBasicInputArrayFormFieldConfig, BuiltInActionType, FilteringActionType, DataGridComponent, EmptyStateComponent, UserPreferencesConfigurationStrategy, DATA_GRID_CONFIGURATION_STRATEGY, DATA_GRID_CONFIGURATION_CONTEXT_PROVIDER, Status, TitleComponent, C8yTranslateDirective, CoreModule, CoreSearchModule, hookRoute, hookSearch } from '@c8y/ngx-components'; import { AssetNodeService } from '@c8y/ngx-components/assets-navigator'; import { DeviceGridService, NameDeviceGridColumn, ModelDeviceGridColumn, SerialNumberDeviceGridColumn, RegistrationDateDeviceGridColumn, SystemIdDeviceGridColumn, ImeiDeviceGridColumn, AlarmsDeviceGridColumn, DeviceGridModule } from '@c8y/ngx-components/device-grid'; import { BehaviorSubject, firstValueFrom, catchError, of, lastValueFrom, Subject } from 'rxjs'; import { AssetTypeGridColumn, AssetTypeCellRendererComponent } from '@c8y/ngx-components/data-grid-columns/asset-type'; import { AsyncPipe } from '@angular/common'; import * as i1$1 from '@angular/router'; import { Router, ActivationEnd } from '@angular/router'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import * as i1 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import { gettext } from '@c8y/ngx-components/gettext'; import { filter, map, startWith, catchError as catchError$1, takeUntil } from 'rxjs/operators'; import { SubAssetsService, DeleteAssetsModalComponent } from '@c8y/ngx-components/sub-assets'; import { BsModalService } from 'ngx-bootstrap/modal'; class AssetSearchService { constructor() { this.GRID_CONFIG_STORAGE_KEY = 'search-grid-config'; this.DEFAULT_PAGE_SIZE = 50; this.getGlobalSearchData = this.getSearchData.bind(this); this.appliedFilters$ = new BehaviorSubject({ allFilters: true, onlyDevices: true, onlyGroupsAndAssets: true, currentHierarchy: false }); this.featureCacheService = inject(FeatureCacheService); this.deviceGridService = inject(DeviceGridService); this.assetNodeService = inject(AssetNodeService); this.contextRouteService = inject(ContextRouteService); this.queriesUtil = new QueriesUtil(); } /** * Resets the status of applied filters, used during the search. * Applies only to filters: 'All', 'Show only devices', 'Show only groups and assets'. */ resetAppliedFilters() { this.appliedFilters$.next({ allFilters: true, onlyDevices: true, onlyGroupsAndAssets: true, currentHierarchy: false }); } buildCombinedRootQueryFilter(columns, pagination) { const rootQuery = { __filter: { __and: { __not: { __has: `c8y_IsBinary` } } } }; const { onlyDevices, onlyGroupsAndAssets } = this.appliedFilters$.value; const searchQuery = this.buildSearchQuery({ onlyDevices, onlyGroupsAndAssets }); const userQuery = this.deviceGridService.getQueryObj(columns, pagination); const queryPart = this.queriesUtil.addOrderbys(rootQuery, userQuery.__orderby, 'append'); const fullQuery = this.queriesUtil.addAndFilter(queryPart, userQuery.__filter); const queryWithSearch = this.queriesUtil.addAndFilter(fullQuery, searchQuery); return queryWithSearch; } async getData(columns, pagination, text) { const query = this.buildCombinedRootQueryFilter(columns, pagination); if (await this.isWildcardSearchEnabled()) { const wildcardQuery = this.queriesUtil.addAndFilter(query, { __or: { name: `*${text.trim().replace(/\s+/g, '*')}*`, id: text.trim(), 'c8y_Hardware.serialNumber': `*${text.trim().replace(/\s+/g, '*')}*` } }); return this.assetNodeService.getAllInventories({ ...pagination, query: this.queriesUtil.buildQuery(wildcardQuery) }); } return this.assetNodeService.getAllInventories({ ...pagination, query: this.queriesUtil.buildQuery(query), text }); } getDefaultColumns() { const defaultColumns = [ new NameDeviceGridColumn({ sortOrder: 'asc' }), new ModelDeviceGridColumn(), new SerialNumberDeviceGridColumn({ visible: false }), new RegistrationDateDeviceGridColumn({ visible: false }), new SystemIdDeviceGridColumn({ visible: false }), new ImeiDeviceGridColumn({ visible: false }), new AlarmsDeviceGridColumn() ]; return defaultColumns; } getDefaultActionControls() { return []; } getDefaultBulkActionControls() { return []; } getDefaultPagination() { return { pageSize: 25, currentPage: 1 }; } async isWildcardSearchEnabled() { return firstValueFrom(this.featureCacheService .getFeatureState(WILDCARD_SEARCH_FEATURE_KEY) .pipe(catchError(() => of(true)))); } buildSearchQuery(model) { const filter = {}; const ors = []; if (model.types?.length) { ors.push({ type: { __in: model.types } }); } if (model.onlyDevices) { ors.push({ __has: 'c8y_IsDevice' }); } if (model.onlyGroupsAndAssets) { ors.push({ __has: 'c8y_IsDynamicGroup' }); ors.push({ __has: 'c8y_IsDeviceGroup' }); } if (!model.onlyDevices && !model.onlyGroupsAndAssets) { ors.push([ { __has: 'c8y_IsDeviceGroup' }, { __has: 'c8y_IsDevice' }, { __has: 'c8y_IsAsset' } ]); } if (ors.length) { filter.__or = ors; } if (model.currentHierarchy && this.contextRouteService.activatedContextData?.contextData?.id) { filter.__and = [ { __isinhierarchyof: this.contextRouteService.activatedContextData.contextData.id } ]; } return filter; } /** * Get search data based on the provided text and pagination. * @param text The search text. * @param pagination The pagination options. * @returns The search results. */ async getSearchData(text, pagination = { currentPage: 1, pageSize: this.DEFAULT_PAGE_SIZE }) { const { onlyDevices, onlyGroupsAndAssets, currentHierarchy } = this.appliedFilters$.value; const query = this.buildSearchQuery({ onlyDevices, onlyGroupsAndAssets, currentHierarchy }); const queryString = this.queriesUtil.buildQuery(query); if (await this.isWildcardSearchEnabled()) { const wildcardQuery = this.queriesUtil.addAndFilter(query, { __or: { name: `*${text.trim().replace(/\s+/g, '*')}*`, id: text.trim(), 'c8y_Hardware.serialNumber': `*${text.trim().replace(/\s+/g, '*')}*` } }); return this.assetNodeService.getAllInventories({ ...pagination, query: this.queriesUtil.buildQuery(wildcardQuery) }); } return this.assetNodeService.getAllInventories({ ...pagination, query: queryString, text }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetSearchService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetSearchService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AssetSearchService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); const SEARCH_CONFIG = new InjectionToken('SearchConfig'); class SearchCustomFiltersComponent { constructor() { this.useTabs = false; this.customDataQuery = new EventEmitter(); this.refresh = new EventEmitter(); this.tabNames = { all: gettext('All'), devices: gettext('Devices'), groupsAndAssets: gettext('Groups and assets') }; this.selectedTab = this.tabNames.all; this.checkboxesState = [ { label: gettext('All'), name: SearchFilters.ALL_FILTERS, value: true, indeterminate: false, isDisabled: true }, { label: gettext('Show only devices'), name: SearchFilters.ONLY_DEVICES, value: true }, { label: gettext('Show only groups and assets'), name: SearchFilters.ONLY_GROUPS_AND_ASSETS, value: true } ]; this.isOnlyHierarchyQuery = false; this.isOnlyHierarchyQueryChange = new EventEmitter(); this.assetSearchService = inject(AssetSearchService); this.contextRouteService = inject(ContextRouteService); this.router = inject(Router); this.contextRouteData$ = this.router.events.pipe(filter(event => event instanceof ActivationEnd), map((routeData) => { const data = this.contextRouteService.getContextData(routeData.snapshot); if (data?.context === ViewContext.Group) { return data; } return null; }), takeUntilDestroyed()); this.context$ = this.contextRouteData$.pipe(startWith(this.contextRouteService.activatedContextData), takeUntilDestroyed()); } ngOnInit() { this.customDataQuery.next(this.assetSearchService.getGlobalSearchData); } onCheckboxChange(event, checkbox) { const { checked } = event.target; if (checked == undefined) { return; } switch (checkbox.name) { case SearchFilters.ALL_FILTERS: this.onSelectAll(checkbox, checked); break; case SearchFilters.ONLY_DEVICES: this.onAllDevices(checkbox, checked); break; case SearchFilters.ONLY_GROUPS_AND_ASSETS: this.onGroupsAndAssets(checkbox, checked); break; } // Handle allFilters checkbox when ONLY_GROUPS_AND_ASSETS and ONLY_DEVICES are selected if (this.getCheckbox(SearchFilters.ONLY_DEVICES).value && this.getCheckbox(SearchFilters.ONLY_GROUPS_AND_ASSETS).value) { Object.assign(this.getCheckbox(SearchFilters.ALL_FILTERS), { indeterminate: false, isDisabled: true, value: true }); } this.onSearchFilterChange(this.getCheckbox(SearchFilters.ALL_FILTERS).value, this.getCheckbox(SearchFilters.ONLY_DEVICES).value, this.getCheckbox(SearchFilters.ONLY_GROUPS_AND_ASSETS).value); this.refresh.next(null); } onTabChange(tabName = this.tabNames.all) { this.selectedTab = tabName; this.onSearchFilterChange(tabName === this.tabNames.all, tabName === this.tabNames.devices, tabName === this.tabNames.groupsAndAssets, this.isOnlyHierarchyQuery); } onHierarchyQueryChange(checked) { this.isOnlyHierarchyQuery = checked; this.isOnlyHierarchyQueryChange.next(checked); this.onSearchFilterChange(this.selectedTab === this.tabNames.all, this.selectedTab === this.tabNames.devices, this.selectedTab === this.tabNames.groupsAndAssets, checked); } onSearchFilterChange(all, devices, groupsAndAssets, currentHierarchy = false) { this.assetSearchService.appliedFilters$.next({ [SearchFilters.ALL_FILTERS]: all, [SearchFilters.ONLY_DEVICES]: devices, [SearchFilters.ONLY_GROUPS_AND_ASSETS]: groupsAndAssets, [SearchFilters.CURRENT_HIERARCHY]: currentHierarchy }); this.refresh.next(null); } saveCheckboxValue(checkbox, value) { checkbox.value = value; } onSelectAll(currentCheckbox, checked) { // Block unchecked state if (checked) { this.saveCheckboxValue(currentCheckbox, checked); } this.getCheckbox(SearchFilters.ALL_FILTERS).isDisabled = true; this.getCheckbox(SearchFilters.ONLY_DEVICES).value = true; this.getCheckbox(SearchFilters.ONLY_GROUPS_AND_ASSETS).value = true; } onAllDevices(currentCheckbox, checked) { this.saveCheckboxValue(currentCheckbox, checked); Object.assign(this.getCheckbox(SearchFilters.ALL_FILTERS), { indeterminate: true, isDisabled: false, value: null }); this.getCheckbox(SearchFilters.ONLY_GROUPS_AND_ASSETS).value = true; } onGroupsAndAssets(currentCheckbox, checked) { this.saveCheckboxValue(currentCheckbox, checked); Object.assign(this.getCheckbox(SearchFilters.ALL_FILTERS), { indeterminate: true, isDisabled: false, value: null }); this.getCheckbox(SearchFilters.ONLY_DEVICES).value = true; } getCheckbox(checkboxName) { return this.checkboxesState.find(checkbox => checkbox.name === checkboxName); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchCustomFiltersComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: SearchCustomFiltersComponent, isStandalone: true, selector: "c8y-search-custom-filters", inputs: { useTabs: "useTabs", isOnlyHierarchyQuery: "isOnlyHierarchyQuery" }, outputs: { customDataQuery: "customDataQuery", refresh: "refresh", isOnlyHierarchyQueryChange: "isOnlyHierarchyQueryChange" }, ngImport: i0, template: "<div class=\"d-flex gap-16 p-l-4 p-l-24 p-r-24\">\n @if (!useTabs) {\n <div class=\"p-b-8 separator-bottom sticky-top p-t-4\">\n @for (checkbox of checkboxesState; track $index) {\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n [checked]=\"checkbox.value\"\n [indeterminate]=\"checkbox.indeterminate\"\n (click)=\"onCheckboxChange($event, checkbox)\"\n [attr.disabled]=\"checkbox.isDisabled ? true : null\"\n />\n <span></span>\n <span>{{ checkbox.label | translate }}</span>\n </label>\n }\n </div>\n }\n</div>\n\n@if (useTabs) {\n <c8y-tabs-outlet\n class=\"elevation-none\"\n outletName=\"searchTabs\"\n orientation=\"horizontal\"\n ></c8y-tabs-outlet>\n}\n\n<c8y-tab\n [icon]=\"'asterisk-key'\"\n [title]=\"'All devices, assets and groups' | translate\"\n [isActive]=\"selectedTab === tabNames.all\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.all | translate\"\n [priority]=\"1000\"\n (onSelect)=\"onTabChange(tabNames.all)\"\n></c8y-tab>\n<c8y-tab\n [icon]=\"'exchange'\"\n [title]=\"'Devices only' | translate\"\n [isActive]=\"selectedTab === tabNames.devices\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.devices | translate\"\n [priority]=\"750\"\n (onSelect)=\"onTabChange(tabNames.devices)\"\n></c8y-tab>\n<c8y-tab\n [icon]=\"'c8y-modules'\"\n [title]=\"'Assets and groups only' | translate\"\n [isActive]=\"selectedTab === tabNames.groupsAndAssets\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.groupsAndAssets | translate\"\n [priority]=\"500\"\n (onSelect)=\"onTabChange(tabNames.groupsAndAssets)\"\n></c8y-tab>\n\n@if (useTabs && (context$ | async)?.contextData?.name) {\n <div class=\"p-l-48 p-t-8 d-flex a-i-center\">\n <span translate>Search only in`specified group or asset`</span>\n <span class=\"text-bold p-l-4\">{{ (context$ | async)?.contextData?.name }}</span>\n <label\n class=\"m-l-8 c8y-switch c8y-switch--inline\"\n [title]=\"'Search only within the current group or asset hierarchy' | translate\"\n >\n <input\n [attr.aria-label]=\"'Search only within the current group or asset hierarchy' | translate\"\n type=\"checkbox\"\n [ngModel]=\"isOnlyHierarchyQuery\"\n (ngModelChange)=\"onHierarchyQueryChange($event)\"\n />\n <span></span>\n </label>\n </div>\n}\n", dependencies: [{ kind: "component", type: TabsOutletComponent, selector: "c8y-tabs-outlet,c8y-ui-tabs", inputs: ["tabs", "orientation", "navigatorOpen", "outletName", "context", "openFirstTab", "hasHeader"] }, { kind: "component", type: TabComponent, selector: "c8y-tab", inputs: ["path", "label", "icon", "priority", "orientation", "injector", "tabsOutlet", "isActive", "text", "showAlways"], outputs: ["onSelect"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchCustomFiltersComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-search-custom-filters', imports: [C8yTranslatePipe, TabsOutletComponent, TabComponent, AsyncPipe, FormsModule], template: "<div class=\"d-flex gap-16 p-l-4 p-l-24 p-r-24\">\n @if (!useTabs) {\n <div class=\"p-b-8 separator-bottom sticky-top p-t-4\">\n @for (checkbox of checkboxesState; track $index) {\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n [checked]=\"checkbox.value\"\n [indeterminate]=\"checkbox.indeterminate\"\n (click)=\"onCheckboxChange($event, checkbox)\"\n [attr.disabled]=\"checkbox.isDisabled ? true : null\"\n />\n <span></span>\n <span>{{ checkbox.label | translate }}</span>\n </label>\n }\n </div>\n }\n</div>\n\n@if (useTabs) {\n <c8y-tabs-outlet\n class=\"elevation-none\"\n outletName=\"searchTabs\"\n orientation=\"horizontal\"\n ></c8y-tabs-outlet>\n}\n\n<c8y-tab\n [icon]=\"'asterisk-key'\"\n [title]=\"'All devices, assets and groups' | translate\"\n [isActive]=\"selectedTab === tabNames.all\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.all | translate\"\n [priority]=\"1000\"\n (onSelect)=\"onTabChange(tabNames.all)\"\n></c8y-tab>\n<c8y-tab\n [icon]=\"'exchange'\"\n [title]=\"'Devices only' | translate\"\n [isActive]=\"selectedTab === tabNames.devices\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.devices | translate\"\n [priority]=\"750\"\n (onSelect)=\"onTabChange(tabNames.devices)\"\n></c8y-tab>\n<c8y-tab\n [icon]=\"'c8y-modules'\"\n [title]=\"'Assets and groups only' | translate\"\n [isActive]=\"selectedTab === tabNames.groupsAndAssets\"\n [tabsOutlet]=\"'searchTabs'\"\n [label]=\"tabNames.groupsAndAssets | translate\"\n [priority]=\"500\"\n (onSelect)=\"onTabChange(tabNames.groupsAndAssets)\"\n></c8y-tab>\n\n@if (useTabs && (context$ | async)?.contextData?.name) {\n <div class=\"p-l-48 p-t-8 d-flex a-i-center\">\n <span translate>Search only in`specified group or asset`</span>\n <span class=\"text-bold p-l-4\">{{ (context$ | async)?.contextData?.name }}</span>\n <label\n class=\"m-l-8 c8y-switch c8y-switch--inline\"\n [title]=\"'Search only within the current group or asset hierarchy' | translate\"\n >\n <input\n [attr.aria-label]=\"'Search only within the current group or asset hierarchy' | translate\"\n type=\"checkbox\"\n [ngModel]=\"isOnlyHierarchyQuery\"\n (ngModelChange)=\"onHierarchyQueryChange($event)\"\n />\n <span></span>\n </label>\n </div>\n}\n" }] }], propDecorators: { useTabs: [{ type: Input }], customDataQuery: [{ type: Output }], refresh: [{ type: Output }], isOnlyHierarchyQuery: [{ type: Input }], isOnlyHierarchyQueryChange: [{ type: Output }] } }); class SearchActionComponent { constructor() { this.typeaheadReload = new EventEmitter(); this.featureCacheService = inject(FeatureCacheService); this.isWildcardSearchEnabled$ = this.featureCacheService .getFeatureState(WILDCARD_SEARCH_FEATURE_KEY) .pipe(catchError(() => of(true))); this.c8yRouter = inject(RouterService); this.router = inject(Router); this.moduleConfig = inject(SEARCH_CONFIG, { optional: true }); this.isOnlyHierarchyQuery = false; this.showAdvancedFilters = this.moduleConfig?.showAdvancedFilters ?? true; this.customPlaceholder = this.moduleConfig?.placeholder; } triggerDataLoad() { this.typeaheadReload.next(null); } onOpenChange(isOpen) { if (isOpen) { this.triggerDataLoad(); } } async onSearch(on) { if (await lastValueFrom(this.isWildcardSearchEnabled$)) { return; } this.navigate(['/assetsearch'], { queryParams: { search: on }, replaceUrl: true }); } onFilter(on) { this.navigate(['/assetsearch'], { queryParams: { filter: on }, replaceUrl: true }); } onClick(mo) { this.router.navigateByUrl(this.c8yRouter.getHref(mo, '/')); } navigate(commands, extras) { this.router .navigateByUrl('/', { skipLocationChange: true }) .then(() => this.router.navigate(commands, extras)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchActionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: SearchActionComponent, isStandalone: true, selector: "c8y-search-action", ngImport: i0, template: "<c8y-search-input\n #input\n (filter)=\"onFilter($event)\"\n (search)=\"onSearch($event)\"\n (onClick)=\"onClick($event)\"\n (onOpenToggle)=\"onOpenChange($event)\"\n [enableCustomTemplatePlaceholder]=\"true\"\n [customPlaceholder]=\"customPlaceholder\"\n [customDataQuery]=\"customQuery\"\n [externalTerm]=\"typeaheadReload\"\n [mode]=\"(isWildcardSearchEnabled$ | async) ? 'wildcardsearch' : 'search'\"\n>\n @if (showAdvancedFilters && input.term.length > 0) {\n <c8y-search-custom-filters\n (refresh)=\"triggerDataLoad()\"\n [useTabs]=\"isWildcardSearchEnabled$ | async\"\n [(isOnlyHierarchyQuery)]=\"isOnlyHierarchyQuery\"\n (customDataQuery)=\"customQuery = $event\"\n ></c8y-search-custom-filters>\n }\n</c8y-search-input>\n", dependencies: [{ kind: "component", type: SearchInputComponent, selector: "c8y-search-input", inputs: ["mode", "enableCustomTemplatePlaceholder", "customPlaceholder", "externalTerm", "customDataQuery", "container", "groupsOnly"], outputs: ["filter", "search", "reset", "onClick", "onOpenToggle"] }, { kind: "component", type: SearchCustomFiltersComponent, selector: "c8y-search-custom-filters", inputs: ["useTabs", "isOnlyHierarchyQuery"], outputs: ["customDataQuery", "refresh", "isOnlyHierarchyQueryChange"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchActionComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-search-action', imports: [SearchInputComponent, SearchCustomFiltersComponent, AsyncPipe], template: "<c8y-search-input\n #input\n (filter)=\"onFilter($event)\"\n (search)=\"onSearch($event)\"\n (onClick)=\"onClick($event)\"\n (onOpenToggle)=\"onOpenChange($event)\"\n [enableCustomTemplatePlaceholder]=\"true\"\n [customPlaceholder]=\"customPlaceholder\"\n [customDataQuery]=\"customQuery\"\n [externalTerm]=\"typeaheadReload\"\n [mode]=\"(isWildcardSearchEnabled$ | async) ? 'wildcardsearch' : 'search'\"\n>\n @if (showAdvancedFilters && input.term.length > 0) {\n <c8y-search-custom-filters\n (refresh)=\"triggerDataLoad()\"\n [useTabs]=\"isWildcardSearchEnabled$ | async\"\n [(isOnlyHierarchyQuery)]=\"isOnlyHierarchyQuery\"\n (customDataQuery)=\"customQuery = $event\"\n ></c8y-search-custom-filters>\n }\n</c8y-search-input>\n" }] }], ctorParameters: () => [] }); class AssetTypeSearchGridColumn extends AssetTypeGridColumn { constructor(hideExtendedFilters, initialColumnConfig, assetSearchService, customPlaceholder) { super(initialColumnConfig); this.filterable = true; this.filteringConfig = this.getFilteringConfig(hideExtendedFilters, assetSearchService, customPlaceholder); } getFilteringConfig(hideExtendedFilters, assetSearchService, customPlaceholder) { return { fields: [ ...getBasicInputArrayFormFieldConfig({ key: 'types', label: gettext('Show items with type'), addText: gettext('Add next`type`'), tooltip: gettext('Use * as a wildcard character'), placeholder: customPlaceholder ? customPlaceholder : gettext('building`e.g. house`'), optional: !hideExtendedFilters }), { key: SearchFilters.ALL_FILTERS, type: 'checkbox', hide: hideExtendedFilters, props: { indeterminate: false, disabled: false, label: gettext('All'), click: (field, clickEvent) => { const { checked } = clickEvent.target; // Handle checked state if (checked) { field.form.get(SearchFilters.ONLY_DEVICES).setValue(true); field.form.get(SearchFilters.ONLY_GROUPS_AND_ASSETS).setValue(true); // Emit new state assetSearchService.appliedFilters$.next({ [SearchFilters.ALL_FILTERS]: checked, [SearchFilters.ONLY_DEVICES]: true, [SearchFilters.ONLY_GROUPS_AND_ASSETS]: true, [SearchFilters.CURRENT_HIERARCHY]: null }); } } }, expressionProperties: { 'props.indeterminate': (model, formState, field) => { // Do nothing if (field.form.get(SearchFilters.ALL_FILTERS).value === true) { return; } // Set indeterminate state if (!field.form.get(SearchFilters.ONLY_DEVICES).value || !field.form.get(SearchFilters.ONLY_GROUPS_AND_ASSETS).value) { field.form.get(SearchFilters.ALL_FILTERS).setValue(null); return true; } return false; }, 'props.disabled': (model, formState, field) => { if (field.form.get(SearchFilters.ALL_FILTERS).value === true) { return true; } return false; } }, hooks: { onInit: field => { // Get initial state const { allFilters } = assetSearchService?.appliedFilters$?.value; field.formControl.setValue(allFilters); } } }, { key: SearchFilters.ONLY_DEVICES, type: 'checkbox', hide: hideExtendedFilters, props: { indeterminate: false, label: gettext('Show only devices'), click: (field, clickEvent) => { const oldFilterValue = assetSearchService.appliedFilters$.value; const { checked } = clickEvent.target; // Handle checked state if (checked) { field.form.get(SearchFilters.ALL_FILTERS).setValue(true); // Emit new state assetSearchService.appliedFilters$.next({ ...oldFilterValue, [SearchFilters.ALL_FILTERS]: true, [SearchFilters.ONLY_DEVICES]: checked }); return; } // Handle unchecked state field.form.get(SearchFilters.ALL_FILTERS).setValue(null); // Trigger indeterminate state field.form.get(SearchFilters.ONLY_GROUPS_AND_ASSETS).setValue(true); // Emit new state assetSearchService.appliedFilters$.next({ [SearchFilters.ALL_FILTERS]: null, [SearchFilters.ONLY_GROUPS_AND_ASSETS]: true, [SearchFilters.ONLY_DEVICES]: checked, [SearchFilters.CURRENT_HIERARCHY]: null }); } }, hooks: { onInit: field => { // Get initial state const { onlyDevices } = assetSearchService?.appliedFilters$?.value; field.formControl.setValue(onlyDevices); } } }, { key: SearchFilters.ONLY_GROUPS_AND_ASSETS, type: 'checkbox', hide: hideExtendedFilters, props: { indeterminate: false, label: gettext('Show only groups and assets'), click: (field, clickEvent) => { const oldFilterValue = assetSearchService.appliedFilters$.value; const { checked } = clickEvent.target; // Handle checked state if (checked) { field.form.get(SearchFilters.ALL_FILTERS).setValue(true); // Emit new state assetSearchService.appliedFilters$.next({ ...oldFilterValue, [SearchFilters.ALL_FILTERS]: true, [SearchFilters.ONLY_GROUPS_AND_ASSETS]: checked }); return; } // Handle unchecked state field.form.get(SearchFilters.ALL_FILTERS).setValue(null); // Trigger indeterminate state field.form.get(SearchFilters.ONLY_DEVICES).setValue(true); // Emit new state assetSearchService.appliedFilters$.next({ [SearchFilters.ALL_FILTERS]: null, [SearchFilters.ONLY_GROUPS_AND_ASSETS]: checked, [SearchFilters.ONLY_DEVICES]: true, [SearchFilters.CURRENT_HIERARCHY]: false }); } }, hooks: { onInit: field => { // Get initial state const { onlyGroupsAndAssets } = assetSearchService?.appliedFilters$?.value; field.formControl.setValue(onlyGroupsAndAssets); } } } ], /** * Adding devices and groups to a filter is already handled in {@link AssetSearchService#buildSearchQuery} * */ getFilter(model) { const filter = {}; const ors = []; if (model.types?.length) { ors.push({ type: { __in: model.types } }); } if (ors.length) { filter.__or = ors; } return filter; } }; } } class SearchGridComponent { constructor() { this.title = ''; this.loadingItemsLabel = gettext('Loading results…'); this.selectable = false; this.onColumnsChange = new EventEmitter(); this.searchText = ''; this.moduleConfig = inject(SEARCH_CONFIG, { optional: true }); this.showAdvancedFilters = this.moduleConfig?.showAdvancedFilters ?? false; this.customPlaceholder = this.moduleConfig?.placeholder ?? undefined; this.assetSearchService = inject(AssetSearchService); this.pagination = this.assetSearchService.getDefaultPagination(); this.bulkActionControls = this.assetSearchService.getDefaultBulkActionControls(); this.refresh = new EventEmitter(); this.featureCacheService = inject(FeatureCacheService); this.isWildcardSearchEnabled$ = this.featureCacheService .getFeatureState(WILDCARD_SEARCH_FEATURE_KEY) .pipe(catchError(() => of(true))); this.wildcardSearchTitle = gettext('Asset data'); this.fullTextSearchTitle = gettext('Search results'); this.sizeCount = 0; this.bsModalService = inject(BsModalService); this.smartGroupsService = inject(SmartGroupsService); this.subAssetsGridService = inject(SubAssetsService); } set _columns(value) { if (value) { this.columns = value; } else { this.columns = this.assetSearchService.getDefaultColumns(); } } set _pagination(value) { if (value) { this.pagination = value; } } set _actionControls(value) { if (value) { this.actionControls = value; } else { this.actionControls = this.assetSearchService.getDefaultActionControls(); } } set _bulkActionControls(value) { if (value) { this.bulkActionControls = value; } else { this.bulkActionControls = this.assetSearchService.getDefaultBulkActionControls(); } } getGridConfigContext() { return { key: this.columnsConfigKey || this.assetSearchService.GRID_CONFIG_STORAGE_KEY, configFilter: { filter: false, sort: false } }; } ngOnInit() { if (!this.filteringName) { this.columns = [ new AssetTypeSearchGridColumn(this.showAdvancedFilters, { sortOrder: 'desc' }, this.assetSearchService, this.customPlaceholder), ...this.assetSearchService.getDefaultColumns() ]; } else { this.columns = [ new AssetTypeSearchGridColumn(this.showAdvancedFilters, { sortOrder: 'desc' }, this.assetSearchService), new NameDeviceGridColumn({ sortOrder: 'asc', filter: { externalFilterQuery: { names: [this.filteringName] } } }), new ModelDeviceGridColumn(), new SerialNumberDeviceGridColumn({ visible: false }), new RegistrationDateDeviceGridColumn({ visible: false }), new SystemIdDeviceGridColumn({ visible: false }), new ImeiDeviceGridColumn({ visible: false }), new AlarmsDeviceGridColumn() ]; } this.serverSideDataCallback = this.onDataSourceModifier.bind(this); this.setActionControls(); } ngAfterViewInit() { this.setInitialFilterForTypeColumn(); } trackByName(_index, column) { return column.name; } async onDataSourceModifier(dataSourceModifier) { const response = await this.assetSearchService.getData(dataSourceModifier.columns, dataSourceModifier.pagination, dataSourceModifier.searchText); const { res, data, paging } = response; if (paging.currentPage === 1) { this.sizeCount = 0; } this.sizeCount += data.length; this.onColumnsChange.emit(dataSourceModifier.columns); return { res, data, paging, filteredSize: this.sizeCount, size: undefined }; } setActionControls() { const actionControls = []; const deleteAction = { type: BuiltInActionType.Delete, callback: (asset) => this.onDeleteAsset(asset, this.parentGroup) }; actionControls.push(deleteAction); if (!this.actionControls) { this.actionControls = actionControls; } } updateFiltering(columnNames, action) { const { type } = action; if (type === FilteringActionType.ResetFilter) { this.dataGrid.clearFilters(); } else { /** * TODO: find better solution. After new changes from DM team, we're running into race condition where * this.dataGrid.updateFiltering is executed before this.configurationStrategy.getConfig$() value is emitted. * Columns setter sets columns after this.dataGrid.updateFiltering executes its logic. Value of this.columns in * dataGrid.updateFiltering is just not yet set. */ setTimeout(() => { this.dataGrid.updateFiltering(columnNames, action, false); }, 500); } } onColumnFilterReset(column) { if (column.name === 'type') { this.assetSearchService.resetAppliedFilters(); } } onDeleteAsset(asset, parentRef) { const initialState = { showWithDeviceUserCheckbox: this.subAssetsGridService.shouldShowWithDeviceUserCheckbox(asset), asset, showWithCascadeCheckbox: !this.smartGroupsService.isSmartGroup(asset) }; const modalRef = this.bsModalService.show(DeleteAssetsModalComponent, { initialState }); modalRef.content.closeSubject.subscribe(async (result) => { if (result) { await this.subAssetsGridService.deleteAsset(asset, parentRef, result); this.refresh.emit(); } }); } setInitialFilterForTypeColumn() { const checkboxes = this.assetSearchService.appliedFilters$.value; // Set filter only when all checkboxes are not selected if (checkboxes[SearchFilters.ONLY_DEVICES] !== checkboxes[SearchFilters.ONLY_GROUPS_AND_ASSETS]) { const externalFilterQuery = { [SearchFilters.ONLY_DEVICES]: checkboxes[SearchFilters.ONLY_DEVICES], [SearchFilters.ONLY_GROUPS_AND_ASSETS]: checkboxes[SearchFilters.ONLY_GROUPS_AND_ASSETS] }; this.updateFiltering(['type'], { type: FilteringActionType.ApplyFilter, payload: { filteringModifier: { externalFilterQuery } } }); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: SearchGridComponent, isStandalone: true, selector: "c8y-search-grid", inputs: { parentGroup: ["parent-group", "parentGroup"], title: "title", loadingItemsLabel: "loadingItemsLabel", _columns: ["columns", "_columns"], _pagination: ["pagination", "_pagination"], _actionControls: ["actionControls", "_actionControls"], selectable: "selectable", _bulkActionControls: ["bulkActionControls", "_bulkActionControls"], searchText: "searchText", filteringName: "filteringName", columnsConfigKey: "columnsConfigKey" }, outputs: { onColumnsChange: "onColumnsChange" }, providers: [ { provide: DATA_GRID_CONFIGURATION_STRATEGY, useClass: UserPreferencesConfigurationStrategy }, { provide: DATA_GRID_CONFIGURATION_CONTEXT_PROVIDER, useExisting: SearchGridComponent } ], viewQueries: [{ propertyName: "dataGrid", first: true, predicate: DataGridComponent, descendants: true, static: true }], ngImport: i0, template: "<div class=\"card--grid--fullpage border-top border-bottom\">\n <c8y-data-grid\n [title]=\"\n ((isWildcardSearchEnabled$ | async) ? wildcardSearchTitle : fullTextSearchTitle) | translate\n \"\n [loadingItemsLabel]=\"loadingItemsLabel\"\n [columns]=\"columns\"\n [pagination]=\"pagination\"\n [actionControls]=\"actionControls\"\n [selectable]=\"selectable\"\n [bulkActionControls]=\"bulkActionControls\"\n [serverSideDataCallback]=\"serverSideDataCallback\"\n [infiniteScroll]=\"'auto'\"\n [showSearch]=\"true\"\n [searchText]=\"searchText\"\n [refresh]=\"refresh\"\n (onColumnFilterReset)=\"onColumnFilterReset($event)\"\n >\n <c8y-ui-empty-state\n [icon]=\"'search'\"\n [title]=\"'No results to display.' | translate\"\n [subtitle]=\"'Refine your search terms or check your spelling.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n </c8y-data-grid>\n</div>\n", dependencies: [{ kind: "component", type: DataGridComponent, selector: "c8y-data-grid", inputs: ["title", "loadMoreItemsLabel", "loadingItemsLabel", "showSearch", "refresh", "loading", "columns", "rows", "pagination", "childNodePagination", "infiniteScroll", "serverSideDataCallback", "selectable", "singleSelection", "selectionPrimaryKey", "displayOptions", "actionControls", "bulkActionControls", "headerActionControls", "searchText", "configureColumnsEnabled", "showCounterWarning", "activeClassName", "expandableRows", "treeGrid", "hideReload", "childNodesProperty", "parentNodeLabelProperty"], outputs: ["rowMouseOver", "rowMouseLeave", "rowClick", "onConfigChange", "onBeforeFilter", "onBeforeSearch", "onFilter", "itemsSelect", "onReload", "onAddCustomColumn", "onRemoveCustomColumn", "onColumnFilterReset", "onSort", "onPageSizeChange", "onColumnReordered", "onColumnVisibilityChange"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: SearchGridComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-search-grid', providers: [ { provide: DATA_GRID_CONFIGURATION_STRATEGY, useClass: UserPreferencesConfigurationStrategy }, { provide: DATA_GRID_CONFIGURATION_CONTEXT_PROVIDER, useExisting: SearchGridComponent } ], imports: [DataGridComponent, EmptyStateComponent, C8yTranslatePipe, AsyncPipe], template: "<div class=\"card--grid--fullpage border-top border-bottom\">\n <c8y-data-grid\n [title]=\"\n ((isWildcardSearchEnabled$ | async) ? wildcardSearchTitle : fullTextSearchTitle) | translate\n \"\n [loadingItemsLabel]=\"loadingItemsLabel\"\n [columns]=\"columns\"\n [pagination]=\"pagination\"\n [actionControls]=\"actionControls\"\n [selectable]=\"selectable\"\n [bulkActionControls]=\"bulkActionControls\"\n [serverSideDataCallback]=\"serverSideDataCallback\"\n [infiniteScroll]=\"'auto'\"\n [showSearch]=\"true\"\n [searchText]=\"searchText\"\n [refresh]=\"refresh\"\n (onColumnFilterReset)=\"onColumnFilterReset($event)\"\n >\n <c8y-ui-empty-state\n [icon]=\"'search'\"\n [title]=\"'No results to display.' | translate\"\n [subtitle]=\"'Refine your search terms or check your spelling.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n </c8y-data-grid>\n</div>\n" }] }], propDecorators: { parentGroup: [{ type: Input, args: ['parent-group'] }], title: [{ type: Input }], loadingItemsLabel: [{ type: Input }], _columns: [{ type: Input, args: ['columns'] }], _pagination: [{ type: Input, args: ['pagination'] }], _actionControls: [{ type: Input, args: ['actionControls'] }], selectable: [{ type: Input }], _bulkActionControls: [{ type: Input, args: ['bulkActionControls'] }], onColumnsChange: [{ type: Output }], searchText: [{ type: Input }], filteringName: [{ type: Input }], columnsConfigKey: [{ type: Input }], dataGrid: [{ type: ViewChild, args: [DataGridComponent, { static: true }] }] } }); class SearchResultsComponent { constructor(route, alert) { this.route = route; this.alert = alert; this.filter = ''; this.searchText = ''; this.featureCacheService = inject(FeatureCacheService); this.isWildcardSearchEnabled$ = this.featureCacheService .getFeatureState(WILDCARD_SEARCH_FEATURE_KEY) .pipe(catchError$1(() => of(true))); this.WARNING_TIMEOUT_TIME = 3000; this.unsubscribe$ = new Subject(); } ngOnInit() { this.route.queryParams.pipe(takeUntil(this.unsubscribe$)).subscribe(params => { if (params.filter) { this.filteringName = params.filter; } }); } ngAfterViewInit() { this.route.queryParams .pipe(takeUntil(this.unsubscribe$)) .subscribe(({ filter, search }) => this.onQueryParamsChange(filter, search)); } resetSearch() { if (this.searchGrid.dataGrid.searchText) { this.alert.add({ text: gettext('Search reset. Full text search does not support filtering.'), type: Status.WARNING, timeout: this.WARNING_TIMEOUT_TIME }); this.searchText = ''; this.searchGrid.dataGrid.searchText = ''; } } resetFilter() { this.filter = ''; if (this.searchGrid.dataGrid.filteringApplied) { this.alert.add({ text: gettext('Filter reset. Full text search does not support filtering.'), type: Status.WARNING, timeout: this.WARNING_TIMEOUT_TIME }); this.searchGrid.dataGrid.clearFilters(false); } } ngOnDestroy() { this.unsubscribe$.next(); this.unsubscribe$.complete(); } onQueryParamsChange(filter, searchTerm) { if (!this.shouldFilter(filter) && searchTerm) { this.searchText = searchTerm || ''; } } shouldFilter(filter) { if (!filter) { return false; } this.resetSearch(); this.filter = filter || ''; this.searchGrid.updateFiltering(['name'], { type: FilteringActionType.ApplyFilter, payload: { filteringModifier: { externalFilterQuery: { names: [this.filter] } } } }); return true; } static {