Prevent a user to create two identical items in a structure field

Hello,

I would like to prevent a user to create two identical items in a structure field. My structure field has a simple text field.

I’ve made a function to check if a multidimensional array has two identical values:

function search_identical_value( $array , $field ) {
    $flag = false;
    foreach ( $array as $current_key => $current_array ) {
        foreach ( $array as $search_key => $search_array ) {
            if ( $search_array[$field] == $current_array[$field]) {
                if ( $search_key != $current_key ) {
                    $flag = true;
                    break;
                }
            }
        }
        if ( $flag ) {
            break;
        }
    }
    return $flag;
}

This function works well and return true if two identical items has been found. I put this function in the plugin folder to be accessible from the config file.

This is my blueprint

fields:
      questions:
        label: Questions or instructions
        help: Start your question or instruction with a numbering
        type: structure
        fields:
          questionsLabel:
            label: Question
            type: textarea
            default: '1- ... (Start your question or instruction with a numbering)'
            required: true

To check and prevent two identical items in my structure field, I put this in my config file:

return [
    'debug' => true,

    'hooks' => [

	    'page.update:after' => function ( $newPage, $oldPage ) {

	            $questions = array();
	            $questions = $newPage->questions()->yaml();

	            if ( search_identical_value( $questions , 'questionslabel' ) ) :
	                throw new Exception('Check your questions, you cannot create two identical questions.');
	            endif;
	    },

	]
];

The Exception pop-up if I create two identical items but when I delete the second item from the structure field, the save orange bar disappears and the two items stay in the txt file. So if I reload my panel page, the two items are visible.

Any idea how I can avoid this?

You can test with this starter kit > go to the about page, the structure field is below the textarea. (delete the account folder to reinstall a new admin user)

Thx

I think a page.update:before hook would be more appropriate, but I’ll test with your example.

I did some test with page.update:before but without success:

return [
    'debug' => true,

    'hooks' => [

	    'page.update:before' => function ( $page, $values, $strings ) {

	            $questions = array();
	            $questions = $page->questions()->yaml();

	            if ( search_identical_value( $questions , 'questionslabel' ) ) :
	                throw new Exception('Check your questions, you cannot create two identical questions.');
	            endif;
	    },

	]
];

PS: I don’t really understand what the parameters $values and $strings contain exactly?

Hm, yes, you are right, neither option works as expected.

And why don’t you remove duplicate items from the array instead of throwing an error message?

The problem is that throwing the error actually prevents the action from being executed. So the duplicate entry is not stored and so if you remove the item again, there is no difference to before, so no orange bar (unless there are more changes than a single added duplicate item).

With your default entry to make it very likely that duplicate entries happen in the first place.

I use the Exception pop-up because I want to inform the user that he/she has created two identical items and he/she can’t do this.

Not sure to understand, why the two identical items are written in the txt file if the action is not executed?

Sorry, I was talking nonsense.

:wink:

Yes, the changes are saved but the orange bar doesn’t disappear after clicking ok in the pop-up. So I assume the content store and what is actually stored in the file are out of sync at this time.

So as you suggested above, the solution would be to delete the duplicate value in the array and update my structure field with the new array state, then throw the error to inform the user :thinking:

I will try to do some test …

Or tell the user in the help text that this is what will happen without throwing the error.

On a side note, you could simplify your logic:

<?php 

$questions = A::pluck(page('about')->questions()->yaml(), 'questionslabel');
$unique = array_unique($questions);

if (count($questions) !== count($unique)) {
  echo "There are duplicate entries";
}
?>

Which has the additional advantage that you can store $unique as your new value.

But what if your users use different numbers but the same questionlabel? Wouldn’t it make more sense to leave the numbers out and only add them in the template. Or if they by mistake use the same number for a different questionlabel? Then your check will find that they are different but they will still come out wrong on the frontend.

Thanks Sonja, it’s an interesting shorten solution.

