Skip to content

Commit cc0ab0e

Browse files
committed
feat: add separateReadonlyFromDisabled config option
Allows independent control of readonly and enabled states. Adds READONLY/WRITABLE rule effects. Backward compatible (default: false).
1 parent e642b3d commit cc0ab0e

Some content is hidden

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

45 files changed

+291
-36
lines changed

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/vue-vuetify/dev/components/ExampleSettings.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,22 @@ const layouts = appstoreLayouts.map((value: AppstoreLayouts) => ({
323323
</v-tooltip>
324324
</v-col>
325325
</v-row>
326+
<v-row>
327+
<v-col>
328+
<v-tooltip bottom>
329+
<template v-slot:activator="{ props }">
330+
<v-switch
331+
v-model="appStore.jsonforms.config.separateReadonlyFromDisabled"
332+
label="ReadonlyAware"
333+
v-bind="props"
334+
></v-switch>
335+
</template>
336+
When false, readonly is treated as disabled for backward
337+
compatibility. When true, readonly and enabled are handled
338+
separately
339+
</v-tooltip>
340+
</v-col>
341+
</v-row>
326342
<v-row>
327343
<v-col>
328344
<v-tooltip bottom>

packages/vue-vuetify/dev/store/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const appstore = reactive({
2626
trim: false,
2727
showUnfocusedDescription: false,
2828
hideRequiredAsterisk: true,
29+
separateReadonlyFromDisabled: true,
2930
collapseNewItems: false,
3031
breakHorizontal: false,
3132
initCollapsed: false,

0 commit comments

Comments
 (0)