From b3f39ea504e91aaed24b3e7807905e19ffb88e55 Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Fri, 15 Nov 2024 13:26:01 -0500 Subject: [PATCH 01/10] Making progress --- .../json_form_widget/json_form_widget.module | 2 +- modules/json_form_widget/src/ArrayHelper.php | 246 +++++++++++++++--- 2 files changed, 207 insertions(+), 41 deletions(-) diff --git a/modules/json_form_widget/json_form_widget.module b/modules/json_form_widget/json_form_widget.module index 444a8d4849..ccdef19e86 100644 --- a/modules/json_form_widget/json_form_widget.module +++ b/modules/json_form_widget/json_form_widget.module @@ -35,7 +35,7 @@ function json_form_widget_remove_one(array &$form, FormStateInterface $form_stat /** * Update count property by the given offset. * - * @param FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state. * @param int $offset * Offset to change count by. diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index 09b13e19fe..9796efa917 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -2,6 +2,7 @@ namespace Drupal\json_form_widget; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\DependencySerializationTrait; @@ -55,6 +56,10 @@ public function setBuilder(FieldTypeRouter $builder): void { $this->objectHelper->setBuilder($builder); } + // public function removeButtonCallback(array &$form, FormStateInterface $form_state): array { + + // } + /** * Update wrapper element of the triggering button after build. * @@ -96,39 +101,74 @@ public function addOrRemoveButtonCallback(array &$form, FormStateInterface $form * Handle form element for an array. */ public function handleArrayElement(array $definition, ?array $data, FormStateInterface $form_state, array $context): array { - // Extract field name from field definition and min items from field schema - // for later reference. + // Extract field name from field definition and min items from field schema. $field_name = $definition['name']; $min_items = $definition['schema']->minItems ?? 0; - // Build context name. + $context_name = self::buildContextName($context); - // Determine number of form items to generate. $item_count = $this->getItemCount($context_name, count($data ?? []), $min_items, $form_state); + $is_required = in_array($field_name, $this->builder->getSchema()->required ?? []); - // Determine if this field is required. - $required_fields = $this->builder->getSchema()->required ?? []; - $field_required = in_array($field_name, $required_fields); // Build the specified number of field item elements. - $field_properties = []; + $items = []; for ($i = 0; $i < $item_count; $i++) { - $property_required = $field_required && ($i < $min_items); - $field_properties[] = $this->buildArrayElement($definition, $data[$i] ?? NULL, $form_state, array_merge($context, [$i]), $property_required); + $property_required = $is_required && ($i < $min_items); + $items[] = $this->buildArrayElement($definition, $data[$i] ?? NULL, $form_state, array_merge($context, [$i]), $property_required); } + // $this->itemsAlter($items, $context_name, $item_count, $form_state); + // Build field element. return [ - '#type' => 'fieldset', - '#title' => ($definition['schema']->title ?? $field_name), - '#description' => ($definition['schema']->description ?? ''), + '#type' => 'fieldset', + '#title' => ($definition['schema']->title ?? $field_name), + '#description' => ($definition['schema']->description ?? ''), '#description_display' => 'before', - '#prefix' => '
', - '#suffix' => '
', - '#tree' => TRUE, - 'actions' => $this->buildActions($item_count, $min_items, $field_name, $context_name), - $field_name => $field_properties, + '#prefix' => '
', + '#suffix' => '
', + '#tree' => TRUE, + 'actions' => [ + '#type' => 'actions', + 'actions' => [ + 'add' => $this->buildAction($this->t('Add one'), 'addOne', $field_name, $context_name), + ], + ], + $field_name => $items, ]; } + /** + * Alter the array of elements based on a form action (e.g. remove). + * + * @param array &$items + * The array of form elements being added to the parent fieldset. + * @param string $context_name + * Field context to target. + * @param int $min_items + * Minimum number of items required. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + */ + protected function itemsAlter(array &$items, string $context_name, int $item_count, FormStateInterface $form_state): void { + $alter_index_property = self::buildAlterProperty($context_name); + $count_property = self::buildCountProperty($context_name); + + // $alter_index = $form_state->get($alter_index_property) ?? NULL; + // if ((!isset($alter_index) || !is_array($alter_index))) { + // $form_state->set($alter_index_property, array_keys($items)); + // return; + // } + // $altered_items = []; + // foreach ($alter_index as $index) { + // if (isset($items[$index])) { + // $altered_items[] = $items[$index]; + // } + // } + // // $items = $altered_items; + // $form_state->set($alter_index_property, array_keys($items)); + // $form_state->set($count_property, count($items)); + } + /** * Get the form items count for the given field. * @@ -200,46 +240,67 @@ public static function buildCountProperty(string $context_name): array { } /** - * Helper function to build form field actions. + * Build count property. + * + * @param string $context_name + * Field element context name. + * + * @return string[] + * Full count property array. */ - protected function buildActions(int $item_count, int $min_items, string $parent, string $context_name): array { - $actions = []; - - // Build add action. - $actions['add'] = $this->buildAction($this->t('Add one'), 'json_form_widget_add_one', $parent, $context_name); - // Build remove action if there are more than the minimum required elements - // in this field array. - if ($item_count > $min_items) { - $actions['remove'] = $this->buildAction($this->t('Remove one'), 'json_form_widget_remove_one', $parent, $context_name); - } - - return [ - '#type' => 'actions', - 'actions' => $actions, - ]; + public static function buildAlterProperty(string $context_name): array { + return ['json_form_widget_info', $context_name, 'alter']; } /** - * Helper function to get action. + * Helper function to build an action button. + * + * @param string $title + * Button title. + * @param string $method + * Button submit method; should be a static method from this class. + * @param string $parent + * The parent element for the action; usually the current field name. + * @param string $context_name + * The context name, output of ::buildContextName(). */ - protected function buildAction(string $title, string $function, string $parent, string $context_name): array { + protected function buildAction(string $title, string $method, string $parent, string $context_name): array { return [ '#type' => 'submit', '#name' => $context_name, '#value' => $title, - '#submit' => [$function], + '#submit' => [self::class . '::' . $method], '#ajax' => [ 'callback' => [$this, 'addOrRemoveButtonCallback'], - 'wrapper' => self::buildWrapperIdentifier($context_name), + 'wrapper' => self::buildWrapperIdentifier($parent), ], '#attributes' => [ 'data-parent' => $parent, - 'data-context' => $context_name, ], '#limit_validation_errors' => [], ]; } + /** + * Build the remove/reorder actions for a single element. + * + * @param string $parent + * Parent element name. + * @param string $context_name + * Data context. + * + * @return array{#type: string, remove: array} + * Actions render array. + */ + protected function buildElementActions(string $parent, string $context_name) { + return [ + '#type' => 'actions', + 'remove' => $this->buildAction($this->t('Remove'), 'remove', $parent, $context_name), + 'move_up' => $this->buildAction($this->t('Move Up'), 'moveUp', $parent, $context_name), + 'move_d' => $this->buildAction($this->t('Move Down'), 'moveDown', $parent, $context_name), + ]; + } + /** * Handle single element from array. * @@ -278,7 +339,112 @@ protected function buildComplexArrayElement(array $definition, $data, FormStateI 'name' => $definition['name'], 'schema' => $definition['schema']->items, ]; - return $this->objectHelper->handleObjectElement($subdefinition, $data, $form_state, $context, $this->builder); + $element = $this->objectHelper->handleObjectElement($subdefinition, $data, $form_state, $context, $this->builder); + $element[$definition['name']]['actions'] = $this->buildElementActions($definition['name'], self::buildContextName($context)); + return $element; + } + + public static function remove(array &$form, FormStateInterface $form_state) { + $button_element = $form_state->getTriggeringElement(); + $parent = $button_element['#attributes']['data-parent']; + $parents = $button_element['#parents']; + $element_index = str_replace("{$parent}-", '', $button_element['#name']); + $alter_property = self::buildAlterProperty($parent); + $count_property = self::buildCountProperty($parent); + + $items_alter_index = $form_state->get($alter_property) ?? []; + $user_input = $form_state->getUserInput(); + + // Remove the specific element from the alter index. + if (isset($items_alter_index[$element_index])) { + unset($items_alter_index[$element_index]); + $form_state->set($alter_property, array_values($items_alter_index)); + } + + // Update the user input to remove the specific element. + $key_exists = NULL; + // We actually want the parent container of all elements. Hopefully going + // back 4 levels will work in all situations. + array_splice($parents, -4); + $distributions = &NestedArray::getValue($user_input, $parents, $key_exists); + if ($key_exists) { + unset($distributions[$element_index]); + // Re-index the array to maintain proper keys. + $distributions = \array_values($distributions); + } + + $form_state->setUserInput($user_input); + + // Modify stored item count. The form rebuilds before the alter, so it needs + // to be one more than the current item count to avoid removing twice. + $item_count = count($items_alter_index); + $form_state->set($count_property, $item_count); + + $form_state->setRebuild(); + } + + public static function moveUp(array &$form, FormStateInterface $form_state) { + return static::moveElement($form_state, -1); + } + + public static function moveDown(array &$form, FormStateInterface $form_state) { + return static::moveElement($form_state, 1); + } + + protected static function moveElement(FormStateInterface $form_state, int $offset) { + $button_element = $form_state->getTriggeringElement(); + $parent = $button_element['#attributes']['data-parent']; + $parents = $button_element['#parents']; + $element_index = str_replace("{$parent}-", '', $button_element['#name']); + $alter_property = self::buildAlterProperty($parent); + + $items_alter_index = $form_state->get($alter_property) ?? []; + $user_input = $form_state->getUserInput(); + + // Move the specific element up in the alter index. + if (isset($items_alter_index[$element_index])) { + $moved_element = array_splice($items_alter_index, $element_index, 1); + array_splice($items_alter_index, $element_index + $offset, 0, $moved_element); + $form_state->set($alter_property, array_values($items_alter_index)); + } + + // Update the user input to change the order. + $key_exists = NULL; + // We actually want the parent container of all elements. Hopefully going + // back 4 levels will work in all situations. + array_splice($parents, -4); + $distributions = &NestedArray::getValue($user_input, $parents, $key_exists); + if ($key_exists) { + $moved_element = array_splice($distributions, $element_index, 1); + array_splice($distributions, $element_index + $offset, 0, $moved_element); + // Re-index the array to maintain proper keys. + $distributions = \array_values($distributions); + } + + $form_state->setUserInput($user_input); + $form_state->setRebuild(); + } + + /** + * Update count property by the given offset. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + */ + public static function addOne(array &$form, FormStateInterface $form_state) { + $button_element = $form_state->getTriggeringElement(); + $alter_property = self::buildAlterProperty($button_element['#name']); + $items_alter_index = $form_state->get($alter_property) ?? []; + $items_alter_index[] = count($items_alter_index); + + $count_property = static::buildCountProperty($button_element['#name']); + // Modify stored item count. + $item_count = $form_state->get($count_property) ?? 0; + $item_count++; + $form_state->set($count_property, $item_count); + + $form_state->set($alter_property, $items_alter_index); + $form_state->setRebuild(); } } From 1364fb05c28c2f1c1985a2221b9278ae4cd1439f Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Fri, 15 Nov 2024 16:42:27 -0500 Subject: [PATCH 02/10] Just use userinput --- modules/json_form_widget/src/ArrayHelper.php | 55 +------------------- 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index 9796efa917..7d2c1d60ad 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -116,8 +116,6 @@ public function handleArrayElement(array $definition, ?array $data, FormStateInt $items[] = $this->buildArrayElement($definition, $data[$i] ?? NULL, $form_state, array_merge($context, [$i]), $property_required); } - // $this->itemsAlter($items, $context_name, $item_count, $form_state); - // Build field element. return [ '#type' => 'fieldset', @@ -137,38 +135,6 @@ public function handleArrayElement(array $definition, ?array $data, FormStateInt ]; } - /** - * Alter the array of elements based on a form action (e.g. remove). - * - * @param array &$items - * The array of form elements being added to the parent fieldset. - * @param string $context_name - * Field context to target. - * @param int $min_items - * Minimum number of items required. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Form state. - */ - protected function itemsAlter(array &$items, string $context_name, int $item_count, FormStateInterface $form_state): void { - $alter_index_property = self::buildAlterProperty($context_name); - $count_property = self::buildCountProperty($context_name); - - // $alter_index = $form_state->get($alter_index_property) ?? NULL; - // if ((!isset($alter_index) || !is_array($alter_index))) { - // $form_state->set($alter_index_property, array_keys($items)); - // return; - // } - // $altered_items = []; - // foreach ($alter_index as $index) { - // if (isset($items[$index])) { - // $altered_items[] = $items[$index]; - // } - // } - // // $items = $altered_items; - // $form_state->set($alter_index_property, array_keys($items)); - // $form_state->set($count_property, count($items)); - } - /** * Get the form items count for the given field. * @@ -349,18 +315,9 @@ public static function remove(array &$form, FormStateInterface $form_state) { $parent = $button_element['#attributes']['data-parent']; $parents = $button_element['#parents']; $element_index = str_replace("{$parent}-", '', $button_element['#name']); - $alter_property = self::buildAlterProperty($parent); $count_property = self::buildCountProperty($parent); - - $items_alter_index = $form_state->get($alter_property) ?? []; $user_input = $form_state->getUserInput(); - // Remove the specific element from the alter index. - if (isset($items_alter_index[$element_index])) { - unset($items_alter_index[$element_index]); - $form_state->set($alter_property, array_values($items_alter_index)); - } - // Update the user input to remove the specific element. $key_exists = NULL; // We actually want the parent container of all elements. Hopefully going @@ -377,7 +334,7 @@ public static function remove(array &$form, FormStateInterface $form_state) { // Modify stored item count. The form rebuilds before the alter, so it needs // to be one more than the current item count to avoid removing twice. - $item_count = count($items_alter_index); + $item_count = count($distributions); $form_state->set($count_property, $item_count); $form_state->setRebuild(); @@ -396,18 +353,8 @@ protected static function moveElement(FormStateInterface $form_state, int $offse $parent = $button_element['#attributes']['data-parent']; $parents = $button_element['#parents']; $element_index = str_replace("{$parent}-", '', $button_element['#name']); - $alter_property = self::buildAlterProperty($parent); - - $items_alter_index = $form_state->get($alter_property) ?? []; $user_input = $form_state->getUserInput(); - // Move the specific element up in the alter index. - if (isset($items_alter_index[$element_index])) { - $moved_element = array_splice($items_alter_index, $element_index, 1); - array_splice($items_alter_index, $element_index + $offset, 0, $moved_element); - $form_state->set($alter_property, array_values($items_alter_index)); - } - // Update the user input to change the order. $key_exists = NULL; // We actually want the parent container of all elements. Hopefully going From aaf2fbe46c3580cbebb10f75b7e6fd9ce5beec27 Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Fri, 15 Nov 2024 16:54:12 -0500 Subject: [PATCH 03/10] Starting to work on simple element, not working --- modules/json_form_widget/src/ArrayHelper.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index 7d2c1d60ad..7065947188 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -31,9 +31,7 @@ class ArrayHelper implements ContainerInjectionInterface { public FieldTypeRouter $builder; /** - * Inherited. - * - * @{inheritdocs} + * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( @@ -56,10 +54,6 @@ public function setBuilder(FieldTypeRouter $builder): void { $this->objectHelper->setBuilder($builder); } - // public function removeButtonCallback(array &$form, FormStateInterface $form_state): array { - - // } - /** * Update wrapper element of the triggering button after build. * @@ -278,7 +272,7 @@ protected function buildArrayElement(array $definition, $data, FormStateInterfac // Attempt to build a complex element, otherwise... $this->buildComplexArrayElement($definition, $data, $form_state, $context) : // Build a simple element. - $this->buildSimpleArrayElement($definition, $data); + $this->buildSimpleArrayElement($definition, $data, $context); // Set element requirement. $element['#required'] = $required; @@ -289,11 +283,12 @@ protected function buildArrayElement(array $definition, $data, FormStateInterfac /** * Returns single simple element from array. */ - protected function buildSimpleArrayElement(array $definition, $data): array { + protected function buildSimpleArrayElement(array $definition, $data, array $context): array { return array_filter([ '#type' => 'textfield', '#title' => $definition['schema']->items->title ?? NULL, '#default_value' => $data, + 'actions' => $this->buildElementActions($definition['name'], self::buildContextName($context)) ]); } From 86fe6c54261795ce79cb3bd4f6eb6ff1b45a0cc6 Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Tue, 19 Nov 2024 17:52:30 -0500 Subject: [PATCH 04/10] Working for complex and simple arrays --- modules/json_form_widget/README.md | 37 +++++++++++++ modules/json_form_widget/src/ArrayHelper.php | 54 ++++++++++++------- modules/json_form_widget/src/ValueHandler.php | 5 +- schema/collections/dataset.json | 1 + schema/collections/dataset.ui.json | 44 ++++----------- 5 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 modules/json_form_widget/README.md diff --git a/modules/json_form_widget/README.md b/modules/json_form_widget/README.md new file mode 100644 index 0000000000..fbfe3e3a35 --- /dev/null +++ b/modules/json_form_widget/README.md @@ -0,0 +1,37 @@ +```mermaid +graph TD + getForm["FormBuilder::getJsonForm()"] --> eachProp["foreach $properties"] + subgraph getElements + eachProp --> getElement["FieldTypeRouter::getFormElement()"] + getElement --> switch[Switch $type] + switch --> object{object} + + object -- true --> handleObject["ObjectHelper::handleObjectElement()"] + handleObject --> generateObject["ObjectHelper::generateObjectElement()"] + generateObject --> generateProperties["ObjectHelper::generateProperties()"] + generateProperties -- recursion --> eachProp + + + object -- false --> array{array} + array -- true --> handleArray["ArrayHelper::handleArrayElement()"] + handleArray --> complex{Items are objects?} + complex -- no --> buildSimple["ArrayHelper::buildSimpleArrayElement()"] + complex -- yes --> buildComplex["ArrayHelper::buildComplexArrayElement()"] + buildComplex --> handleObject + + array -- false --> string["string"] + string -- true --> handleString["StringHelper::handleStringElement()"] + string -- false --> integer["integer"] + integer -- true --> handleInteger["IntegerHelper::handleIntegerElement()"] + switch --> eachProp + end + eachProp -->getForm + getForm --> applySchemaUi["SchemaUiHandler::applySchemaUi()"] + + subgraph SchemaUI + applySchemaUi --> eachProp2["foreach schemaUI property"] + eachProp2 --> applyOnBaseField["SchemaUiHandler::applyOnBaseField()"] + eachProp2 --> handlePropertySpec["SchemaUiHandler::handlePropertySpec()"] + + end +``` diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index 7065947188..ce8b942971 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -111,7 +111,7 @@ public function handleArrayElement(array $definition, ?array $data, FormStateInt } // Build field element. - return [ + $element = [ '#type' => 'fieldset', '#title' => ($definition['schema']->title ?? $field_name), '#description' => ($definition['schema']->description ?? ''), @@ -127,6 +127,7 @@ public function handleArrayElement(array $definition, ?array $data, FormStateInt ], $field_name => $items, ]; + return $element; } /** @@ -225,7 +226,7 @@ public static function buildAlterProperty(string $context_name): array { * The context name, output of ::buildContextName(). */ protected function buildAction(string $title, string $method, string $parent, string $context_name): array { - return [ + $action = [ '#type' => 'submit', '#name' => $context_name, '#value' => $title, @@ -239,6 +240,7 @@ protected function buildAction(string $title, string $method, string $parent, st ], '#limit_validation_errors' => [], ]; + return $action; } /** @@ -257,7 +259,7 @@ protected function buildElementActions(string $parent, string $context_name) { '#type' => 'actions', 'remove' => $this->buildAction($this->t('Remove'), 'remove', $parent, $context_name), 'move_up' => $this->buildAction($this->t('Move Up'), 'moveUp', $parent, $context_name), - 'move_d' => $this->buildAction($this->t('Move Down'), 'moveDown', $parent, $context_name), + 'move_down' => $this->buildAction($this->t('Move Down'), 'moveDown', $parent, $context_name), ]; } @@ -284,12 +286,19 @@ protected function buildArrayElement(array $definition, $data, FormStateInterfac * Returns single simple element from array. */ protected function buildSimpleArrayElement(array $definition, $data, array $context): array { - return array_filter([ - '#type' => 'textfield', - '#title' => $definition['schema']->items->title ?? NULL, - '#default_value' => $data, - 'actions' => $this->buildElementActions($definition['name'], self::buildContextName($context)) - ]); + return [ + '#type' => 'container', + '#attributes' => [ + 'data-parent' => $definition['name'], + 'class' => ['json-form-widget-array-item'], + ], + 'field' => array_filter([ + '#type' => 'textfield', + '#title' => $definition['schema']->items->title ?? NULL, + '#default_value' => $data, + ]), + 'actions' => $this->buildElementActions($definition['name'], self::buildContextName($context)), + ]; } /** @@ -315,26 +324,35 @@ public static function remove(array &$form, FormStateInterface $form_state) { // Update the user input to remove the specific element. $key_exists = NULL; - // We actually want the parent container of all elements. Hopefully going - // back 4 levels will work in all situations. - array_splice($parents, -4); - $distributions = &NestedArray::getValue($user_input, $parents, $key_exists); + static::trimParents($parents, $element_index); + $input_values = &NestedArray::getValue($user_input, $parents, $key_exists); if ($key_exists) { - unset($distributions[$element_index]); + unset($input_values[$element_index]); // Re-index the array to maintain proper keys. - $distributions = \array_values($distributions); + $input_values = \array_values($input_values); } $form_state->setUserInput($user_input); // Modify stored item count. The form rebuilds before the alter, so it needs // to be one more than the current item count to avoid removing twice. - $item_count = count($distributions); + $item_count = count($input_values); $form_state->set($count_property, $item_count); $form_state->setRebuild(); } + public static function trimParents(array &$parents, int $element_index): void { + for ($i = count($parents) - 1; $i >= 0; $i--) { + if ($parents[$i] == $element_index) { + $ei_position = $i; + break; + } + } + $offset = 0 - (count($parents) - $ei_position); + \array_splice($parents, $offset); + } + public static function moveUp(array &$form, FormStateInterface $form_state) { return static::moveElement($form_state, -1); } @@ -352,9 +370,7 @@ protected static function moveElement(FormStateInterface $form_state, int $offse // Update the user input to change the order. $key_exists = NULL; - // We actually want the parent container of all elements. Hopefully going - // back 4 levels will work in all situations. - array_splice($parents, -4); + static::trimParents($parents, $element_index); $distributions = &NestedArray::getValue($user_input, $parents, $key_exists); if ($key_exists) { $moved_element = array_splice($distributions, $element_index, 1); diff --git a/modules/json_form_widget/src/ValueHandler.php b/modules/json_form_widget/src/ValueHandler.php index 8902f50533..3858347b59 100644 --- a/modules/json_form_widget/src/ValueHandler.php +++ b/modules/json_form_widget/src/ValueHandler.php @@ -148,9 +148,12 @@ public function handleArrayValues($formValues, $property, $schema) { */ private function flattenArraysInArrays($value) { $data = []; + if (isset($value['actions'])) { + unset($value['actions']); + } if (is_array($value)) { foreach ($value as $item) { - $data[] = $this->cleanSelectId($item); + $data[] = $this->flattenArraysInArrays($item); } } elseif (!empty($value)) { diff --git a/schema/collections/dataset.json b/schema/collections/dataset.json index 95378504b1..76d23e3f99 100644 --- a/schema/collections/dataset.json +++ b/schema/collections/dataset.json @@ -313,6 +313,7 @@ "title": "Related Documents", "description": "Related documents such as technical information about a dataset, developer documentation, etc.", "type": "array", + "minItems": 1, "items": { "type": "string", "format": "uri" diff --git a/schema/collections/dataset.ui.json b/schema/collections/dataset.ui.json index 75d046ebe3..043ddc8d1c 100644 --- a/schema/collections/dataset.ui.json +++ b/schema/collections/dataset.ui.json @@ -133,58 +133,32 @@ }, "description": { "ui:options": { - "widget": "textarea", - "rows": 5, - "title": "File Description", - "description": "" + "widget": "hidden" } }, - "format": { + "accessURL": { "ui:options": { - "title": "File Format", - "widget": "list", - "type": "select_other", - "other_type": "textfield", - "description": "CSV files must be encoded in UTF-8 format to be imported correctly. UTF-8 encoding is an established standard that provides optimal compatibility between applications and operating systems. Note that Excel provides a CSV UTF-8 option when saving data files.", - "source": { - "enum": [ - "arcgis", - "csv", - "esri rest", - "geojson", - "json", - "kml", - "pdf", - "tsv", - "xls", - "xlsx", - "xml", - "zip" - ] - } + "widget": "hidden" } }, - "downloadURL": { + "format": { "ui:options": { - "widget": "upload_or_link", - "extensions": "csv html xls json xlsx doc docx rdf txt jpg png gif tiff pdf odf ods odt tsv tab geojson xml zip kml kmz shp", - "progress_indicator": "bar", - "description": "URL providing direct access to a downloadable file." + "widget": "hidden" } }, - "accessURL": { + "conformsTo": { "ui:options": { - "description": "URL providing indirect access to the data, for example via API or a graphical interface." + "widget": "hidden" } }, "describedBy": { "ui:options": { - "description": "URL to the data dictionary for the file found at the Download URL." + "widget": "hidden" } }, "describedByType": { "ui:options": { - "description": "The machine-readable file format (IANA Media Type or MIME Type) of the distribution’s Data Dictionary URL." + "widget": "hidden" } } } From e63259d94c561f19f8c99a7dd304e218dd1cd9d7 Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Tue, 19 Nov 2024 23:42:03 -0500 Subject: [PATCH 05/10] Clean up --- modules/json_form_widget/src/ArrayHelper.php | 109 +++++++++++++++--- .../json_form_widget/src/SchemaUiHandler.php | 2 +- 2 files changed, 94 insertions(+), 17 deletions(-) diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index ce8b942971..5398bfdca6 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -3,9 +3,9 @@ namespace Drupal\json_form_widget; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Form\FormStateInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\DependencySerializationTrait; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -264,26 +264,47 @@ protected function buildElementActions(string $parent, string $context_name) { } /** - * Handle single element from array. + * Build a single element from an array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param string[] $context + * Field context. + * @param bool $required + * Whether the field is required. * - * Chooses whether element is simple or complex. + * @return array + * Render array for the array element. */ protected function buildArrayElement(array $definition, $data, FormStateInterface $form_state, array $context, bool $required): array { - // If this element's definition has properties defined... - $element = isset($definition['schema']->items->properties) ? - // Attempt to build a complex element, otherwise... - $this->buildComplexArrayElement($definition, $data, $form_state, $context) : - // Build a simple element. + // Use the simple or complex method depending on whether items are objects. + if (isset($definition['schema']->items->properties)) { + $element = $this->buildComplexArrayElement($definition, $data, $form_state, $context); + } + else { $this->buildSimpleArrayElement($definition, $data, $context); - - // Set element requirement. + } + // If we show the element on the form, it's required. $element['#required'] = $required; - return $element; } /** * Returns single simple element from array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param string[] $context + * Field context. + * + * @return array + * Render array for the simple array element. */ protected function buildSimpleArrayElement(array $definition, $data, array $context): array { return [ @@ -303,6 +324,18 @@ protected function buildSimpleArrayElement(array $definition, $data, array $cont /** * Returns single complex element from array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param string[] $context + * Field context. + * + * @return array + * Render array for the complex array element. */ protected function buildComplexArrayElement(array $definition, $data, FormStateInterface $form_state, array $context): array { $subdefinition = [ @@ -314,6 +347,14 @@ protected function buildComplexArrayElement(array $definition, $data, FormStateI return $element; } + /** + * Submit function for element "remove" button. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + */ public static function remove(array &$form, FormStateInterface $form_state) { $button_element = $form_state->getTriggeringElement(); $parent = $button_element['#attributes']['data-parent']; @@ -342,6 +383,16 @@ public static function remove(array &$form, FormStateInterface $form_state) { $form_state->setRebuild(); } + /** + * Utility function to trim the triggering element's parents array. + * + * Used to get the correct position in the user input array for modifications. + * + * @param array $parents + * Parents array. + * @param int $element_index + * Element index. + */ public static function trimParents(array &$parents, int $element_index): void { for ($i = count($parents) - 1; $i >= 0; $i--) { if ($parents[$i] == $element_index) { @@ -353,14 +404,38 @@ public static function trimParents(array &$parents, int $element_index): void { \array_splice($parents, $offset); } + /** + * Submit function for element "move up" button. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + */ public static function moveUp(array &$form, FormStateInterface $form_state) { return static::moveElement($form_state, -1); } + /** + * Submit function for element "move down" button. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + */ public static function moveDown(array &$form, FormStateInterface $form_state) { return static::moveElement($form_state, 1); } + /** + * Common function to move element within array by the given offset. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param int $offset + * Offset to move the element by. + */ protected static function moveElement(FormStateInterface $form_state, int $offset) { $button_element = $form_state->getTriggeringElement(); $parent = $button_element['#attributes']['data-parent']; @@ -371,12 +446,12 @@ protected static function moveElement(FormStateInterface $form_state, int $offse // Update the user input to change the order. $key_exists = NULL; static::trimParents($parents, $element_index); - $distributions = &NestedArray::getValue($user_input, $parents, $key_exists); + $input_values = &NestedArray::getValue($user_input, $parents, $key_exists); if ($key_exists) { - $moved_element = array_splice($distributions, $element_index, 1); - array_splice($distributions, $element_index + $offset, 0, $moved_element); + $moved_element = array_splice($input_values, $element_index, 1); + array_splice($input_values, $element_index + $offset, 0, $moved_element); // Re-index the array to maintain proper keys. - $distributions = \array_values($distributions); + $input_values = \array_values($input_values); } $form_state->setUserInput($user_input); @@ -384,8 +459,10 @@ protected static function moveElement(FormStateInterface $form_state, int $offse } /** - * Update count property by the given offset. + * Submit function for array "add one" button. * + * @param array $form + * Form render array. * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state. */ diff --git a/modules/json_form_widget/src/SchemaUiHandler.php b/modules/json_form_widget/src/SchemaUiHandler.php index 8d47b9a5eb..75a0320f45 100644 --- a/modules/json_form_widget/src/SchemaUiHandler.php +++ b/modules/json_form_widget/src/SchemaUiHandler.php @@ -66,7 +66,7 @@ public static function create(ContainerInterface $container) { public function __construct( SchemaRetriever $schema_retriever, LoggerInterface $loggerChannel, - WidgetRouter $widget_router + WidgetRouter $widget_router, ) { $this->schemaRetriever = $schema_retriever; $this->schemaUi = FALSE; From cafc2f5fb0680745a0a32fc0b7b0cf0a2c172245 Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Wed, 20 Nov 2024 09:10:54 -0500 Subject: [PATCH 06/10] Revert schema changes --- schema/collections/dataset.json | 1 - schema/collections/dataset.ui.json | 44 ++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/schema/collections/dataset.json b/schema/collections/dataset.json index 76d23e3f99..95378504b1 100644 --- a/schema/collections/dataset.json +++ b/schema/collections/dataset.json @@ -313,7 +313,6 @@ "title": "Related Documents", "description": "Related documents such as technical information about a dataset, developer documentation, etc.", "type": "array", - "minItems": 1, "items": { "type": "string", "format": "uri" diff --git a/schema/collections/dataset.ui.json b/schema/collections/dataset.ui.json index 043ddc8d1c..75d046ebe3 100644 --- a/schema/collections/dataset.ui.json +++ b/schema/collections/dataset.ui.json @@ -133,32 +133,58 @@ }, "description": { "ui:options": { - "widget": "hidden" + "widget": "textarea", + "rows": 5, + "title": "File Description", + "description": "" } }, - "accessURL": { + "format": { "ui:options": { - "widget": "hidden" + "title": "File Format", + "widget": "list", + "type": "select_other", + "other_type": "textfield", + "description": "CSV files must be encoded in UTF-8 format to be imported correctly. UTF-8 encoding is an established standard that provides optimal compatibility between applications and operating systems. Note that Excel provides a CSV UTF-8 option when saving data files.", + "source": { + "enum": [ + "arcgis", + "csv", + "esri rest", + "geojson", + "json", + "kml", + "pdf", + "tsv", + "xls", + "xlsx", + "xml", + "zip" + ] + } } }, - "format": { + "downloadURL": { "ui:options": { - "widget": "hidden" + "widget": "upload_or_link", + "extensions": "csv html xls json xlsx doc docx rdf txt jpg png gif tiff pdf odf ods odt tsv tab geojson xml zip kml kmz shp", + "progress_indicator": "bar", + "description": "URL providing direct access to a downloadable file." } }, - "conformsTo": { + "accessURL": { "ui:options": { - "widget": "hidden" + "description": "URL providing indirect access to the data, for example via API or a graphical interface." } }, "describedBy": { "ui:options": { - "widget": "hidden" + "description": "URL to the data dictionary for the file found at the Download URL." } }, "describedByType": { "ui:options": { - "widget": "hidden" + "description": "The machine-readable file format (IANA Media Type or MIME Type) of the distribution’s Data Dictionary URL." } } } From 877cf812e0e3685d52db8b9a0c61daa9c32d4055 Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Wed, 20 Nov 2024 10:02:36 -0500 Subject: [PATCH 07/10] Fix simple array element --- modules/json_form_widget/src/ArrayHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index 5398bfdca6..d6f3636b48 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -286,7 +286,7 @@ protected function buildArrayElement(array $definition, $data, FormStateInterfac $element = $this->buildComplexArrayElement($definition, $data, $form_state, $context); } else { - $this->buildSimpleArrayElement($definition, $data, $context); + $element = $this->buildSimpleArrayElement($definition, $data, $context); } // If we show the element on the form, it's required. $element['#required'] = $required; From 9a5da9570ca15dac1f0928eaeea6da732608812b Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Thu, 21 Nov 2024 11:36:41 -0500 Subject: [PATCH 08/10] Cleanup, select2 improvements, required --- modules/json_form_widget/src/ArrayHelper.php | 237 ++++++++---------- modules/json_form_widget/src/ValueHandler.php | 2 +- modules/json_form_widget/src/WidgetRouter.php | 5 + .../tests/src/Unit/ArrayHelperTest.php | 12 +- 4 files changed, 121 insertions(+), 135 deletions(-) diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index d6f3636b48..687d48dd62 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -55,7 +55,7 @@ public function setBuilder(FieldTypeRouter $builder): void { } /** - * Update wrapper element of the triggering button after build. + * Shared AJAX callback function for all array buttons. * * @param array $form * Newly built form render array. @@ -65,7 +65,7 @@ public function setBuilder(FieldTypeRouter $builder): void { * @return array * Field wrapper render array. */ - public function addOrRemoveButtonCallback(array &$form, FormStateInterface $form_state): array { + public function arrayActionButtonCallback(array &$form, FormStateInterface $form_state): array { // Retrieve triggering button element. $button = $form_state->getTriggeringElement(); // Extract full heritage for the triggered button. @@ -107,7 +107,7 @@ public function handleArrayElement(array $definition, ?array $data, FormStateInt $items = []; for ($i = 0; $i < $item_count; $i++) { $property_required = $is_required && ($i < $min_items); - $items[] = $this->buildArrayElement($definition, $data[$i] ?? NULL, $form_state, array_merge($context, [$i]), $property_required); + $items[] = $this->buildArrayItemElement($definition, $data[$i] ?? NULL, $form_state, array_merge($context, [$i]), $property_required); } // Build field element. @@ -119,6 +119,7 @@ public function handleArrayElement(array $definition, ?array $data, FormStateInt '#prefix' => '
', '#suffix' => '
', '#tree' => TRUE, + '#required' => $is_required, 'actions' => [ '#type' => 'actions', 'actions' => [ @@ -130,6 +131,90 @@ public function handleArrayElement(array $definition, ?array $data, FormStateInt return $element; } + /** + * Build a single element from an array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param string[] $context + * Field context. + * @param bool $required + * Whether the field is required. + * + * @return array + * Render array for the array element. + */ + protected function buildArrayItemElement(array $definition, $data, FormStateInterface $form_state, array $context, bool $required): array { + // Use the simple or complex method depending on whether items are objects. + if (isset($definition['schema']->items->properties)) { + $element = $this->buildComplexArrayElement($definition, $data, $form_state, $context); + } + else { + $element = $this->buildSimpleArrayElement($definition, $data, $context); + } + // If we show the element on the form, it's required. + $element['#required'] = $required; + return $element; + } + + /** + * Returns single simple element from array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param string[] $context + * Field context. + * + * @return array + * Render array for the simple array element. + */ + protected function buildSimpleArrayElement(array $definition, $data, array $context): array { + return [ + '#type' => 'fieldset', + '#attributes' => [ + 'data-parent' => $definition['name'], + 'class' => ['json-form-widget-array-item'], + ], + 'field' => array_filter([ + '#type' => 'textfield', + '#title' => $definition['schema']->items->title ?? NULL, + '#default_value' => $data, + ]), + 'actions' => $this->buildElementActions($definition['name'], self::buildContextName($context)), + ]; + } + + /** + * Returns single complex element from array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param string[] $context + * Field context. + * + * @return array + * Render array for the complex array element. + */ + protected function buildComplexArrayElement(array $definition, $data, FormStateInterface $form_state, array $context): array { + $subdefinition = [ + 'name' => $definition['name'], + 'schema' => $definition['schema']->items, + ]; + $element = $this->objectHelper->handleObjectElement($subdefinition, $data, $form_state, $context, $this->builder); + $element[$definition['name']]['actions'] = $this->buildElementActions($definition['name'], self::buildContextName($context)); + return $element; + } + /** * Get the form items count for the given field. * @@ -200,19 +285,6 @@ public static function buildCountProperty(string $context_name): array { return ['json_form_widget_info', $context_name, 'count']; } - /** - * Build count property. - * - * @param string $context_name - * Field element context name. - * - * @return string[] - * Full count property array. - */ - public static function buildAlterProperty(string $context_name): array { - return ['json_form_widget_info', $context_name, 'alter']; - } - /** * Helper function to build an action button. * @@ -232,7 +304,7 @@ protected function buildAction(string $title, string $method, string $parent, st '#value' => $title, '#submit' => [self::class . '::' . $method], '#ajax' => [ - 'callback' => [$this, 'addOrRemoveButtonCallback'], + 'callback' => [$this, 'arrayActionButtonCallback'], 'wrapper' => self::buildWrapperIdentifier($parent), ], '#attributes' => [ @@ -263,90 +335,6 @@ protected function buildElementActions(string $parent, string $context_name) { ]; } - /** - * Build a single element from an array. - * - * @param array $definition - * Field definition. - * @param mixed $data - * Field data. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Form state. - * @param string[] $context - * Field context. - * @param bool $required - * Whether the field is required. - * - * @return array - * Render array for the array element. - */ - protected function buildArrayElement(array $definition, $data, FormStateInterface $form_state, array $context, bool $required): array { - // Use the simple or complex method depending on whether items are objects. - if (isset($definition['schema']->items->properties)) { - $element = $this->buildComplexArrayElement($definition, $data, $form_state, $context); - } - else { - $element = $this->buildSimpleArrayElement($definition, $data, $context); - } - // If we show the element on the form, it's required. - $element['#required'] = $required; - return $element; - } - - /** - * Returns single simple element from array. - * - * @param array $definition - * Field definition. - * @param mixed $data - * Field data. - * @param string[] $context - * Field context. - * - * @return array - * Render array for the simple array element. - */ - protected function buildSimpleArrayElement(array $definition, $data, array $context): array { - return [ - '#type' => 'container', - '#attributes' => [ - 'data-parent' => $definition['name'], - 'class' => ['json-form-widget-array-item'], - ], - 'field' => array_filter([ - '#type' => 'textfield', - '#title' => $definition['schema']->items->title ?? NULL, - '#default_value' => $data, - ]), - 'actions' => $this->buildElementActions($definition['name'], self::buildContextName($context)), - ]; - } - - /** - * Returns single complex element from array. - * - * @param array $definition - * Field definition. - * @param mixed $data - * Field data. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Form state. - * @param string[] $context - * Field context. - * - * @return array - * Render array for the complex array element. - */ - protected function buildComplexArrayElement(array $definition, $data, FormStateInterface $form_state, array $context): array { - $subdefinition = [ - 'name' => $definition['name'], - 'schema' => $definition['schema']->items, - ]; - $element = $this->objectHelper->handleObjectElement($subdefinition, $data, $form_state, $context, $this->builder); - $element[$definition['name']]['actions'] = $this->buildElementActions($definition['name'], self::buildContextName($context)); - return $element; - } - /** * Submit function for element "remove" button. * @@ -383,27 +371,6 @@ public static function remove(array &$form, FormStateInterface $form_state) { $form_state->setRebuild(); } - /** - * Utility function to trim the triggering element's parents array. - * - * Used to get the correct position in the user input array for modifications. - * - * @param array $parents - * Parents array. - * @param int $element_index - * Element index. - */ - public static function trimParents(array &$parents, int $element_index): void { - for ($i = count($parents) - 1; $i >= 0; $i--) { - if ($parents[$i] == $element_index) { - $ei_position = $i; - break; - } - } - $offset = 0 - (count($parents) - $ei_position); - \array_splice($parents, $offset); - } - /** * Submit function for element "move up" button. * @@ -468,18 +435,32 @@ protected static function moveElement(FormStateInterface $form_state, int $offse */ public static function addOne(array &$form, FormStateInterface $form_state) { $button_element = $form_state->getTriggeringElement(); - $alter_property = self::buildAlterProperty($button_element['#name']); - $items_alter_index = $form_state->get($alter_property) ?? []; - $items_alter_index[] = count($items_alter_index); - $count_property = static::buildCountProperty($button_element['#name']); // Modify stored item count. $item_count = $form_state->get($count_property) ?? 0; $item_count++; $form_state->set($count_property, $item_count); - - $form_state->set($alter_property, $items_alter_index); $form_state->setRebuild(); } + /** + * Utility function to trim the triggering element's parents array. + * + * Used to get the correct position in the user input array for modifications. + * + * @param array $parents + * Parents array. + * @param int $element_index + * Element index. + */ + public static function trimParents(array &$parents, int $element_index): void { + for ($i = count($parents) - 1; $i >= 0; $i--) { + if ($parents[$i] == $element_index) { + $ei_position = $i; + break; + } + } + $offset = 0 - (count($parents) - $ei_position); + \array_splice($parents, $offset); + } } diff --git a/modules/json_form_widget/src/ValueHandler.php b/modules/json_form_widget/src/ValueHandler.php index 3858347b59..79f4dfd94a 100644 --- a/modules/json_form_widget/src/ValueHandler.php +++ b/modules/json_form_widget/src/ValueHandler.php @@ -153,7 +153,7 @@ private function flattenArraysInArrays($value) { } if (is_array($value)) { foreach ($value as $item) { - $data[] = $this->flattenArraysInArrays($item); + $data[] = is_array($item) ? $this->flattenArraysInArrays($item) : $this->cleanSelectId($item); } } elseif (!empty($value)) { diff --git a/modules/json_form_widget/src/WidgetRouter.php b/modules/json_form_widget/src/WidgetRouter.php index 79047a42d1..5081b32d81 100644 --- a/modules/json_form_widget/src/WidgetRouter.php +++ b/modules/json_form_widget/src/WidgetRouter.php @@ -147,6 +147,11 @@ public function handleListElement(mixed $spec, array $element) { * The dropdown element configured. */ public function getDropdownElement(mixed $element, mixed $spec, mixed $titleProperty = FALSE) { + // Array elements may be fieldsets, since by default they are simple array + // elements with Remove/Up/Down buttons. + if (isset($element['field']) && $element['#type'] == 'fieldset') { + $element = ['#required' => ($element['#required'] ?? FALSE)] + $element['field']; + } $element['#type'] = $this->getSelectType($spec); $element['#options'] = $this->getDropdownOptions($spec->source, $titleProperty); if ($element['#type'] === 'select_or_other_select') { diff --git a/modules/json_form_widget/tests/src/Unit/ArrayHelperTest.php b/modules/json_form_widget/tests/src/Unit/ArrayHelperTest.php index 7819dc8bd4..2f88b400d8 100644 --- a/modules/json_form_widget/tests/src/Unit/ArrayHelperTest.php +++ b/modules/json_form_widget/tests/src/Unit/ArrayHelperTest.php @@ -2,20 +2,20 @@ namespace Drupal\Tests\json_form_widget\Unit; -use PHPUnit\Framework\TestCase; -use Drupal\json_form_widget\ArrayHelper; -use MockChain\Chain; use Drupal\Component\DependencyInjection\Container; use Drupal\Core\Form\FormState; use Drupal\Core\Logger\LoggerChannelFactory; use Drupal\Core\StringTranslation\TranslationManager; +use Drupal\json_form_widget\ArrayHelper; use Drupal\json_form_widget\FieldTypeRouter; use Drupal\json_form_widget\IntegerHelper; use Drupal\json_form_widget\ObjectHelper; use Drupal\json_form_widget\SchemaUiHandler; use Drupal\json_form_widget\StringHelper; use Drupal\metastore\SchemaRetriever; +use MockChain\Chain; use MockChain\Options; +use PHPUnit\Framework\TestCase; /** * Test class for ArrayHelper. @@ -79,7 +79,7 @@ public function testComplex() { $result = $array_helper->handleArrayElement($definition, [], $form_state, $context); $expected = $this->getExpectedComplexArrayElement(); unset($result['actions']); - unset($result['distribution'][0]['distribution']['schema']['schema']['fields']['actions']); + unset($result['distribution'][0]['distribution']['actions']); $this->assertEquals($expected, $result); } @@ -182,10 +182,10 @@ private function getExpectedObject() { "#required" => FALSE, ], ], - ] + ], ], ], - ] + ], ], ], "#required" => FALSE, From 49c70d88c2916e5889784cc520541d0b751db285 Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Thu, 21 Nov 2024 13:06:38 -0500 Subject: [PATCH 09/10] Trying to fix array flattening issues --- modules/json_form_widget/src/ArrayHelper.php | 12 ++++++++++++ modules/json_form_widget/src/SchemaUiHandler.php | 3 ++- modules/json_form_widget/src/WidgetRouter.php | 6 +----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index 687d48dd62..da2e87bd3e 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -190,6 +190,18 @@ protected function buildSimpleArrayElement(array $definition, $data, array $cont ]; } + /** + * Flatten array element fieldset w/buttons for processing. + * + * @param array $element + * A form element. + */ + public static function flattenArrayElementFieldset(array &$element): void { + if (isset($element['field']) && $element['#type'] == 'fieldset') { + $element = ['#required' => ($element['#required'] ?? FALSE)] + $element['field']; + } + } + /** * Returns single complex element from array. * diff --git a/modules/json_form_widget/src/SchemaUiHandler.php b/modules/json_form_widget/src/SchemaUiHandler.php index 75a0320f45..57d20ca5e4 100644 --- a/modules/json_form_widget/src/SchemaUiHandler.php +++ b/modules/json_form_widget/src/SchemaUiHandler.php @@ -173,7 +173,7 @@ public function applyOnBaseField(mixed $spec, array $element) { $element = $this->changeFieldDescriptions($spec->{"ui:options"}, $element); $element = $this->changeFieldTitle($spec->{"ui:options"}, $element); if (isset($spec->{"ui:options"}->hideActions)) { - $element = $this->flattenArrays($spec->{"ui:options"}, $element); + // $element = $this->flattenArrays($spec->{"ui:options"}, $element); } } return $element; @@ -194,6 +194,7 @@ public function flattenArrays(mixed $spec, array $element) { unset($element['actions']); $default_value = []; foreach ($element[$spec->child] as $key => $item) { + ArrayHelper::flattenArrayElementFieldset($item); $default_value = array_merge($default_value, $this->formatArrayDefaultValue($item)); if ($key != 0) { unset($element[$spec->child][$key]); diff --git a/modules/json_form_widget/src/WidgetRouter.php b/modules/json_form_widget/src/WidgetRouter.php index 5081b32d81..00a0663558 100644 --- a/modules/json_form_widget/src/WidgetRouter.php +++ b/modules/json_form_widget/src/WidgetRouter.php @@ -147,11 +147,7 @@ public function handleListElement(mixed $spec, array $element) { * The dropdown element configured. */ public function getDropdownElement(mixed $element, mixed $spec, mixed $titleProperty = FALSE) { - // Array elements may be fieldsets, since by default they are simple array - // elements with Remove/Up/Down buttons. - if (isset($element['field']) && $element['#type'] == 'fieldset') { - $element = ['#required' => ($element['#required'] ?? FALSE)] + $element['field']; - } + ArrayHelper::flattenArrayElementFieldset($element); $element['#type'] = $this->getSelectType($spec); $element['#options'] = $this->getDropdownOptions($spec->source, $titleProperty); if ($element['#type'] === 'select_or_other_select') { From e2c28bab7059273a388750c049b85325ad555755 Mon Sep 17 00:00:00 2001 From: Dan Feder Date: Fri, 22 Nov 2024 17:32:19 -0500 Subject: [PATCH 10/10] Think everything's working --- modules/json_form_widget/README.md | 121 +++++++++++++----- modules/json_form_widget/src/ArrayHelper.php | 1 + modules/json_form_widget/src/FormBuilder.php | 2 +- .../Field/FieldWidget/JsonFormWidget.php | 6 +- .../json_form_widget/src/SchemaUiHandler.php | 14 +- 5 files changed, 108 insertions(+), 36 deletions(-) diff --git a/modules/json_form_widget/README.md b/modules/json_form_widget/README.md index fbfe3e3a35..51d4748d63 100644 --- a/modules/json_form_widget/README.md +++ b/modules/json_form_widget/README.md @@ -1,37 +1,100 @@ +## High level + +```mermaid +sequenceDiagram + participant FormBuilder + participant FieldTypeRouter + participant Element Handlers + participant SchemaUiHandler + + loop each $property in $form + FormBuilder ->> FieldTypeRouter: getFormElement() + FieldTypeRouter ->> Element Handlers: handler functions
on helper classes + Note over FieldTypeRouter, Element Handlers: Initial build of property
Elements + Element Handlers ->> FormBuilder: Return default element for $property + end + + FormBuilder ->> SchemaUiHandler: applySchemaUi() + Note over FormBuilder, SchemaUiHandler: Now apply SchemaUi to full $form + loop each $property + SchemaUiHandler ->> SchemaUiHandler: applyOnBaseField() + SchemaUiHandler ->> SchemaUiHandler: handlePropertySpec() + Note over SchemaUiHandler, SchemaUiHandler: See handlePropertySpec()
internals diagram + end + SchemaUiHandler ->> FormBuilder: Return $form with SchemaUi alterations + +``` + +## The handlePropertySpec() method + +As called from withing SchemaUiHandler::applyShemaUi + +```mermaid +sequenceDiagram + participant handlePropertySpec + + +``` + +## The initial build + ```mermaid graph TD getForm["FormBuilder::getJsonForm()"] --> eachProp["foreach $properties"] - subgraph getElements - eachProp --> getElement["FieldTypeRouter::getFormElement()"] - getElement --> switch[Switch $type] - switch --> object{object} - - object -- true --> handleObject["ObjectHelper::handleObjectElement()"] - handleObject --> generateObject["ObjectHelper::generateObjectElement()"] - generateObject --> generateProperties["ObjectHelper::generateProperties()"] - generateProperties -- recursion --> eachProp - - - object -- false --> array{array} - array -- true --> handleArray["ArrayHelper::handleArrayElement()"] - handleArray --> complex{Items are objects?} - complex -- no --> buildSimple["ArrayHelper::buildSimpleArrayElement()"] - complex -- yes --> buildComplex["ArrayHelper::buildComplexArrayElement()"] - buildComplex --> handleObject - - array -- false --> string["string"] - string -- true --> handleString["StringHelper::handleStringElement()"] - string -- false --> integer["integer"] - integer -- true --> handleInteger["IntegerHelper::handleIntegerElement()"] - switch --> eachProp - end - eachProp -->getForm - getForm --> applySchemaUi["SchemaUiHandler::applySchemaUi()"] + eachProp --> getElement["FieldTypeRouter::getFormElement()"] + getElement --> switch[Switch $type] + switch --> object{object} + + object -- true --> handleObject["ObjectHelper::handleObjectElement()"] + handleObject --> generateObject["ObjectHelper::generateObjectElement()"] + generateObject --> generateProperties["ObjectHelper::generateProperties()"] + generateProperties -- recursion --> eachProp + + + object -- false --> array{array} + array -- true --> handleArray["ArrayHelper::handleArrayElement()"] + handleArray --> complex{Items are objects?} + complex -- no --> buildSimple["ArrayHelper::buildSimpleArrayElement()"] + complex -- yes --> buildComplex["ArrayHelper::buildComplexArrayElement()"] + buildComplex --> handleObject - subgraph SchemaUI + array -- false --> string["string"] + string -- true --> handleString["StringHelper::handleStringElement()"] + string -- false --> integer["integer"] + integer -- true --> handleInteger["IntegerHelper::handleIntegerElement()"] + switch --> eachProp + eachProp --> getForm +``` + +## Customizing widgets w/SchemaUI + +```mermaid +flowchart-elk TD + getForm["FormBuilder::getJsonForm()"] --> applySchemaUi["SchemaUiHandler::applySchemaUi()"] applySchemaUi --> eachProp2["foreach schemaUI property"] eachProp2 --> applyOnBaseField["SchemaUiHandler::applyOnBaseField()"] - eachProp2 --> handlePropertySpec["SchemaUiHandler::handlePropertySpec()"] + eachProp2 --> handlePropertySpec + subgraph s1["applyOnBaseField()"] + applyOnBaseField --> updateWidgets["SchemaUiHandler::updatewidgets()"] + updateWidgets --> disableFields["SchemaUiHandler::disableFields()"] + disableFields --> addPlaceholders["SchemaUiHandler::addPlaceholders()"] + addPlaceholders --> changeFieldDescriptions["SchemaUiHandler::changeFieldDescriptions()"] + changeFieldDescriptions --> changeFieldTitle["SchemaUiHandler::changeFieldTitle()"] + end + subgraph s2["handlePropertySpec"] + handlePropertySpec["SchemaUiHandler::handlePropertySpec()"] --> what{"what is it"} + what -- array --> eachArrayElement + eachArrayElement --> applyOnArrayFields + + applyOnArrayFields --> eachArrayElementField + eachArrayElementField --> inSpec{"Does SchemaUI
contain config for
this field?"} + inSpec -- yes --> handlePropertySpec + inSpec -- no --> applyOnBaseFieldRec["SchemaUiHandler::applyOnBaseField()"] + + what -- object --> applyOnObjectFields + applyOnObjectFields --> eachObjField["foreach object property in the SchemaUi spec"] + eachObjField --> applyOnBaseFieldRec + + end - end ``` diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index da2e87bd3e..05900610ce 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -475,4 +475,5 @@ public static function trimParents(array &$parents, int $element_index): void { $offset = 0 - (count($parents) - $ei_position); \array_splice($parents, $offset); } + } diff --git a/modules/json_form_widget/src/FormBuilder.php b/modules/json_form_widget/src/FormBuilder.php index 0eef2582f6..38f2771117 100644 --- a/modules/json_form_widget/src/FormBuilder.php +++ b/modules/json_form_widget/src/FormBuilder.php @@ -68,7 +68,7 @@ public function __construct( SchemaRetriever $schema_retriever, FieldTypeRouter $router, SchemaUiHandler $schema_ui_handler, - LoggerInterface $loggerChannel + LoggerInterface $loggerChannel, ) { $this->schemaRetriever = $schema_retriever; $this->router = $router; diff --git a/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidget.php b/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidget.php index 57a632e976..3c260f3a6f 100644 --- a/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidget.php +++ b/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidget.php @@ -3,14 +3,14 @@ namespace Drupal\json_form_widget\Plugin\Field\FieldWidget; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\json_form_widget\FormBuilder; +use Drupal\json_form_widget\ValueHandler; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Field\FieldDefinitionInterface; use Symfony\Component\HttpFoundation\RequestStack; -use Drupal\json_form_widget\ValueHandler; /** * Plugin implementation of the 'json_form_widget'. @@ -82,7 +82,7 @@ public function __construct( array $third_party_settings, FormBuilder $builder, ValueHandler $value_handler, - RequestStack $request_stack + RequestStack $request_stack, ) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); $this->builder = $builder; diff --git a/modules/json_form_widget/src/SchemaUiHandler.php b/modules/json_form_widget/src/SchemaUiHandler.php index 57d20ca5e4..b8e67532f6 100644 --- a/modules/json_form_widget/src/SchemaUiHandler.php +++ b/modules/json_form_widget/src/SchemaUiHandler.php @@ -173,7 +173,7 @@ public function applyOnBaseField(mixed $spec, array $element) { $element = $this->changeFieldDescriptions($spec->{"ui:options"}, $element); $element = $this->changeFieldTitle($spec->{"ui:options"}, $element); if (isset($spec->{"ui:options"}->hideActions)) { - // $element = $this->flattenArrays($spec->{"ui:options"}, $element); + $element = $this->flattenArrays($spec->{"ui:options"}, $element); } } return $element; @@ -194,13 +194,18 @@ public function flattenArrays(mixed $spec, array $element) { unset($element['actions']); $default_value = []; foreach ($element[$spec->child] as $key => $item) { - ArrayHelper::flattenArrayElementFieldset($item); $default_value = array_merge($default_value, $this->formatArrayDefaultValue($item)); if ($key != 0) { unset($element[$spec->child][$key]); } } - $element[$spec->child][0]['#default_value'] = $default_value; + + if (isset($element[$spec->child][0]['field'])) { + $element[$spec->child][0]['field']['#default_value'] = $default_value; + } + else { + $element[$spec->child][0]['#default_value'] = $default_value; + } return $element; } @@ -211,6 +216,9 @@ private function formatArrayDefaultValue($item) { if (!empty($item['#default_value'])) { return [$item['#default_value'] => $item['#default_value']]; } + if (!empty($item['field']['#default_value'])) { + return [$item['field']['#default_value'] => $item['field']['#default_value']]; + } return []; }