Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Implement a CanBeDeletedInterface #5210

Closed
realtebo opened this issue Jul 12, 2023 · 3 comments
Closed

[Feature Request] Implement a CanBeDeletedInterface #5210

realtebo opened this issue Jul 12, 2023 · 3 comments

Comments

@realtebo
Copy link
Contributor

realtebo commented Jul 12, 2023

Feature Request

What's the feature you think Backpack should have?

In a common list , we have usually the delete button.

But some entry cannot be deleted, if they are related from other records, to ensure data integrity.
Usually this check is done my the db, but we could think to add some 'live checks' to single entry to allow the delete button to be dinamically disabled

Have you already implemented a prototype solution, for your own project?

We created app/Models/Interfaces/CanBeDeletedInterface.php

namespace App\Models\Interfaces;

interface CanBeDeletedInterface
{
    function getCanBeDeletedAttribute();
    function getWhyCannotBeDeletedAttribute();
}

A model could implement this interface.

For example: if an Organization has at least one active member, the organization cannot be deleted.

...
use App\Models\Interfaces\CanBeDeletedInterface;
.... 

class Organization extends Model implements CanBeDeletedInterface
{
   ...
    function getCanBeDeletedAttribute()
    {
        if ($this->active_members->count() >0) {
            return false;
        }

        return true;
    }

    function getWhyCannotBeDeletedAttribute()
    {
        if ($this->active_members->count() >0) {
            return "You cannot delete an organization with active members.";
        }

        return null;
    }
}

We customized delete button in resources/views/vendor/backpack/crud/buttons/delete.blade.php

This is the relevant part of our implementation

@if ($crud->hasAccess('delete'))

    @if($entry instanceof  \App\Models\Interfaces\CanBeDeletedInterface)

        @if($entry->can_be_deleted === true)
            <a href="javascript:void(0)" onclick="deleteEntry(this)"
               data-route="{{ url($crud->route.'/'.$entry->getKey()) }}" class="btn btn-sm btn-danger"
               data-button-type="delete"
               data-toggle="tooltip" data-placement="top" title="{{ trans('backpack::crud.delete') }}"
            >
                <i class="la la-trash"></i>
                {{--        {{ trans('backpack::crud.delete') }}--}}
            </a>

        @else
            <span
               data-route="{{ url($crud->route.'/'.$entry->getKey()) }}" class="btn btn-sm btn-outline-danger"
               data-button-type="delete"
               data-toggle="tooltip" data-placement="top" title="{{ $entry->why_cannot_be_deleted }}"
            >
                <i class="la la-trash"></i>
                {{--        {{ trans('backpack::crud.delete') }}--}}
            </span>

        @endif

    @else

        <a href="javascript:void(0)" onclick="deleteEntry(this)"
           data-route="{{ url($crud->route.'/'.$entry->getKey()) }}" class="btn btn-sm btn-danger"
           data-button-type="delete"
           data-toggle="tooltip" data-placement="top" title="{{ trans('backpack::crud.delete') }}"
        >
                <i class="la la-trash"></i>
    {{--        {{ trans('backpack::crud.delete') }}--}}
        </a>

     @endif
@endif

Do you see this as a core feature or an add-on?

I think it could be a core feature, because it's plug-n-play

@realtebo
Copy link
Contributor Author

realtebo commented Jul 12, 2023

Updated version of resources/views/vendor/backpack/crud/buttons/delete.blade.php , simpler, just taken from an updated V6 project

Note: simpler HTML, added a js function to handle cannot-delete buttons.

I am not able to find a disabled class to apply when using tabler theme

@if ($crud->hasAccess('delete'))

        <a href="javascript:void(0)"
           class="btn btn-sm btn-link"
            @if($entry instanceof  \App\Models\Interfaces\CanBeDeletedInterface && $entry->can_be_deleted === false)
                onclick="cannotDeleteEntry(this)"
                data-button-type="canont-delete"
                data-toggle="tooltip" data-placement="top" title="{{ $entry->why_cannot_be_deleted }}"
            @else
                data-button-type="delete"
                onclick="deleteEntry(this)"
                data-route="{{ url($crud->route.'/'.$entry->getKey()) }}"
            @endif
        >
            <span><i class="la la-trash"></i> {{ trans('backpack::crud.delete') }}</span>
        </a>

@endif

