From 80fb893b8adebf85c860b1e2753d5a63bde092a1 Mon Sep 17 00:00:00 2001 From: MartinaeyNL Date: Mon, 15 Jun 2026 16:33:49 +0200 Subject: [PATCH 1/4] Potential bugfix for #2874 with the addition of a assetDataProvider() that queries assets based on search dynamically. --- .../json-viewer/or-rule-action-attribute.ts | 52 ++++++++++++++----- .../src/json-viewer/or-rule-asset-query.ts | 52 ++++++++++++++----- .../src/json-viewer/or-rule-json-viewer.ts | 2 +- .../src/or-vaadin-combo-box.ts | 1 + .../src/or-vaadin-multi-select-combo-box.ts | 2 +- 5 files changed, 81 insertions(+), 28 deletions(-) diff --git a/ui/component/or-rules/src/json-viewer/or-rule-action-attribute.ts b/ui/component/or-rules/src/json-viewer/or-rule-action-attribute.ts index 814cdfc4c6..e60baba072 100644 --- a/ui/component/or-rules/src/json-viewer/or-rule-action-attribute.ts +++ b/ui/component/or-rules/src/json-viewer/or-rule-action-attribute.ts @@ -3,11 +3,13 @@ import {customElement, property, state} from "lit/decorators.js"; import {getAssetIdsFromQuery, getAssetTypeFromQuery, RulesConfig} from "../index"; import { Asset, + AssetModelUtil, + AssetQuery, + AssetQueryMatch, AssetTypeInfo, RuleActionUpdateAttribute, RuleActionWriteAttribute, - WellknownValueTypes, - AssetModelUtil, AssetQuery + WellknownValueTypes } from "@openremote/model"; import {Util} from "@openremote/core"; import "@openremote/or-attribute-input"; @@ -17,7 +19,12 @@ import {OrRulesJsonRuleChangedEvent} from "./or-rule-json-viewer"; import {OrAttributeInputChangedEvent} from "@openremote/or-attribute-input"; import {ifDefined} from "lit/directives/if-defined.js"; import {when} from "lit/directives/when.js"; -import {OrVaadinComboBox} from "@openremote/or-vaadin-components/or-vaadin-combo-box"; +import { + ComboBoxDataProviderCallback, + ComboBoxDataProviderParams, + OrVaadinComboBox +} from "@openremote/or-vaadin-components/or-vaadin-combo-box"; +import debounce from "lodash.debounce"; // language=CSS const style = css` @@ -89,6 +96,19 @@ export class OrRuleActionAttribute extends translate(i18next)(LitElement) { return query ? getAssetTypeFromQuery(query) : undefined; } + protected _assetDataProvider = debounce((params: ComboBoxDataProviderParams, callback: ComboBoxDataProviderCallback<{value: any, label: string}>) => { + const query = this.action.target?.assets ?? this.action?.target?.matchedAssets; + const assetType = getAssetTypeFromQuery(query); + const ids = getAssetIdsFromQuery(query); + const idValue = ids && ids.length > 0 ? ids[0] : "*"; + console.debug(`Searching for ${assetType} assets with filter '${params.filter}'...`); + this.loadAssets(assetType!, params.filter, idValue).finally(() => { + const assets = this._cache?.assets ?? []; + const filtered = assets.filter(a => a.name?.toLowerCase().includes(params.filter.toLowerCase())); + callback(filtered.map(a => ({value: a.id, label: a.name!})), filtered.length); + }) + }, 500); + protected render() { if (!this.action.target) { @@ -103,18 +123,18 @@ export class OrRuleActionAttribute extends translate(i18next)(LitElement) { const query = this.action.target.assets ? this.action.target.assets : this.action.target.matchedAssets ? this.action.target.matchedAssets : undefined; const assetDescriptor = this.assetInfos ? this.assetInfos.find(assetTypeInfo => assetTypeInfo.assetDescriptor!.name === assetType) : undefined; + const assetIds = getAssetIdsFromQuery(query); if (!assetDescriptor) { return html``; } if (!this._cache && !this._loading) { - this.loadAssets(assetType); + this.loadAssets(assetType, undefined, assetIds?.[0]); } // TODO: Add multiselect support - const ids = getAssetIdsFromQuery(query); - const idValue = ids && ids.length > 0 ? ids[0] : "*"; + const idValue = assetIds?.[0] ?? "*"; const idOptions: {value: any, label: string}[] = [ {value: "*", label: i18next.t("matched")} ]; @@ -164,11 +184,19 @@ export class OrRuleActionAttribute extends translate(i18next)(LitElement) { } return html` - this._assetId = (ev.currentTarget as OrVaadinComboBox).value}> - - + ${when(idOptions.length >= 100, () => html` + this._assetId = (ev.currentTarget as OrVaadinComboBox).value}> + + + `, () => html` + this._assetId = (ev.currentTarget as OrVaadinComboBox).value}> + + + `)} ${when(attributes.length > 0, () => html` ) => { + const assetType = getAssetTypeFromQuery(this.query); + const ids = getAssetIdsFromQuery(this.query); + const idValue = ids && ids.length > 0 ? ids[0] : "*"; + console.debug(`Searching for ${assetType} assets with filter '${params.filter}'...`); + this.loadAssets(assetType!, params.filter, idValue).finally(() => { + const assets = this._cache?.assets ?? []; + const filtered = assets.filter(a => a.name?.toLowerCase().includes(params.filter.toLowerCase())); + callback(filtered.map(a => ({value: a.id, label: a.name!})), filtered.length); + }) + }, 500); + protected render() { const assetType = getAssetTypeFromQuery(this.query); + const assetIds = getAssetIdsFromQuery(this.query); if (!assetType) { return html`${i18next.t("errorOccurred")}`; @@ -430,7 +449,7 @@ export class OrRuleAssetQuery extends translate(i18next)(LitElement) { } if (!this._cache && !this._loading) { - this.loadAssets(assetType); + this.loadAssets(assetType, undefined, assetIds?.[0]); } if (!this.query.attributes) { @@ -444,8 +463,7 @@ export class OrRuleAssetQuery extends translate(i18next)(LitElement) { const showRemoveAttribute = !this.readonly && this.query.attributes && this.query.attributes.items && this.query.attributes.items.length > 1; // TODO: Add multiselect support - const ids = getAssetIdsFromQuery(this.query); - const idValue = ids && ids.length > 0 ? ids[0] : "*"; + const idValue = assetIds?.[0] ?? "*"; const idOptions: {value: any, label: string}[] = [ {value: "*", label: i18next.t("anyOfThisType")} ]; @@ -456,8 +474,6 @@ export class OrRuleAssetQuery extends translate(i18next)(LitElement) { // If >= 100 assets: only display if in line with search input const assets: Asset[] = this._cache ? this._cache.assets : []; - let searchProvider: (search?: string) => Promise<[any, string][]>; - return html`
@@ -483,10 +499,18 @@ export class OrRuleAssetQuery extends translate(i18next)(LitElement) { }; return html` - this._assetId = (ev.currentTarget as OrVaadinComboBox).value}> - - + ${when(idOptions.length >= 100, () => html` + this._assetId = (ev.currentTarget as OrVaadinComboBox).value}> + + + `, () => html` + this._assetId = (ev.currentTarget as OrVaadinComboBox).value}> + + + `)}
${this.query.attributes && this.query.attributes.items ? this.query.attributes.items.map((attributePredicate, index) => { return html` @@ -955,7 +979,7 @@ export class OrRuleAssetQuery extends translate(i18next)(LitElement) { const query: AssetQuery = { limit: 100 }; if (search) { query.names ??= []; - query.names.push({ predicateType: "string", value: search }); + query.names.push({ predicateType: "string", match: AssetQueryMatch.CONTAINS, value: search }); } // If the cache contains assets from the same query, don't send HTTP request again const isQueryCached = this._cache?.query && Util.objectsEqual(this._cache.query, query, true); diff --git a/ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts b/ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts index 9cfe0b8dbb..f11bacc350 100644 --- a/ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts +++ b/ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts @@ -219,7 +219,7 @@ export class OrRuleJsonViewer extends translate(i18next)(LitElement) implements // loadAssets function that also tracks what promises/fetches are active. // If so, await for those to finish to prevent multiple API requests. protected async loadAssets(type: string, query?: AssetQuery): Promise { - if (this._activeAssetPromises.has(type)) { + if (this._activeAssetPromises.has(type) && !query?.ids) { const data = await (this._activeAssetPromises.get(type)); // await for the already existing fetch return data.assets; } else { diff --git a/ui/component/or-vaadin-components/src/or-vaadin-combo-box.ts b/ui/component/or-vaadin-components/src/or-vaadin-combo-box.ts index be04eb7a9a..e352413741 100644 --- a/ui/component/or-vaadin-components/src/or-vaadin-combo-box.ts +++ b/ui/component/or-vaadin-components/src/or-vaadin-combo-box.ts @@ -28,6 +28,7 @@ import {PropertyValues, type LitElement} from "lit"; * https://vaadin.com/docs/latest/components/combo-box/ */ export {comboBoxRenderer, ComboBoxLitRenderer} from "@vaadin/combo-box/lit"; +export {ComboBoxDataProviderCallback, ComboBoxDataProviderParams} from "@vaadin/combo-box"; @customElement("or-vaadin-combo-box") export class OrVaadinComboBox extends (ComboBox as new () => ComboBox & LitElement) implements OrVaadinComponent { diff --git a/ui/component/or-vaadin-components/src/or-vaadin-multi-select-combo-box.ts b/ui/component/or-vaadin-components/src/or-vaadin-multi-select-combo-box.ts index 1a906b413b..32c4ac193c 100644 --- a/ui/component/or-vaadin-components/src/or-vaadin-multi-select-combo-box.ts +++ b/ui/component/or-vaadin-components/src/or-vaadin-multi-select-combo-box.ts @@ -20,7 +20,7 @@ import { customElement } from "lit/decorators.js"; import { MultiSelectComboBox } from "@vaadin/multi-select-combo-box"; import { OrVaadinComponent } from "./util"; -import { LitElement, PropertyValues } from "lit"; +import { LitElement } from "lit"; @customElement("or-vaadin-multi-select-combo-box") export class OrVaadinMultiSelectComboBox extends (MultiSelectComboBox as new () => MultiSelectComboBox & LitElement) implements OrVaadinComponent { From 5d8710cdf6528db277459268471d0e5a81d0172f Mon Sep 17 00:00:00 2001 From: MartinaeyNL Date: Mon, 15 Jun 2026 16:44:01 +0200 Subject: [PATCH 2/4] lodash dependency fix --- ui/component/or-rules/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/ui/component/or-rules/package.json b/ui/component/or-rules/package.json index 4f7f25d795..ec46cdd9e3 100644 --- a/ui/component/or-rules/package.json +++ b/ui/component/or-rules/package.json @@ -39,6 +39,7 @@ "iso-639-1": "^3.1.3", "linqts": "^1.12.6", "lit": "^3.3.1", + "lodash.debounce": "^4.0.8", "moment": "^2.29.4", "resize-observer": "^1.0.3", "shortid": "^2.2.15" diff --git a/yarn.lock b/yarn.lock index 99516fb812..1959bf9a7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2235,6 +2235,7 @@ __metadata: iso-639-1: "npm:^3.1.3" linqts: "npm:^1.12.6" lit: "npm:^3.3.1" + lodash.debounce: "npm:^4.0.8" moment: "npm:^2.29.4" resize-observer: "npm:^1.0.3" shortid: "npm:^2.2.15" From 2f9daec964c5775432828f30eabfd965412e81a4 Mon Sep 17 00:00:00 2001 From: MartinaeyNL Date: Mon, 15 Jun 2026 17:28:36 +0200 Subject: [PATCH 3/4] Improve coverage of Rules UI test, to take 'assets beyond the pagination threshold' into count. Asset pagination is now covered. --- ui/app/manager/test/rules.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/manager/test/rules.test.ts b/ui/app/manager/test/rules.test.ts index e1cf945325..0592dcff98 100644 --- a/ui/app/manager/test/rules.test.ts +++ b/ui/app/manager/test/rules.test.ts @@ -150,7 +150,7 @@ test("Create a When-Then rule for an asset with a trigger and action", async ({p * @then The When-Then rule should appear in the rule list */ test("Create a When-Then rule by searching for an asset", async ({page, manager, shared}) => { - const multiplier = 100; + const multiplier = 200; // Using a multiplier above 100, which is the default querying limit const aLotOfAssets = generateALotOfAssets(multiplier); await manager.setup("smartcity", {assets: aLotOfAssets}); await manager.goToRealmStartPage("smartcity"); From 555fe1accd6dca9bb7df2f0f0b4ef8514bc0450f Mon Sep 17 00:00:00 2001 From: MartinaeyNL Date: Mon, 15 Jun 2026 17:59:04 +0200 Subject: [PATCH 4/4] Fix to prevent _activeAssetPromises to get corrupted --- ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts b/ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts index f11bacc350..05339f8dc7 100644 --- a/ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts +++ b/ui/component/or-rules/src/json-viewer/or-rule-json-viewer.ts @@ -230,9 +230,9 @@ export class OrRuleJsonViewer extends translate(i18next)(LitElement) implements name: this._ruleset.type == "realm" ? this._ruleset.realm : undefined }; const promise = getAssetsByType(type, query); - this._activeAssetPromises.set(type, promise); + if(!query.ids) this._activeAssetPromises.set(type, promise); const data = await promise; - this._activeAssetPromises.delete(type); + if(!query.ids) this._activeAssetPromises.delete(type); return data.assets; } }