Skip to content

Commit 4940308

Browse files
committed
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.
1 parent 001fd47 commit 4940308

3 files changed

Lines changed: 170 additions & 4 deletions

File tree

packages/pluggableWidgets/combobox-web/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- We fixed an issue where the custom editability condition (`never` or `conditionally`) was ignored when the combobox attribute was present and editable.
12+
13+
- 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.
14+
15+
### Breaking changes
16+
17+
- 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.
18+
919
## [2.8.2] - 2026-07-17
1020

1121
### Fixed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import Big from "big.js";
2+
import { EditableValue } from "mendix";
3+
import { dynamic, EditableValueBuilder } from "@mendix/widget-plugin-test-utils";
4+
import { getReadonly } from "../utils";
5+
6+
describe("getReadonly", () => {
7+
describe("when targetAttribute is readOnly", () => {
8+
it("is read-only regardless of customEditability 'default'", () => {
9+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").isReadOnly().build();
10+
11+
const result = getReadonly(
12+
targetAttribute as unknown as EditableValue<string | Big>,
13+
"default",
14+
dynamic.available(true)
15+
);
16+
17+
expect(result).toBe(true);
18+
});
19+
20+
it("is read-only regardless of customEditability 'never'", () => {
21+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").isReadOnly().build();
22+
23+
const result = getReadonly(
24+
targetAttribute as unknown as EditableValue<string | Big>,
25+
"never",
26+
dynamic.available(true)
27+
);
28+
29+
expect(result).toBe(true);
30+
});
31+
32+
it("is read-only regardless of customEditability 'conditionally' with expression true", () => {
33+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").isReadOnly().build();
34+
35+
const result = getReadonly(
36+
targetAttribute as unknown as EditableValue<string | Big>,
37+
"conditionally",
38+
dynamic.available(true)
39+
);
40+
41+
expect(result).toBe(true);
42+
});
43+
});
44+
45+
describe("when targetAttribute is editable", () => {
46+
it("is editable when customEditability is 'default'", () => {
47+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").build();
48+
49+
const result = getReadonly(
50+
targetAttribute as unknown as EditableValue<string | Big>,
51+
"default",
52+
dynamic.available(true)
53+
);
54+
55+
expect(result).toBe(false);
56+
});
57+
58+
it("is read-only when customEditability is 'never'", () => {
59+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").build();
60+
61+
const result = getReadonly(
62+
targetAttribute as unknown as EditableValue<string | Big>,
63+
"never",
64+
dynamic.available(true)
65+
);
66+
67+
expect(result).toBe(true);
68+
});
69+
70+
it("is read-only when customEditability is 'conditionally' and expression value is false", () => {
71+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").build();
72+
73+
const result = getReadonly(
74+
targetAttribute as unknown as EditableValue<string | Big>,
75+
"conditionally",
76+
dynamic.available(false)
77+
);
78+
79+
expect(result).toBe(true);
80+
});
81+
82+
it("is editable when customEditability is 'conditionally' and expression value is true", () => {
83+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").build();
84+
85+
const result = getReadonly(
86+
targetAttribute as unknown as EditableValue<string | Big>,
87+
"conditionally",
88+
dynamic.available(true)
89+
);
90+
91+
expect(result).toBe(false);
92+
});
93+
94+
it("is read-only when customEditability is 'conditionally' and expression is loading", () => {
95+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").build();
96+
97+
const result = getReadonly(
98+
targetAttribute as unknown as EditableValue<string | Big>,
99+
"conditionally",
100+
dynamic.loading()
101+
);
102+
103+
expect(result).toBe(true);
104+
});
105+
106+
it("is read-only when customEditability is 'conditionally' and expression is unavailable", () => {
107+
const targetAttribute = new EditableValueBuilder<string>().withValue("value").build();
108+
109+
const result = getReadonly(
110+
targetAttribute as unknown as EditableValue<string | Big>,
111+
"conditionally",
112+
dynamic.unavailable()
113+
);
114+
115+
expect(result).toBe(true);
116+
});
117+
});
118+
119+
describe("when targetAttribute is undefined", () => {
120+
it("is read-only when customEditability is 'never'", () => {
121+
const result = getReadonly(undefined, "never", dynamic.available(true));
122+
123+
expect(result).toBe(true);
124+
});
125+
126+
it("is editable when customEditability is 'default'", () => {
127+
const result = getReadonly(undefined, "default", dynamic.available(false));
128+
129+
expect(result).toBe(false);
130+
});
131+
132+
it("is read-only when customEditability is 'conditionally' and expression value is false", () => {
133+
const result = getReadonly(undefined, "conditionally", dynamic.available(false));
134+
135+
expect(result).toBe(true);
136+
});
137+
138+
it("is editable when customEditability is 'conditionally' and expression value is true", () => {
139+
const result = getReadonly(undefined, "conditionally", dynamic.available(true));
140+
141+
expect(result).toBe(false);
142+
});
143+
144+
it("is read-only when customEditability is 'conditionally' and expression is loading", () => {
145+
const result = getReadonly(undefined, "conditionally", dynamic.loading());
146+
147+
expect(result).toBe(true);
148+
});
149+
150+
it("is read-only when customEditability is 'conditionally' and expression is unavailable", () => {
151+
const result = getReadonly(undefined, "conditionally", dynamic.unavailable());
152+
153+
expect(result).toBe(true);
154+
});
155+
});
156+
});

packages/pluggableWidgets/combobox-web/src/helpers/Database/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,14 @@ export function getReadonly(
9999
customEditability: ComboboxContainerProps["customEditability"],
100100
customEditabilityExpression: ComboboxContainerProps["customEditabilityExpression"]
101101
): boolean {
102-
if (targetAttribute) {
103-
return targetAttribute.readOnly;
102+
if (targetAttribute?.readOnly) {
103+
return true;
104104
}
105105
if (customEditability === "never") {
106106
return true;
107107
}
108-
if (customEditability === "conditionally") {
109-
return customEditabilityExpression.value ?? true;
108+
if (customEditability === "conditionally" && customEditabilityExpression.value !== true) {
109+
return true;
110110
}
111111
return false;
112112
}

0 commit comments

Comments
 (0)