@hestia-earth/ui-components 0.43.11 → 0.43.13

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.
@@ -498,9 +498,22 @@ const guidePageId = (nodeType, errorMessage) => {
498
498
  };
499
499
  const mapErrorMessage = 'does not contain latitude and longitude';
500
500
  const allowedDataPathsLabels = Object.values(SchemaType);
501
+ const isInfrastructureInputTermType = (dataPath = '') => /infrastructure\[\d+\]\.inputs\[\d+\]\.term\.termType$/.test(dataPath);
501
502
  const idPatternError = (chars) => `You can only use the following characters for this field: ${chars.join(', ')}`;
502
503
  const noTillage = { id: 'noTillage', name: 'No tillage' };
503
504
  const pastureGrass = { id: 'pastureGrass', name: 'Pasture grass' };
505
+ /**
506
+ * Which distribution flagged a value as outside its confidence interval.
507
+ *
508
+ * The two mean very different things, so the message has to say which one fired: a `posterior` is
509
+ * fitted to HESTIA Cycle data for this country and product, while a `prior` is only the FAOSTAT
510
+ * national average — it stands in for any country/product too thin to have a posterior, so it is
511
+ * weak evidence rather than an alarm.
512
+ */
513
+ const distributionSourceMessage = {
514
+ prior: 'This was flagged against the FAOSTAT prior (national average), not HESTIA Cycle data for this product, so it is weak evidence.',
515
+ posterior: 'This was flagged against HESTIA Cycle data for this product in this country.'
516
+ };
504
517
  const isMigrationError = ({ params }) => params?.current && 'expected' in params;
505
518
  /**
506
519
  * Builds the error formatting functions for a given presentation (`renderer`) and link
@@ -519,6 +532,8 @@ const createErrorFormatter = ({ renderer: r, urls }) => {
519
532
  const glossaryTypeLink = (type, text = termTypeLabel(type)) => r.link(`${urls.glossaryBaseUrl()}?termType=${type}`, text);
520
533
  const termLink = ({ id, name }) => r.link(`${urls.baseUrl()}/term/${id}`, name || id);
521
534
  const listAsCode = (values) => (values ?? []).map(value => r.code(value)).join(', ');
535
+ const joinWithAnd = (values) => values.length > 1 ? `${values.slice(0, -1).join(', ')} and ${values[values.length - 1]}` : values.join('');
536
+ const listAsCodeWithAnd = (values) => joinWithAnd((values ?? []).map(value => r.code(value)));
522
537
  const joinValues = (values) => (values || [])
523
538
  .filter(Boolean)
524
539
  .map(value => r.code(value))
@@ -583,6 +598,18 @@ const createErrorFormatter = ({ renderer: r, urls }) => {
583
598
  ? `${r.underline(negation ? 'none' : 'one')} of the following must be valid: ${formatList(rule.anyOf.map(nested => formatRule(nested)))}`
584
599
  : '';
585
600
  const patternErrorKey = (pattern) => `should match pattern "${pattern}"`;
601
+ /**
602
+ * Names the distribution(s) behind the grouped warnings, as a trailing sentence.
603
+ *
604
+ * Returns an empty string when no error carries a `source`, so warnings raised before the
605
+ * validation service started sending it read exactly as they did before.
606
+ */
607
+ const distributionSources = (errors) => {
608
+ const sentences = unique(errors.map(({ params }) => params?.source))
609
+ .map(source => distributionSourceMessage[source])
610
+ .filter(Boolean);
611
+ return sentences.length ? `${sentences.join(r.lineBreak)}${r.lineBreak}` : '';
612
+ };
586
613
  // set message as empty to not display it
