Skip to main content

Actions

A resource may have custom actions. Actions are pieces of code that can be executed in bulk (from the index page), or from the detail page.

By default, every resource includes a DeleteResource action:

(new DeleteResource())
->canRun(fn (Model $model) => auth()->user()->can('delete', $model))
->rejectReason(function () {
return __("You don't have permission to delete this");
})

Creation an action

Let's start by creating a BlockUser action:

class BlockUser extends Action
{
public function icon(): string
{
return 'block';
}

public function title(): string
{
return __('Block user');
}

public function run(Model $model, array $state): void
{
$model->blocked = true;
$model->save();
logout($model);
}
}

The run action will be called for every model that is selected (via bulk).

Registering an action

You can register an action on the resource:

public function actions(): ActionCollection
{
return new ActionCollection(
new BlockUser()
};
}

Authorization and rejection

Visibility

You can hide actions on detail or on index using:

public function actions(): ActionCollection
{
return new ActionCollection(
(new BlockUser())
->hideOnIndex()
};
}

Authorization

Actions will can only be executed when users are authorized to see and run the action. To check whether an action can be seen:

public function actions(): ActionCollection
{
return new ActionCollection(
(new BlockUser())
->canSee(function() {
return auth()->user()->hasPermissionTo('update users');
})
};
}

And to check if it can be run:

public function actions(): ActionCollection
{
return new ActionCollection(
(new BlockUser())
->canRun(fn(UserModel $model) => !$model->blocked)
};
}

Rejection

When, for example in bulk, an action cannot be run for a specific model, you can also supply a reject reason:

public function actions(): ActionCollection
{
return new ActionCollection(
(new BlockUser())
->canRun(fn(UserModel $model) => !$model->blocked)
->rejectReason(function () {
return __('This user is already blocked');
})
};
}

You can optionally hide the eligible and denied model lists by added the following properties to your action:

protected bool $showEligibleModelsList = false;
protected bool $showDeniedModelsList = false;

Including fields

You can include fields in your actions. For example you might want to fill in a block_reason when blocking an user:

class BlockUser extends Action {
//...

public function fields(): FieldCollection
{
return new FieldCollection(
Text::make(__('Block reason'), 'block_reason')
->rules('nullable', 'string', 'max:255')
);
}

public function run(Model $model, array $state): void
{
$model->block_reason = $state['block_reason'];
}

//...
}

Redirecting

Sometimes, you may want to redirect the user to the index page after executing an action. For example after deleting the resource:

public bool $shouldRedirectToIndex = true;
note

This will do nothing if the Action is called from the Index Table, since the table will already be refreshed.

Custom URLs

You can also have a custom redirect url by adding the following method on your Action.

public function redirectTo(Collection $models, array $arguments): null|string|array
{
return 'https://google.com';
}

You can redirect to a specific model detail page:

public function redirectTo(Collection $models, array $arguments): null|string|array
{
return "resources/tickets/{$this->model->id}";
}

// or using helpers & resources
public function redirectTo(Collection $models, array $arguments): null|string|array
{
return front_url(resources('tickets')->url($this->model->id));
}
note

Redirects with Custom URLs will happen when Actions are called from Index as well (not like $shouldRedirectToIndex).

Opening in a new page

You can open one URL or multiple URLs in a new tab by using an array as a return.

// to open google.com in a new tab
public function redirectTo(Collection $models, array $arguments): string|array|null
{
return ['https://google.com'];
}

// to open 3 tabs of google.com
public function redirectTo(Collection $models, array $arguments): string|array|null
{
return [
'https://google.com',
'https://google.com',
'https://google.com',
];
}
info

Oppening in new tabs can be blocked by the browsers popup-blocker, this should be allowed by the user in order to work properly.

Downloading files

It is also possible to return a file response when an action is finished.

In order to make this work, you will need to add the following function, which should return the full path to the file:

public function downloadableFilePath(): ?string
{
return Storage::disk('public_global')->path('avatars/users/1.png');
}
warning

You should not generate a file inside downloadableFilePath function, because it is called multiple times. Instead, you should generate a file in the run function (or somewhere else), and store the path as a property on the class, which you then can return in the downloadableFilePath function.

By default, downloaded files are opened in a new browser tab. You can disable this:

