@abgov/jsonforms-components 2.36.2 → 2.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.esm.js CHANGED
@@ -13022,71 +13022,191 @@ function resolveScope(scope, data) {
13022
13022
  const cleaned = scope.replace(/^#\/(properties\/)?/, '');
13023
13023
  const parts = cleaned.split('/').filter(Boolean);
13024
13024
  let cur = data;
13025
- for (const p of parts) {
13026
- if (cur == null || typeof cur !== 'object') return undefined;
13027
- cur = cur[p];
13025
+ for (const part of parts) {
13026
+ if (cur == null) return undefined;
13027
+ if (Array.isArray(cur) && /^\d+$/.test(part)) {
13028
+ cur = cur[Number(part)];
13029
+ } else if (typeof cur === 'object') {
13030
+ if (part === 'properties') continue;
13031
+ cur = cur[part];
13032
+ } else {
13033
+ return undefined;
13034
+ }
13028
13035
  }
13029
13036
  return cur;
13030
13037
  }
13031
- function sumColumn(scope, data) {
13032
- if (!scope || typeof scope !== 'string') return undefined;
13033
- // normalize scope: remove leading "#/" and optional "properties/"
13038
+ function evaluateSum(scope, data) {
13039
+ if (!scope || typeof scope !== 'string') {
13040
+ return {
13041
+ value: undefined,
13042
+ error: 'SUM() requires a JSON pointer argument.'
13043
+ };
13044
+ }
13034
13045
  const cleaned = scope.replace(/^#\/(properties\/)?/, '');
13035
13046
  const parts = cleaned.split('/').filter(Boolean);
13036
- if (parts.length < 2) return undefined;
13037
- const colKey = parts[parts.length - 1];
13047
+ if (parts.length < 2) {
13048
+ return {
13049
+ value: undefined,
13050
+ error: `SUM() pointer "${scope}" must include an array and field name.`
13051
+ };
13052
+ }
13053
+ const fieldKey = parts[parts.length - 1];
13038
13054
  const arrPath = parts.slice(0, -1);
13039
13055
  let cur = data;
13040
- for (const p of arrPath) {
13041
- if (cur == null) return undefined;
13042
- // handle numeric indices for arrays
13043
- if (Array.isArray(cur) && /^\d+$/.test(p)) {
13044
- cur = cur[Number(p)];
13056
+ for (const segment of arrPath) {
13057
+ if (cur == null) {
13058
+ cur = undefined;
13059
+ break;
13060
+ }
13061
+ if (Array.isArray(cur) && /^\d+$/.test(segment)) {
13062
+ cur = cur[Number(segment)];
13063
+ } else if (typeof cur === 'object') {
13064
+ if (segment === 'properties') continue;
13065
+ cur = cur[segment];
13045
13066
  } else {
13046
- cur = cur[p];
13067
+ cur = undefined;
13068
+ break;
13047
13069
  }
13048
13070
  }
13049
- if (!Array.isArray(cur)) return undefined;
13071
+ if (cur == null) {
13072
+ return {
13073
+ value: undefined,
13074
+ error: `Please provide values for: ${scope}`
13075
+ };
13076
+ }
13077
+ if (!Array.isArray(cur)) {
13078
+ return {
13079
+ value: undefined,
13080
+ error: `SUM() pointer "${scope}" does not resolve to an array.`
13081
+ };
13082
+ }
13083
+ const missingIndices = [];
13084
+ const invalidIndices = [];
13050
13085
  let sum = 0;
13051
- for (const row of cur) {
13052
- if (row == null) return undefined;
13053
- const v = row[colKey];
13054
- if (typeof v !== 'number') return undefined;
13055
- sum += v;
13086
+ for (let i = 0; i < cur.length; i++) {
13087
+ const row = cur[i];
13088
+ if (row == null || typeof row !== 'object') {
13089
+ missingIndices.push(i);
13090
+ continue;
13091
+ }
13092
+ const v = row[fieldKey];
13093
+ if (v === undefined || v === null || v === '') {
13094
+ missingIndices.push(i);
13095
+ } else if (typeof v !== 'number' || Number.isNaN(v)) {
13096
+ invalidIndices.push(i);
13097
+ } else {
13098
+ sum += v;
13099
+ }
13100
+ }
13101
+ if (invalidIndices.length > 0) {
13102
+ return {
13103
+ value: undefined,
13104
+ error: `Expected numeric values for: ${scope} at rows [${invalidIndices.join(', ')}]`
13105
+ };
13056
13106
  }
13057
- return sum;
13107
+ if (missingIndices.length === cur.length) {
13108
+ return {
13109
+ value: undefined,
13110
+ error: `Please provide values for: ${scope}`
13111
+ };
13112
+ }
13113
+ return {
13114
+ value: sum,
13115
+ error: missingIndices.length ? `Some rows are missing values for: ${scope}` : undefined
13116
+ };
13058
13117
  }
13118
+ /**
13119
+ * General expression evaluation.
13120
+ * - Supports SUM(#/properties/arr/c3)
13121
+ * - Supports arithmetic with JSON pointers:
13122
+ * "#/properties/x * #/properties/y + #/properties/z"
13123
+ *
13124
+ * Returns { value, error } and NEVER throws.
13125
+ */
13059
13126
  function evaluateExpression(expression, data) {
13060
- if (!expression || typeof expression !== 'string') return undefined;
13127
+ if (!expression || typeof expression !== 'string') {
13128
+ return {
13129
+ value: undefined,
13130
+ error: undefined
13131
+ };
13132
+ }
13061
13133
  const trimmed = expression.trim();
13134
+ if (!trimmed) return {
13135
+ value: undefined,
13136
+ error: undefined
13137
+ };
13062
13138
  const sumMatch = trimmed.match(/^SUM\(\s*['"]?(.+?)['"]?\s*\)$/i);
13063
13139
  if (sumMatch) {
13064
- return sumColumn(sumMatch[1], data);
13140
+ const pointer = sumMatch[1];
13141
+ return evaluateSum(pointer, data);
13065
13142
  }
13066
- // Find unique scope tokens like #/properties/x or #/arr/0/c3
13067
13143
  const scopeRegex = /#\/(?:properties\/)?[^\s"')]+/g;
13068
13144
  const matches = trimmed.match(scopeRegex) || [];
13069
13145
  const uniqueScopes = Array.from(new Set(matches));
13070
- // Prepare variable mapping for expr-eval
13146
+ let exprForParse = trimmed;
13147
+ const scopeToVar = new Map();
13148
+ uniqueScopes.forEach((scope, index) => {
13149
+ const varName = `v${index}`;
13150
+ scopeToVar.set(scope, varName);
13151
+ const pattern = new RegExp(`['"]?${escapeRegExp(scope)}['"]?`, 'g');
13152
+ exprForParse = exprForParse.replace(pattern, varName);
13153
+ });
13154
+ let parsed;
13155
+ try {
13156
+ const parser = new Parser();
13157
+ parsed = parser.parse(exprForParse);
13158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13159
+ } catch (e) {
13160
+ return {
13161
+ value: undefined,
13162
+ error: 'Invalid expression syntax'
13163
+ };
13164
+ }
13071
13165
  const vars = {};
13072
- let expr = trimmed;
13073
- for (let i = 0; i < uniqueScopes.length; i++) {
13074
- const scope = uniqueScopes[i];
13166
+ const missingScopes = [];
13167
+ const invalidTypeScopes = [];
13168
+ for (const scope of uniqueScopes) {
13075
13169
  const val = resolveScope(scope, data);
13076
- if (val === undefined || typeof val !== 'number') return undefined;
13077
- const varName = `v${i}`;
13170
+ if (val === undefined || val === null || val === '') {
13171
+ missingScopes.push(scope);
13172
+ continue;
13173
+ }
13174
+ if (typeof val !== 'number' || Number.isNaN(val)) {
13175
+ invalidTypeScopes.push(scope);
13176
+ continue;
13177
+ }
13178
+ const varName = scopeToVar.get(scope);
13078
13179
  vars[varName] = val;
13079
- const pattern = new RegExp(`['"]?${escapeRegExp(scope)}['"]?`, 'g');
13080
- expr = expr.replace(pattern, varName);
13081
13180
  }
13082
- // Evaluate with expr-eval parser
13181
+ if (invalidTypeScopes.length > 0) {
13182
+ return {
13183
+ value: undefined,
13184
+ error: `Expected numeric values for: ${invalidTypeScopes.join(', ')}`
13185
+ };
13186
+ }
13187
+ if (missingScopes.length > 0) {
13188
+ return {
13189
+ value: undefined,
13190
+ error: `Please provide values for: ${missingScopes.join(', ')}`
13191
+ };
13192
+ }
13083
13193
  try {
13084
- const parser = new Parser();
13085
- const parsed = parser.parse(expr);
13086
13194
  const result = parsed.evaluate(vars);
13087
- return typeof result === 'number' && Number.isFinite(result) ? result : undefined;
13195
+ if (typeof result === 'number' && Number.isFinite(result)) {
13196
+ return {
13197
+ value: result,
13198
+ error: undefined
13199
+ };
13200
+ }
13201
+ return {
13202
+ value: undefined,
13203
+ error: 'Expression did not produce a numeric result'
13204
+ };
13088
13205
  } catch (_a) {
13089
- return undefined;
13206
+ return {
13207
+ value: undefined,
13208
+ error: 'Error while evaluating expression'
13209
+ };
13090
13210
  }
13091
13211
  }
13092
13212
 
@@ -13094,29 +13214,44 @@ const GoACalculation = props => {
13094
13214
  var _a;
13095
13215
  const {
13096
13216
  uischema,
13097
- data,
13098
13217
  schema,
13099
13218
  path,
13100
13219
  id,
13101
13220
  visible,
13102
13221
  handleChange
13103
13222
  } = props;
13104
- const expression = schema === null || schema === void 0 ? void 0 : schema.description;
13105
13223
  const label = typeof (uischema === null || uischema === void 0 ? void 0 : uischema.label) === 'string' ? uischema.label : undefined;
13224
+ const expression = schema === null || schema === void 0 ? void 0 : schema.description;
13106
13225
  const {
13107
13226
  core
13108
13227
  } = useJsonForms();
13109
13228
  const rootData = (_a = core === null || core === void 0 ? void 0 : core.data) !== null && _a !== void 0 ? _a : {};
13110
- const computedValue = expression ? evaluateExpression(expression, rootData) : undefined;
13111
- React.useEffect(() => {
13229
+ const [hasInteracted, setHasInteracted] = useState(false);
13230
+ const prevDataRef = useRef(rootData);
13231
+ useEffect(() => {
13232
+ setHasInteracted(was => was ? true : true);
13233
+ }, [rootData]);
13234
+ useEffect(() => {
13235
+ if (prevDataRef.current !== rootData) {
13236
+ setHasInteracted(true);
13237
+ prevDataRef.current = rootData;
13238
+ }
13239
+ }, [rootData]);
13240
+ const {
13241
+ value: computedValue,
13242
+ error
13243
+ } = evaluateExpression(expression, rootData);
13244
+ useEffect(() => {
13112
13245
  if (computedValue !== undefined && typeof handleChange === 'function' && path) {
13113
13246
  handleChange(path, computedValue);
13114
13247
  }
13115
13248
  }, [computedValue, handleChange, path]);
13249
+ const showError = hasInteracted && !!error;
13116
13250
  return jsx(Visible, {
13117
13251
  visible: visible,
13118
13252
  children: jsx(GoAFormItem, {
13119
13253
  label: label,
13254
+ error: showError ? error : '',
13120
13255
  children: jsx(GoAInput, {
13121
13256
  name: `computed-input-${id}`,
13122
13257
  testId: `computed-input-${id}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/jsonforms-components",
3
- "version": "2.36.2",
3
+ "version": "2.37.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Government of Alberta - React renderers for JSON Forms based on the design system.",
6
6
  "repository": "https://github.com/GovAlta/adsp-monorepo",
@@ -1,3 +1,15 @@
1
+ export interface EvaluationResult {
2
+ value?: number;
3
+ error?: string;
4
+ }
1
5
  export declare function resolveScope(scope: string, data: unknown): unknown;
2
- export declare function sumColumn(scope: string, data: unknown): number | undefined;
3
- export declare function evaluateExpression(expression: string, data: unknown): number | undefined;
6
+ export declare function evaluateSum(scope: string | undefined, data: unknown): EvaluationResult;
7
+ /**
8
+ * General expression evaluation.
9
+ * - Supports SUM(#/properties/arr/c3)
10
+ * - Supports arithmetic with JSON pointers:
11
+ * "#/properties/x * #/properties/y + #/properties/z"
12
+ *
13
+ * Returns { value, error } and NEVER throws.
14
+ */
15
+ export declare function evaluateExpression(expression: string | undefined, data: unknown): EvaluationResult;