Skip to content

Commit fd7979a

Browse files
authored
feat: Optional separateReadonlyFromDisabled option and Vuetify impl (#2532)
- Add `separateReadonlyFromDisabled` config option enabling independent control of readonly and enabled states. - By default, they are not separated keeping the legacy behavior - Adds READONLY/WRITABLE rules corresponding to the existing ENABLE/DISABLE rules - Add optional property `readonly` to renderer props and state. Optional to keep backward compatibility - Extend the Vue Vuetify renderer set to consider the the new flag and set renderers' inputs to readonly (instead of disabled) if the flag is enabled and the control set to readonly fixes #2479
1 parent 754d9dd commit fd7979a

47 files changed

Lines changed: 650 additions & 50 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/core/src/configDefault.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,11 @@ export const configDefault = {
4545
* [text] if asterisks in labels for required fields should be hidden
4646
*/
4747
hideRequiredAsterisk: false,
48+
49+
/**
50+
* When false (default), readonly is treated as disabled for backward compatibility.
51+
* When true, readonly and enabled are handled separately and exposed to renderers,
52+
* allowing UI libraries to distinguish between disabled and readonly states.
53+
*/
54+
separateReadonlyFromDisabled: false,
4855
};

packages/core/src/mappers/cell.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {
5353
JsonFormsCellRendererRegistryEntry,
5454
JsonFormsState,
5555
} from '../store';
56-
import { isInherentlyEnabled } from './util';
56+
import { isInherentlyEnabled, isInherentlyReadonly } from './util';
5757

5858
export interface OwnPropsOfCell extends OwnPropsOfControl {
5959
data?: any;
@@ -126,10 +126,14 @@ export const mapStateToCellProps = (
126126
* table renderer, determines whether a cell is enabled and should hand
127127
* over the prop themselves. If that prop was given, we prefer it over
128128
* anything else to save evaluation effort (except for the global readonly
129-
* flag). For example it would be quite expensive to evaluate the same ui schema
129+
* flag when separateReadonlyFromDisabled is disabled).
130+
* For example it would be quite expensive to evaluate the same ui schema
130131
* rule again and again for each cell of a table. */
131132
let enabled;
132-
if (state.jsonforms.readonly === true) {
133+
if (
134+
!config?.separateReadonlyFromDisabled &&
135+
state.jsonforms.readonly === true
136+
) {
133137
enabled = false;
134138
} else if (typeof ownProps.enabled === 'boolean') {
135139
enabled = ownProps.enabled;
@@ -144,6 +148,22 @@ export const mapStateToCellProps = (
144148
);
145149
}
146150

151+
/* Similar to enabled, we take a shortcut for readonly state. The parent
152+
* renderer can pass the readonly prop directly if it has already computed it,
153+
* saving re-evaluation for each cell. */
154+
let readonly;
155+
if (typeof ownProps.readonly === 'boolean') {
156+
readonly = ownProps.readonly;
157+
} else {
158+
readonly = isInherentlyReadonly(
159+
state,
160+
ownProps,
161+
uischema,
162+
schema || rootSchema,
163+
rootData,
164+
config
165+
);
166+
}
147167
const t = getTranslator()(state);
148168
const te = getErrorTranslator()(state);
149169
const errors = getCombinedErrorMessage(
@@ -160,6 +180,7 @@ export const mapStateToCellProps = (
160180
data: Resolve.data(rootData, path),
161181
visible,
162182
enabled,
183+
readonly,
163184
id,
164185
path,
165186
errors,

packages/core/src/mappers/renderer.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ import {
8383
getUISchemas,
8484
getUiSchema,
8585
} from '../store';
86-
import { isInherentlyEnabled } from './util';
86+
import { isInherentlyEnabled, isInherentlyReadonly } from './util';
8787
import { CombinatorKeyword } from './combinators';
8888
import isEqual from 'lodash/isEqual';
8989

@@ -375,6 +375,10 @@ export interface OwnPropsOfRenderer {
375375
* Whether the rendered element should be enabled.
376376
*/
377377
enabled?: boolean;
378+
/**
379+
* Whether the rendered element should be readonly.
380+
*/
381+
readonly?: boolean;
378382
/**
379383
* Whether the rendered element should be visible.
380384
*/
@@ -440,6 +444,12 @@ export interface StatePropsOfRenderer {
440444
* Whether the rendered element should be enabled.
441445
*/
442446
enabled: boolean;
447+
448+
/**
449+
* Whether the rendered element should be readonly.
450+
*/
451+
readonly?: boolean;
452+
443453
/**
444454
* Whether the rendered element should be visible.
445455
*/
@@ -614,6 +624,14 @@ export const mapStateToControlProps = (
614624
rootData,
615625
config
616626
);
627+
const readonly: boolean = isInherentlyReadonly(
628+
state,
629+
ownProps,
630+
uischema,
631+
resolvedSchema || rootSchema,
632+
rootData,
633+
config
634+
);
617635

618636
const schema = resolvedSchema ?? rootSchema;
619637
const t = getTranslator()(state);
@@ -646,6 +664,7 @@ export const mapStateToControlProps = (
646664
label: i18nLabel,
647665
visible,
648666
enabled,
667+
readonly,
649668
id,
650669
path,
651670
required,
@@ -1062,6 +1081,14 @@ export const mapStateToLayoutProps = (
10621081
rootData,
10631082
config
10641083
);
1084+
const readonly: boolean = isInherentlyReadonly(
1085+
state,
1086+
ownProps,
1087+
uischema,
1088+
undefined, // layouts have no associated schema
1089+
rootData,
1090+
config
1091+
);
10651092

10661093
// some layouts have labels which might need to be translated
10671094
const t = getTranslator()(state);
@@ -1075,6 +1102,7 @@ export const mapStateToLayoutProps = (
10751102
cells: ownProps.cells || getCells(state),
10761103
visible,
10771104
enabled,
1105+
readonly,
10781106
path: ownProps.path,
10791107
data,
10801108
uischema: ownProps.uischema,

packages/core/src/mappers/util.ts

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { JsonSchema, UISchemaElement } from '../models';
22
import { JsonFormsState, getAjv } from '../store';
3-
import { hasEnableRule, isEnabled } from '../util';
3+
import { hasEnableRule, hasReadonlyRule, isEnabled, isReadonly } from '../util';
44

55
/**
66
* Indicates whether the given `uischema` element shall be enabled or disabled.
7-
* Checks the global readonly flag, uischema rule, uischema options (including the config),
7+
* Checks the global readonly flag (unless separateReadonlyFromDisabled is enabled), uischema rule, uischema options (including the config),
88
* the schema and the enablement indicator of the parent.
99
*/
1010
export const isInherentlyEnabled = (
@@ -15,29 +15,80 @@ export const isInherentlyEnabled = (
1515
rootData: any,
1616
config: any
1717
) => {
18-
if (state?.jsonforms?.readonly) {
18+
if (!config?.separateReadonlyFromDisabled && state?.jsonforms?.readonly) {
1919
return false;
2020
}
2121
if (uischema && hasEnableRule(uischema)) {
2222
return isEnabled(uischema, rootData, ownProps?.path, getAjv(state), config);
2323
}
24+
if (!config?.separateReadonlyFromDisabled) {
25+
if (typeof uischema?.options?.readonly === 'boolean') {
26+
return !uischema.options.readonly;
27+
}
28+
if (typeof uischema?.options?.readOnly === 'boolean') {
29+
return !uischema.options.readOnly;
30+
}
31+
if (typeof config?.readonly === 'boolean') {
32+
return !config.readonly;
33+
}
34+
if (typeof config?.readOnly === 'boolean') {
35+
return !config.readOnly;
36+
}
37+
if (schema?.readOnly === true) {
38+
return false;
39+
}
40+
}
41+
if (typeof ownProps?.enabled === 'boolean') {
42+
return ownProps.enabled;
43+
}
44+
return true;
45+
};
46+
47+
/**
48+
* Indicates whether the given `uischema` element shall be readonly or writable.
49+
* Checks the global readonly flag, uischema rule, uischema options (including the config),
50+
* the schema and the readonly indicator of the parent.
51+
*/
52+
export const isInherentlyReadonly = (
53+
state: JsonFormsState,
54+
ownProps: any,
55+
uischema: UISchemaElement,
56+
schema: (JsonSchema & { readOnly?: boolean }) | undefined,
57+
rootData: any,
58+
config: any
59+
) => {
60+
if (state?.jsonforms?.readonly) {
61+
return true;
62+
}
63+
64+
if (uischema && hasReadonlyRule(uischema)) {
65+
return isReadonly(
66+
uischema,
67+
rootData,
68+
ownProps?.path,
69+
getAjv(state),
70+
config
71+
);
72+
}
73+
2474
if (typeof uischema?.options?.readonly === 'boolean') {
25-
return !uischema.options.readonly;
75+
return uischema.options.readonly;
2676
}
2777
if (typeof uischema?.options?.readOnly === 'boolean') {
28-
return !uischema.options.readOnly;
78+
return uischema.options.readOnly;
2979
}
3080
if (typeof config?.readonly === 'boolean') {
31-
return !config.readonly;
81+
return config.readonly;
3282
}
3383
if (typeof config?.readOnly === 'boolean') {
34-
return !config.readOnly;
84+
return config.readOnly;
3585
}
3686
if (schema?.readOnly === true) {
37-
return false;
87+
return true;
3888
}
39-
if (typeof ownProps?.enabled === 'boolean') {
40-
return ownProps.enabled;
89+
if (typeof ownProps?.readonly === 'boolean') {
90+
return ownProps.readonly;
4191
}
42-
return true;
92+
93+
return false;
4394
};

packages/core/src/models/uischema.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ export enum RuleEffect {
109109
* Effect that disables the associated element.
110110
*/
111111
DISABLE = 'DISABLE',
112+
/**
113+
* Effect that makes the associated element read-only
114+
* (interaction allowed, value cannot be changed).
115+
*/
116+
READONLY = 'READONLY',
117+
/**
118+
* Effect that makes the associated element writable.
119+
*/
120+
WRITABLE = 'WRITABLE',
112121
}
113122

114123
/**

packages/core/src/util/runtime.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,26 @@ export const evalEnablement = (
158158
}
159159
};
160160

161+
export const evalReadonly = (
162+
uischema: UISchemaElement,
163+
data: any,
164+
path: string = undefined,
165+
ajv: Ajv,
166+
config: unknown
167+
): boolean => {
168+
const fulfilled = isRuleFulfilled(uischema, data, path, ajv, config);
169+
170+
switch (uischema.rule.effect) {
171+
case RuleEffect.WRITABLE:
172+
return !fulfilled;
173+
case RuleEffect.READONLY:
174+
return fulfilled;
175+
// writable by default
176+
default:
177+
return false;
178+
}
179+
};
180+
161181
export const hasShowRule = (uischema: UISchemaElement): boolean => {
162182
if (
163183
uischema.rule &&
@@ -180,6 +200,17 @@ export const hasEnableRule = (uischema: UISchemaElement): boolean => {
180200
return false;
181201
};
182202

203+
export const hasReadonlyRule = (uischema: UISchemaElement): boolean => {
204+
if (
205+
uischema.rule &&
206+
(uischema.rule.effect === RuleEffect.READONLY ||
207+
uischema.rule.effect === RuleEffect.WRITABLE)
208+
) {
209+
return true;
210+
}
211+
return false;
212+
};
213+
183214
export const isVisible = (
184215
uischema: UISchemaElement,
185216
data: any,
@@ -207,3 +238,17 @@ export const isEnabled = (
207238

208239
return true;
209240
};
241+
242+
export const isReadonly = (
243+
uischema: UISchemaElement,
244+
data: any,
245+
path: string = undefined,
246+
ajv: Ajv,
247+
config: unknown
248+
): boolean => {
249+
if (uischema.rule) {
250+
return evalReadonly(uischema, data, path, ajv, config);
251+
}
252+
253+
return false;
254+
};

packages/core/test/generators/schema.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ test('default schema generation array types', (t) => {
113113
test.failing('default schema generation tuple array types', (t) => {
114114
const instance: any = { tupleArray: [3.14, 'PI'] };
115115
const schema = generateJsonSchema(instance);
116-
// FIXME: This assumption is the correct one, but we crteate a oneOf in this case
116+
// FIXME: This assumption is the correct one, but we create a oneOf in this case
117117
t.deepEqual(schema, {
118118
type: 'object',
119119
properties: {

0 commit comments

Comments
 (0)