public function openFileInTab(): bool
{
return false;
}
info

After changing anything file related, make sure to refresh the page once

If for some reason you need to send a custom response (with custom headers), you can override the following function:

public function onFinished(Collection $models, array $arguments): mixed
{
return response()->[...]
}

Action metadata

By default, an action description is the same as the title. You can supply your own description:

public function description(): string
{
return __('I am the description for this action');
}

You can set a width for the dialog/modal that is shown (default is 700):

public int $width = 1000;

You can change the labels of submit and cancel buttons:

public function submitLabel(): string
{
return __('Save');
}

public function cancelLabel(): string
{
return __('Cancel');
}

You can hide the submit button by default:

public function showSubmitButton(): bool
{
return $this->model->is_active;
}

You can mark the submit button as disabled by default:

public function disableSubmitButton(): bool
{
return true;
}

public function disableSubmitButtonMessage(): ?string
{
return __('Disabled message');
}

To apply a front-end indicator that the action might be "dangerous", you can use the following property:

public bool $dangerous = true;

You can set widths:

public int $width = 700; // Pixels
public int $maxWidth = 80; // VW

You can set a custom response message:

protected function responseMessage(Collection $models, array $arguments): string
{
return __('Action :action ran successfully', [
'action' => $this->description()
]);
}

If you'd like to modify the form in some other way, you can override the following method:

public function onFormReady(ManagesForm $form): void
{
// On subsequent requests we do nothing
if (!$form->isInitialRequest()) {
return;
}

// Hide the submit button by default
if (!$this->showSubmitButton()) {
$form->hideSubmitButton();
}

// Disable the submit button by default
if ($this->disableSubmitButton()) {
$form->disableSubmitButton($this->disableSubmitButtonMessage());
}
}

Overriding components

You can override the front-end with the following 2 methods:

    public function headerComponent(): string
{
return 'CustomActionHeader';
}

public function bodyComponent(): string
{
return 'CustomActionBody';
}

Then in the front-end:

import CustomActionBody from '../CustomActionBody'
import CustomActionHeader from '../CustomActionHeader'

Vue.component('CustomActionBody', CustomActionBody)
Vue.component('CustomActionHeader', CustomActionHeader)

CustomActionHeader:

<template>
<q-card-section>
<div class="text-h6">
<q-avatar
:icon="action.icon"
size="md"
:color="action.dangerous ? 'negative' : 'primary'"
text-color="contrast"
/>
{{ action.title }}
</div>
<div
class="text-subtitle2"
v-if="action.title !== action.description"
>
{{ action.description }}
</div>
</q-card-section>
</template>

<script>
export default {
props: {
resource: {
type: Object,
required: true
},
action: {
type: Object,
required: true
},
model: {
required: false,
type: Object,
default: null
},
models: {
type: Array,
required: false,
default: () => []
},
relation: {
required: false,
type: Object,
default: undefined
}
}
}
</script>

CustomActionBody:

<template>
<q-card-section class="q-pt-sm">
<base-form
ref="form"
:endpoint="endpoint"
:resource="resource"
:notify="false"
:appendToRequest="{ models: models.map(model => model.id) }"
@ready="(args) => $emit('ready', args)"
hideReset
@success="(response, headers) => $emit('success', response, headers)"
@busy="(busy) => loading = busy"
:submitButtonColor="action.dangerous ? 'negative' : 'primary'"
:fieldsWrapperAttributes="{ style: 'max-height: 70vh;', class: 'scroll q-pt-sm' }"
>
<template v-slot:buttons="{ props }">
<q-btn
flat
:label="props.cancel_label"
:class="{ 'q-mr-sm': props.meta.submit_button_shown }"
color="grey-8 "
v-close-popup
no-caps
@click="$emit('cancel')"
/>
</template>
</base-form>
</q-card-section>
</template>

<script>
export default {
props: {
resource: {
type: Object,
required: true
},
action: {
type: Object,
required: true
},
endpoint: {
type: String,
required: true
},
model: {
required: false,
type: Object,
default: null
},
models: {
type: Array,
required: false,
default: () => []
},
relation: {
required: false,
type: Object,
default: undefined
}
},

methods: {
confirm () {
this.$refs.form.submit()
}
}
}
</script>