I ask for the numbering because there was a bug with the sortBy options, but I think the last version has fixed this bug, so maybe I don’t need to ask the user to numbering there questions anymore.

Did you manage to sort this out? I’m current looking for a similar solution and wonder if you have any code you can share?

@texnixe I’m trying to implement a similar function to the panel by following the function you wrote about here: Structure field update
But I can’t get i to work.

My function looks like this:

function addToStructure($pageURI, $field, $data = array()) {
  kirby()->impersonate('kirby');
  $fieldData = page($pageURI)->$field()->yaml();
  $fieldData[] = $data;
  $fieldData = yaml::encode($fieldData);
  try {
    page($pageURI)->update(array(
      $field => $fieldData
      )
    );
    return true;
  } catch(Exception $e) {
    return $e->getMessage();
  }
}

And my hook like this:

'page.update:before' => function ($page, $values, $strings) {

    if ($page == page('tag-manager')):
      $globalTags = A::pluck($page->global_tags()->yaml(), 'tag');
      $unique = array_unique($globalTags);

      if (count($globalTags) !== count($unique)):
        // There are duplicate entries
        addToStructure($pageURI = 'tag-manager', $field = 'global_tags', $data = $unique);
      endif;
    endif;
  }

And the blueprint with the structure field looks like this:

tags_section:
  type: fields
  fields:
    global_tags:
      label: Globala taggar
      type: structure
      sortBy: tag asc
      fields:
        tag:
          label: Tag
          type: text

If I enter two identical values to the structure field from the panel and hit save, the data is just saved, and the duplicates are still there. I’m using Kirby v3.2.3

What am I missing?

@felixbridell I’ll look into that later, don’t have time now.

You are so kind. Thank you.

I got i working if I run the function from a page but still not from the panel via the hook.

I changed the function to this:

function addToStructure($pageURI, $field, $data = array()) {
  kirby()->impersonate('kirby');
  $fieldData = yaml::encode($data);

  try {
    page($pageURI)->update(array(
      $field => $fieldData
      )
    );
    return true;
  } catch(Exception $e) {
    return $e->getMessage();
  }
}

And the hook to this:

'page.update:before' => function ($page, $values, $strings) {

    if ($page == page('tag-manager')):
      $globalTags = A::pluck($page->global_tags()->yaml(), 'tag');
      $unique = array_values(array_unique($globalTags));

      if (count($globalTags) !== count($unique)):
        // There are duplicate entries
        $uniqueTags = [];
        foreach ($unique as $tag) {
          $uniqueTags[] = [
            'tag' => $tag
          ];
        }
        
        addToStructure($pageURI = 'tag-manager', $field = 'global_tags', $data = $unique);
      endif;
    endif;
  }

I think the problem is that you are trying to do this from a before hook?

If I change

'page.update:before' => function ($page, $values, $strings) {

to

'page.update:after' => function ($page, $values, $strings) {

I get this error from panel:
Exception: ArgumentCountError
Too few arguments to function Kirby\Cms\App::{closure}(), 2 passed and exactly 3 expected.

And If I change to:

'page.update:after' => function ($page) {

Nothing happen on save…

This works


    'hooks' => [
        'page.update:after' => function ($page) {

            if ($page == page('tag-manager')):
              $globalTags = A::pluck($page->global_tags()->yaml(), 'tag');
              $unique = array_values(array_unique($globalTags));
        
              if (count($globalTags) !== count($unique)):
                // There are duplicate entries
                $uniqueTags = [];
                foreach ($unique as $tag) {
                  $uniqueTags[] = [
                    'tag' => $tag
                  ];
                }
                
                addToStructure('tag-manager', 'global_tags', $uniqueTags);
              endif;
            endif;
          },
    ]

Note that I changed the array you passed to the addToStructure() method.
But note the changes the hook makes will only be visible in the Panel if you reload the page.