From 86b58b86010f7d203e7f5e90da7599854ba65599 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 16 Jul 2026 11:04:16 +0200 Subject: [PATCH] fix(combobox-web): fix inverted editability logic in getReadonly The `conditionally` branch returned `expression.value` directly, so a truthy value (meaning "editable") made the widget read-only, and vice versa. Fixed by returning `true` only when the expression is not `true`, and added unit tests for all `getReadonly` branches. --- .../combobox-web/CHANGELOG.md | 8 + .../helpers/Database/__tests__/utils.spec.ts | 156 ++++++++++++++++++ .../src/helpers/Database/utils.ts | 8 +- 3 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 packages/pluggableWidgets/combobox-web/src/helpers/Database/__tests__/utils.spec.ts diff --git a/packages/pluggableWidgets/combobox-web/CHANGELOG.md b/packages/pluggableWidgets/combobox-web/CHANGELOG.md index 165795776d..69de2fee60 100644 --- a/packages/pluggableWidgets/combobox-web/CHANGELOG.md +++ b/packages/pluggableWidgets/combobox-web/CHANGELOG.md @@ -12,6 +12,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We fixed an issue where the widget crashed when using the "On filter input change" action with the new string behavior enabled. +- We fixed an issue where the custom editability condition (`never` or `conditionally`) was ignored when the combobox attribute was present and editable. + +- We fixed an issue where the editability setting was inverted: setting editability to `false` allowed editing, and setting it to `true` prevented editing. The editability condition expression now behaves correctly: `true` means editable, `false` means read-only. + +### Breaking changes + +- The editability fix above is a potential breaking change. Apps that worked around the bug by inverting their editability expression will now have the opposite (incorrect) behavior after upgrading. Review any combobox widgets using a custom editability expression and remove the inversion workaround, otherwise fields may unexpectedly become editable or read-only. + ## [2.8.1] - 2026-04-30 ### Fixed diff --git a/packages/pluggableWidgets/combobox-web/src/helpers/Database/__tests__/utils.spec.ts b/packages/pluggableWidgets/combobox-web/src/helpers/Database/__tests__/utils.spec.ts new file mode 100644 index 0000000000..62623b7d1f --- /dev/null +++ b/packages/pluggableWidgets/combobox-web/src/helpers/Database/__tests__/utils.spec.ts @@ -0,0 +1,156 @@ +import Big from "big.js"; +import { EditableValue } from "mendix"; +import { dynamic, EditableValueBuilder } from "@mendix/widget-plugin-test-utils"; +import { getReadonly } from "../utils"; + +describe("getReadonly", () => { + describe("when targetAttribute is readOnly", () => { + it("is read-only regardless of customEditability 'default'", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").isReadOnly().build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "default", + dynamic.available(true) + ); + + expect(result).toBe(true); + }); + + it("is read-only regardless of customEditability 'never'", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").isReadOnly().build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "never", + dynamic.available(true) + ); + + expect(result).toBe(true); + }); + + it("is read-only regardless of customEditability 'conditionally' with expression true", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").isReadOnly().build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "conditionally", + dynamic.available(true) + ); + + expect(result).toBe(true); + }); + }); + + describe("when targetAttribute is editable", () => { + it("is editable when customEditability is 'default'", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "default", + dynamic.available(true) + ); + + expect(result).toBe(false); + }); + + it("is read-only when customEditability is 'never'", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "never", + dynamic.available(true) + ); + + expect(result).toBe(true); + }); + + it("is read-only when customEditability is 'conditionally' and expression value is false", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "conditionally", + dynamic.available(false) + ); + + expect(result).toBe(true); + }); + + it("is editable when customEditability is 'conditionally' and expression value is true", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "conditionally", + dynamic.available(true) + ); + + expect(result).toBe(false); + }); + + it("is read-only when customEditability is 'conditionally' and expression is loading", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "conditionally", + dynamic.loading() + ); + + expect(result).toBe(true); + }); + + it("is read-only when customEditability is 'conditionally' and expression is unavailable", () => { + const targetAttribute = new EditableValueBuilder().withValue("value").build(); + + const result = getReadonly( + targetAttribute as unknown as EditableValue, + "conditionally", + dynamic.unavailable() + ); + + expect(result).toBe(true); + }); + }); + + describe("when targetAttribute is undefined", () => { + it("is read-only when customEditability is 'never'", () => { + const result = getReadonly(undefined, "never", dynamic.available(true)); + + expect(result).toBe(true); + }); + + it("is editable when customEditability is 'default'", () => { + const result = getReadonly(undefined, "default", dynamic.available(false)); + + expect(result).toBe(false); + }); + + it("is read-only when customEditability is 'conditionally' and expression value is false", () => { + const result = getReadonly(undefined, "conditionally", dynamic.available(false)); + + expect(result).toBe(true); + }); + + it("is editable when customEditability is 'conditionally' and expression value is true", () => { + const result = getReadonly(undefined, "conditionally", dynamic.available(true)); + + expect(result).toBe(false); + }); + + it("is read-only when customEditability is 'conditionally' and expression is loading", () => { + const result = getReadonly(undefined, "conditionally", dynamic.loading()); + + expect(result).toBe(true); + }); + + it("is read-only when customEditability is 'conditionally' and expression is unavailable", () => { + const result = getReadonly(undefined, "conditionally", dynamic.unavailable()); + + expect(result).toBe(true); + }); + }); +}); diff --git a/packages/pluggableWidgets/combobox-web/src/helpers/Database/utils.ts b/packages/pluggableWidgets/combobox-web/src/helpers/Database/utils.ts index 69c2e2bed3..b2a8ecfe6e 100644 --- a/packages/pluggableWidgets/combobox-web/src/helpers/Database/utils.ts +++ b/packages/pluggableWidgets/combobox-web/src/helpers/Database/utils.ts @@ -99,14 +99,14 @@ export function getReadonly( customEditability: ComboboxContainerProps["customEditability"], customEditabilityExpression: ComboboxContainerProps["customEditabilityExpression"] ): boolean { - if (targetAttribute) { - return targetAttribute.readOnly; + if (targetAttribute?.readOnly) { + return true; } if (customEditability === "never") { return true; } - if (customEditability === "conditionally") { - return customEditabilityExpression.value ?? true; + if (customEditability === "conditionally" && customEditabilityExpression.value !== true) { + return true; } return false; }