diff --git a/modules/json_form_widget/css/style.css b/modules/json_form_widget/css/style.css new file mode 100644 index 0000000000..0b4100578c --- /dev/null +++ b/modules/json_form_widget/css/style.css @@ -0,0 +1,10 @@ +/* Target any fieldset with the error class */ +fieldset.error { + border-width: var(--input--error-border-size) !important; + border-color: var(--input--error-border-color) !important; +} + +/* Target any fieldset legend when it has an error */ +fieldset.error > legend > span.fieldset__label { + color: var(--input--error-color) !important; +} diff --git a/modules/json_form_widget/json_form_widget.libraries.yml b/modules/json_form_widget/json_form_widget.libraries.yml new file mode 100644 index 0000000000..491d7bdb5a --- /dev/null +++ b/modules/json_form_widget/json_form_widget.libraries.yml @@ -0,0 +1,4 @@ +style: + css: + theme: + css/style.css: {} diff --git a/modules/json_form_widget/json_form_widget.module b/modules/json_form_widget/json_form_widget.module index e7edcef7cb..ed46853a60 100644 --- a/modules/json_form_widget/json_form_widget.module +++ b/modules/json_form_widget/json_form_widget.module @@ -36,9 +36,19 @@ function json_form_widget_form_alter(&$form, FormStateInterface $form_state, $fo } if ($form_state->get('has_json_form_widget')) { $form['actions']['submit']['#submit'][] = [UploadOrLink::class, 'submit']; + + // Add a validation handler. + $form['#validate'][] = '_json_form_widget_post_constraint_validation'; } } +/** + * Validation handler. + */ +function _json_form_widget_post_constraint_validation(array &$form, FormStateInterface $form_state) { + \Drupal::service('json_form.form_post_validate')->onPostConstraintValidate($form, $form_state); +} + /** * Implements hook_entity_delete(). * diff --git a/modules/json_form_widget/json_form_widget.services.yml b/modules/json_form_widget/json_form_widget.services.yml index 3371a02aae..cf5506bc58 100644 --- a/modules/json_form_widget/json_form_widget.services.yml +++ b/modules/json_form_widget/json_form_widget.services.yml @@ -42,3 +42,5 @@ services: parent: logger.channel_base arguments: - 'json_form_widget' + json_form.form_post_validate: + class: \Drupal\json_form_widget\FormPostValidate diff --git a/modules/json_form_widget/src/FormPostValidate.php b/modules/json_form_widget/src/FormPostValidate.php new file mode 100644 index 0000000000..d406d6afe2 --- /dev/null +++ b/modules/json_form_widget/src/FormPostValidate.php @@ -0,0 +1,46 @@ +getErrors())) { + return; + } + + $form_state->clearErrors(); + + $form['#attached']['library'][] = 'json_form_widget/style'; + + foreach ($errors as $error) { + $message = $error->__toString(); + $field_name_json = $error->getArguments()['json_field_pointer']; + $field_name = json_decode($field_name_json, TRUE); + + $full_path = ['field_json_metadata', 'widget', 0, 'value']; + $full_path = array_merge($full_path, $field_name); + + $element = &NestedArray::getValue($form, $full_path, $key_exists); + + if ($key_exists && is_array($element)) { + $form_state->setError($element, $message); + } + } + } + +} \ No newline at end of file diff --git a/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidgetBase.php b/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidgetBase.php index 0e255e44b3..12706e7353 100644 --- a/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidgetBase.php +++ b/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidgetBase.php @@ -103,10 +103,41 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // Attempt to build the form. $json_form = $this->builder->getJsonForm($default_data, $form_state); if ($json_form) { - return ['value' => $json_form]; + // Add entity_builders callback - runs AFTER entity validation + //$element['#element_validate'][] = [$this, 'lateValidationHandler']; + $element['value'] = $json_form; + return $element; } } +/** + * Late validation handler. + */ +public static function lateValidationHandler(array &$form, FormStateInterface $form_state) { + if ($form_state->hasAnyErrors()) { + return; + } + + // Get the entity being validated + $form_object = $form_state->getFormObject(); + if (method_exists($form_object, 'getEntity')) { + $entity = $form_object->getEntity(); + + // Manually validate the entity constraints + $violations = $entity->validate(); + + // If there are violations, process them with your service + if (count($violations) > 0) { + $field_name = $form_state->get('json_form_widget_field'); + $element = $form[$field_name]['widget'][0] ?? null; + + if ($element) { + \Drupal::service('json_form.form_post_validate')->onPostConstraintValidate($element, $form, $form_state); + } + } + } +} + /** * {@inheritdoc} */ diff --git a/modules/metastore/src/Plugin/Validation/Constraint/ProperJsonValidator.php b/modules/metastore/src/Plugin/Validation/Constraint/ProperJsonValidator.php index e1abc8c699..fb806a7b22 100644 --- a/modules/metastore/src/Plugin/Validation/Constraint/ProperJsonValidator.php +++ b/modules/metastore/src/Plugin/Validation/Constraint/ProperJsonValidator.php @@ -107,7 +107,7 @@ private function doValidate(string $schema_id, $item): array { $this->validMetadataFactory->get($item->value, $schema_id); } catch (ValidationException $e) { - $errors = $this->getValidationErrorsMessages($e->getResult()->getErrors()); + $errors = $e->getResult()->getErrors(); } catch (InvalidArgumentException $e) { $errors[] = $e->getMessage(); @@ -116,31 +116,86 @@ private function doValidate(string $schema_id, $item): array { } /** - * Presents errors. - * - * @param array $errors - * Validation errors array. - * - * @return array - * Presented errors array. + * Add Violations with field context. */ - private function getValidationErrorsMessages(array $errors): array { - $presented = $this->presenter->present(...$errors); - return array_map( - function ($presented_error) { - return $presented_error->message(); - }, - $presented - ); + private function addViolations($errors) { + foreach ($errors as $error) { + // Extract field information from error pointer + $field_name = $this->extractFieldFromPointer($error); + $message = is_array($error) ? $error['message'] : $this->presenter->present($error)[0]->message(); + + // Add violation with field context + $violation = $this->context->buildViolation($message); + + // Store field information in the violation for later use + if ($field_name) { + $violation->setParameter('json_field_pointer', json_encode($field_name)); + } + + $violation->addViolation(); + } } /** - * Add Violations. + * Extract field name from JSON Schema error pointer. */ - private function addViolations($errors) { - foreach ($errors as $error) { - $this->context->addViolation($error); + private function extractFieldFromPointer($error) { + if (!is_object($error)) { + return null; + } + + // Handle required field errors - field name is in keywordArgs['missing'] + if (method_exists($error, 'keywordArgs')) { + $keywordArgs = $error->keywordArgs(); + if (isset($keywordArgs['missing'])) { + return [$keywordArgs['missing']]; + } + } + + // Handle other validation errors - field name is in dataPointer + if (method_exists($error, 'dataPointer')) { + $pointer = $error->dataPointer(); + if (is_array($pointer) && !empty($pointer)) { + // If dataPointer contains just one index, return it as an array + if (count($pointer) === 1) { + return $pointer; + } + + // For array fields with multiple indices, we need to add the field name both before and after numeric indices + $processed_pointer = []; + + foreach ($pointer as $index => $part) { + // If this is the first part and it's a field name, add it twice + if ($index === 0 && !is_numeric($part)) { + $processed_pointer[] = $part; // First occurrence + $processed_pointer[] = $part; // Second occurrence for array structure + } + // If this is a numeric index, add it and then add the field name after + elseif (is_numeric($part)) { + $processed_pointer[] = $part; + // Find the field name (first non-numeric part) + $field_name = null; + foreach ($pointer as $p) { + if (!is_numeric($p)) { + $field_name = $p; + break; + } + } + if ($field_name) { + $processed_pointer[] = $field_name; + } + } + // For other parts (like 'privateEmail'), just add them + else { + $processed_pointer[] = $part; + } + } + + return $processed_pointer; + } } + + return null; } } diff --git a/schema/collections/dataset.json b/schema/collections/dataset.json index e740307be1..ac2a0149eb 100644 --- a/schema/collections/dataset.json +++ b/schema/collections/dataset.json @@ -9,7 +9,8 @@ "identifier", "accessLevel", "modified", - "keyword" + "keyword", + "creators" ], "properties": { "@type": { @@ -22,7 +23,80 @@ "title": "Title", "description": "Human-readable name of the asset. Should be in plain English and include sufficient detail to facilitate search and discovery.", "type": "string", - "minLength": 1 + "minLength": 10 + }, + "number": { + "title": "Number", + "description": "Human-readable name of the asset. Should be in plain English and include sufficient detail to facilitate search and discovery.", + "type": "integer", + "minimum": 10 + }, + "creators": { + "title": "Author Information", + "type": "array", + "items": { + "title": "Author", + "type": "object", + "properties": { + "givenName": { + "title": "First Name", + "type": "string" + }, + "middleName": { + "title": "Middle Name", + "type": "string" + }, + "familyName": { + "title": "Last Name", + "type": "string" + }, + "privateEmail": { + "title": "Email", + "format": "email", + "type": "string", + "minLength": 1 + } + }, + "required": [ + "givenName", + "familyName", + "privateEmail" + ] + }, + "minItems": 2 + }, + "creators-not-required": { + "title": "Author Information NOT", + "type": "array", + "items": { + "title": "Author", + "type": "object", + "properties": { + "givenName": { + "title": "First Name", + "type": "string" + }, + "middleName": { + "title": "Middle Name", + "type": "string" + }, + "familyName": { + "title": "Last Name", + "type": "string" + }, + "privateEmail": { + "title": "Email", + "format": "email", + "type": "string", + "minLength": 1 + } + }, + "required": [ + "givenName", + "familyName", + "privateEmail" + ] + } }, "identifier": { "title": "Unique Identifier",