{{-- Button Javascript --}}
{{-- - used right away in AJAX operations (ex: List) --}}
{{-- - pushed to the end of the page, after jQuery is loaded, for non-AJAX operations (ex: Show) --}}
@push('after_scripts') @if (request()->ajax()) @endpush @endif
@bassetBlock('backpack/crud/buttons/delete-button-'.app()->getLocale().'.js')
<script>

	if (typeof deleteEntry != 'function') {
	  $("[data-button-type=delete]").unbind('click');

	  function deleteEntry(button) {
		// ask for confirmation before deleting an item
		// e.preventDefault();
		var route = $(button).attr('data-route');

		swal({
		  title: "{!! trans('backpack::base.warning') !!}",
		  text: "{!! trans('backpack::crud.delete_confirm') !!}",
		  icon: "warning",
		  buttons: ["{!! trans('backpack::crud.cancel') !!}", "{!! trans('backpack::crud.delete') !!}"],
		  dangerMode: true,
		}).then((value) => {
			if (value) {
				$.ajax({
			      url: route,
			      type: 'DELETE',
			      success: function(result) {
			          if (result == 1) {
						  // Redraw the table
						  if (typeof crud != 'undefined' && typeof crud.table != 'undefined') {
							  // Move to previous page in case of deleting the only item in table
							  if(crud.table.rows().count() === 1) {
							    crud.table.page("previous");
							  }

							  crud.table.draw(false);
						  }

			          	  // Show a success notification bubble
			              new Noty({
		                    type: "success",
		                    text: "{!! '<strong>'.trans('backpack::crud.delete_confirmation_title').'</strong><br>'.trans('backpack::crud.delete_confirmation_message') !!}"
		                  }).show();

			              // Hide the modal, if any
			              $('.modal').modal('hide');
			          } else {
			              // if the result is an array, it means
			              // we have notification bubbles to show
			          	  if (result instanceof Object) {
			          	  	// trigger one or more bubble notifications
			          	  	Object.entries(result).forEach(function(entry, index) {
			          	  	  var type = entry[0];
			          	  	  entry[1].forEach(function(message, i) {
					          	  new Noty({
				                    type: type,
				                    text: message
				                  }).show();
			          	  	  });
			          	  	});
			          	  } else {// Show an error alert
				              swal({
				              	title: "{!! trans('backpack::crud.delete_confirmation_not_title') !!}",
	                            text: "{!! trans('backpack::crud.delete_confirmation_not_message') !!}",
				              	icon: "error",
				              	timer: 4000,
				              	buttons: false,
				              });
			          	  }
			          }
			      },
			      error: function(result) {
			          // Show an alert with the result
			          swal({
		              	title: "{!! trans('backpack::crud.delete_confirmation_not_title') !!}",
                        text: "{!! trans('backpack::crud.delete_confirmation_not_message') !!}",
		              	icon: "error",
		              	timer: 4000,
		              	buttons: false,
		              });
			      }
			  });
			}
		});

      }
	}

    if (typeof cannotDeleteEntry != 'function') {
        $("[data-button-type=cannot-delete]").unbind('click');

        function cannotDeleteEntry(button) {
            swal({
                title: "{!! trans('backpack::base.warning') !!}",
                text:  $(button).attr('title')  ,
                icon: "warning",
            })
        }
    }

	// make it so that the function above is run after each DataTable draw event
	// crud.addFunctionToDataTablesDrawEventQueue('deleteEntry');
</script>
@endBassetBlock
@if (!request()->ajax()) @endpush @endif

@tabacitu
Copy link
Member

Thanks for sharing your code @realtebo 🙏

I agree we don't have a good solution for this, at the moment. You can't easily allow/deny item-access right now in Backpack. And we should probably work on that.

But I don't think we should go for this solution, but a more general one. One that could apply to ALL operations, not just to Delete. Because ok your use case is to hide the Delete button. But what if the need is to hide the Clone button? Or the Edit button? Then we'd need to do the same thing inside all our Operations, which is not ok - it'll end up pretty spaghetti.

I'll tag this to think about in v6.x when we have more time. Right now we're swamped with the launch of 6.0. Until then I'm sure your code will help someone fix their problem.

Thanks!

@tabacitu
Copy link
Member

tabacitu commented Nov 9, 2023

I have great news @realtebo - we have a solution for this now. And it's beautiful! Check out Access Closures - https://backpackforlaravel.com/articles/tutorials/new-in-v6-granular-user-access-using-custom-closures - using them you can define that the Delete operation is forbidden for some entries. And this will not only hide the button, but also prevent the routes from working. Which is exactly what you needed, I think 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

2 participants