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;
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));
}
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',
];
}
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');
}
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;
}
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>