587
614
  const customErrorMessage = {
588
615
  'this field is required': ({ dataPath }, _errorCount, allErrors) => {
@@ -612,6 +639,9 @@ const createErrorFormatter = ({ renderer: r, urls }) => {
612
639
  },
613
640
  "should have required property '@id'": idNotFoundError,
614
641
  "should have required property 'id'": idNotFoundError,
642
+ "should have required property 'inputsCompleteness'": () => `You must add the ${schemaLink('Infrastructure#inputsCompleteness', 'inputsCompleteness')} field when you have specified inputs for the Site infrastructure.
643
+ This allows us to know whether the ${code('material')} completeness can be updated after copying the
644
+ infrastructure inputs to the Cycle.`,
615
645
  'should match exactly one schema in oneOf': () => '',
616
646
  'should match some schema in anyOf': () => '',
617
647
  'should match "then" schema': () => '',
@@ -709,16 +739,20 @@ const createErrorFormatter = ({ renderer: r, urls }) => {
709
739
  ].every(Boolean))
710
740
  : null;
711
741
  return [
712
- dataPath.endsWith('term.termType')
713
- ? [
714
- `For ${pluralize(paths[paths.length - 3].label, 0)}, we only allow terms from the following glossaries: ${listAsCode(params?.allowedValues)}.`,
715
- ifError
716
- ? ''
717
- : 'Please either change the Term you are using and ensure it is from one of the glossaries listed above, or follow the schema to identify where this Term can be added.'
718
- ]
719
- .filter(Boolean)
720
- .join(' ')
721
- : `Only the following ${values} are permitted: ${listAsCode(params?.allowedValues)}.`,
742
+ isInfrastructureInputTermType(dataPath)
743
+ ? `Only terms in the ${listAsCodeWithAnd(params?.allowedValues)} glossaries can be added as inputs to Site
744
+ infrastructure. Other inputs, e.g., ${code('fuel')}, should be specified on the Cycle.
745
+ You can use the ${schemaLink('Input#operation', 'operation')} field on inputs to specify what the input is for.`
746
+ : dataPath.endsWith('term.termType')
747
+ ? [
748
+ `For ${pluralize(paths[paths.length - 3].label, 0)}, we only allow terms from the following glossaries: ${listAsCode(params?.allowedValues)}.`,
749
+ ifError
750
+ ? ''
751
+ : 'Please either change the Term you are using and ensure it is from one of the glossaries listed above, or follow the schema to identify where this Term can be added.'
752
+ ]
753
+ .filter(Boolean)
754
+ .join(' ')
755
+ : `Only the following ${values} are permitted: ${listAsCode(params?.allowedValues)}.`,
722
756
  ifError?.schema.properties
723
757
  ? `This validation applies when the following conditions are met:${formatList(Object.entries(ifError.schema.properties).map(([key, value]) => formatRule(value, key)))}`
724
758
  : ''
@@ -893,7 +927,7 @@ const createErrorFormatter = ({ renderer: r, urls }) => {
893
927
  Please, use a term with termType ${glossaryTypeLink(TermTermType.landCover)} instead.
894
928
  This will help us gap-fill the amount of grass consumed by the herd.`,
895
929
  'the sum of all pastureGrass values must be 100': ({ params }) => `The sum of all ${termLink(pastureGrass)} values must be equal to ${params.expected}%.`,
896
- 'is outside confidence interval': (error, errorCount, allErrors) => `${unique((allErrors.length ? allErrors : [error]).flatMap(({ dataPath, params }) => params?.outliers?.map(value => `The ${dataPath.includes('product') ? 'product' : 'input'} value ${errorCount === 1 ? `${code(value)} ` : ''}is ${value > params?.min ? 'above' : 'below'} ${params?.threshold ? `${code(`${(params.threshold + (1 - params.threshold) / 2) * 100}%`)} of` : ''} all ${params?.group ? code(params.group) : code(params?.term?.name)} data in ${code(params?.country.name)} on the HESTIA platform.`))).join(r.lineBreak)}${r.lineBreak}
930
+ 'is outside confidence interval': (error, errorCount, allErrors) => `${unique((allErrors.length ? allErrors : [error]).flatMap(({ dataPath, params }) => params?.outliers?.map(value => `The ${dataPath.includes('product') ? 'product' : 'input'} value ${errorCount === 1 ? `${code(value)} ` : ''}is ${value > params?.min ? 'above' : 'below'} ${params?.threshold ? `${code(`${(params.threshold + (1 - params.threshold) / 2) * 100}%`)} of` : ''} all ${params?.group ? code(params.group) : code(params?.term?.name)} data in ${code(params?.country.name)} on the HESTIA platform.`))).join(r.lineBreak)}${r.lineBreak}${distributionSources(allErrors.length ? allErrors : [error])}
897
931
  Please double-check these values before submitting.`,
898
932
  'should be equal to boundary': ({ params }) => `The calculated ${schemaLink('Site#area', 'area')} from ${code('boundary')} is ${code(params?.expected)} ha.
899
933
  This may be an error if the boundary represents the area under cultivation.`,
@@ -1198,7 +1232,7 @@ const createErrorFormatter = ({ renderer: r, urls }) => {
1198
1232
  const field = message.split("'")[1].replace("'", '');
1199
1233
  const nodeType = dataPathLabel(error?.dataPath);
1200
1234
  return `You are missing the following required field: ${code(field)}. ${nodeType && nodeType.toLowerCase() !== 'completeness'
1201
- ? `Either: add ${code(field)}, or if you if you did not intend to add this ${nodeType}, please remove all fields on this ${nodeType}.`
1235
+ ? `Either: add ${code(field)}, or if you did not intend to add this ${nodeType}, please remove all fields on this ${nodeType}.`
1202
1236
  : ''}`.trim();
1203
1237
  };
1204
1238
  const parseDefaultMessage = (message, error) => r.capitalizeFirst(message?.startsWith('should have required property') ? requiredPropertyError(message, error) : message);