Merge branch '3.x' into pr/11448

This commit is contained in:
Dan Harrin
2024-02-23 19:52:44 +00:00
113 changed files with 2157 additions and 763 deletions
Generated
+198 -193
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -14,9 +14,9 @@
"filament/infolists": "self.version",
"filament/notifications": "self.version",
"filament/support": "self.version",
"illuminate/contracts": "^10.0",
"illuminate/database": "^10.0",
"illuminate/support": "^10.0",
"illuminate/contracts": "^10.45",
"illuminate/database": "^10.45",
"illuminate/support": "^10.45",
"league/csv": "^9.14",
"openspout/openspout": "^4.23",
"spatie/laravel-package-tools": "^1.9"
+1 -1
View File
@@ -293,7 +293,7 @@ Instead of opening in the center of the screen, the modal content will now slide
## Making the modal header sticky
The header of a modal scrolls out of view with the modal content when it overflows the modal size. However, slide-overs have a sticky modal that's always visible. You may control this behavior using `stickyModalHeader()`:
The header of a modal scrolls out of view with the modal content when it overflows the modal size. However, slide-overs have a sticky header that's always visible. You may control this behavior using `stickyModalHeader()`:
```php
Action::make('updateAuthor')
@@ -320,6 +320,18 @@ ImportColumn::make('sku')
})
```
### Adding helper text below the import column
Sometimes, you may wish to provide extra information for the user before validation. You can do this by adding `helperText()` to a column, which gets displayed below the mapping select:
```php
use Filament\Forms\Components\TextInput;
ImportColumn::make('skus')
->array(',')
->helperText('A comma-separated list of SKUs.')
```
## Updating existing records when importing
When generating an importer class, you will see this `resolveRecord()` method:
@@ -124,6 +124,18 @@ ExportColumn::make('description')
->enabledByDefault(false)
```
### Disabling column selection
By default, user will be asked which columns they would like to export. You can disable this functionality using `columnMapping(false)`:
```php
use App\Filament\Exports\ProductExporter;
ExportAction::make()
->exporter(ProductExporter::class)
->columnMapping(false)
```
### Calculated export column state
Sometimes you need to calculate the state of a column, instead of directly reading it from a database column.
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Exporter :label',
'modal' => [
'heading' => 'Exporter :label',
'form' => [
'columns' => [
'label' => 'Colonnes',
'form' => [
'is_enabled' => [
'label' => ':column sélectionnée',
],
'label' => [
'label' => ':column label',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Exporter',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Exportation terminée',
'actions' => [
'download_csv' => [
'label' => 'Télécharger .csv',
],
'download_xlsx' => [
'label' => 'Télécharger .xlsx',
],
],
],
'max_rows' => [
'title' => 'L\'exportation est trop volumineuse',
'body' => 'Vous ne pouvez pas exporter plus d’une ligne à la fois.|Vous ne pouvez pas exporter plus de :count lignes à la fois',
],
'started' => [
'title' => 'L’exportation a commencé',
'body' => 'Votre exportation a commencé et 1 ligne sera traitée en arrière-plan.|Votre exportation a commencé et :count lignes seront traitées en arrière-plan.',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Eksportuj :label',
'modal' => [
'heading' => 'Eksportuj :label',
'form' => [
'columns' => [
'label' => 'Kolumny',
'form' => [
'is_enabled' => [
'label' => ':column włączony',
],
'label' => [
'label' => 'Etykieta :column',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Eksportuj',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Eksport zakończony',
'actions' => [
'download_csv' => [
'label' => 'Pobierz .csv',
],
'download_xlsx' => [
'label' => 'Pobierz .xlsx',
],
],
],
'max_rows' => [
'title' => 'Zbyt dużo wierszy',
'body' => 'Nie możesz wyeksportować więcej niż 1 wiersz na raz.|Nie możesz wyeksportować więcej niż :count wierszy na raz.',
],
'started' => [
'title' => 'Eksport rozpoczęty',
'body' => 'Rozpoczęto eksport i 1 wiersz zostanie przetworzony w tle.|Rozpoczęto eksport i :count wierszy zostanie przetworzonych w tle.',
],
],
'file_name' => 'eksport-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Importuj :label',
'modal' => [
'heading' => 'Importuj :label',
'form' => [
'file' => [
'label' => 'Plik',
'placeholder' => 'Wybierz plik CSV',
],
'columns' => [
'label' => 'Kolumny',
'placeholder' => 'Wybierz kolumnę',
],
],
'actions' => [
'download_example' => [
'label' => 'Pobierz przykładowy plik CSV',
],
'import' => [
'label' => 'Importuj',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Import zakończony',
'actions' => [
'download_failed_rows_csv' => [
'label' => 'Pobierz informacje o niepowodzeniach',
],
],
],
'max_rows' => [
'title' => 'Przesłany plik CSV jest zbyt duży',
'body' => 'Nie możesz zaimportować więcej niż 1 wiersz na raz.|Nie możesz zaimportować więcej niż :count wierszy na raz.',
],
'started' => [
'title' => 'Import rozpoczęty',
'body' => 'Import rozpoczęty i 1 wiersz zostanie przetworzony w tle.|Import rozpoczęty i :count wierszy zostanie przetworzonych w tle.',
],
],
'example_csv' => [
'file_name' => ':importer-example',
],
'failure_csv' => [
'file_name' => 'import-:import_id-:csv_name-failed-rows',
'error_header' => 'błąd',
'system_error' => 'Błąd systemu, skontaktuj się z pomocą techniczną.',
],
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Exportă :label',
'modal' => [
'heading' => 'Exportă :label',
'form' => [
'columns' => [
'label' => 'Coloane',
'form' => [
'is_enabled' => [
'label' => ':column activat',
],
'label' => [
'label' => 'Eticheta :column',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Exportă',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Export complet',
'actions' => [
'download_csv' => [
'label' => 'Descarcă .csv',
],
'download_xlsx' => [
'label' => 'Descarcă .xlsx',
],
],
],
'max_rows' => [
'title' => 'Exportul este prea mare',
'body' => 'Nu puteți exporta mai mult de 1 rând odată.|Nu puteți exporta mai mult de :count rânduri odată',
],
'started' => [
'title' => 'Exportul a început',
'body' => 'Exportul dvs. a început și 1 rând va fi procesat în fundal.|Exportul dvs. a început și :count rânduri vor fi procesate în fundal.',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Importă :label',
'modal' => [
'heading' => 'Importă :label',
'form' => [
'file' => [
'label' => 'Fișier',
'placeholder' => 'Încarcă un fișier CSV',
],
'columns' => [
'label' => 'Coloane',
'placeholder' => 'Alege Coloanele',
],
],
'actions' => [
'download_example' => [
'label' => 'Descarcă exemplu în format CSV',
],
'import' => [
'label' => 'Importă',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Import finalizat',
'actions' => [
'download_failed_rows_csv' => [
'label' => 'Descarcă informațiiler despre câmpul ce a avut eroaro|Descarcă informații despre câmpurile ce au auvt erori',
],
],
],
'max_rows' => [
'title' => 'Fișierul CSV file este prea mare',
'body' => 'Nu este permis importul a mai mult de o linile într-un singur fișier.|Nu este permis imporutl a mai mult de :count linii într-un singur fișier.',
],
'started' => [
'title' => 'Importul a început',
'body' => 'Importul dumneavostră a început, iar rândul va fi procesa în fundal.|Importul dumneavoastră a început, iar cele :count rânduri vor fi procesate în fundal.',
],
],
'example_csv' => [
'file_name' => ':importer-example',
],
'failure_csv' => [
'file_name' => 'import-:import_id-:csv_name-câmpuri-gresite',
'error_header' => 'error',
'system_error' => 'Eroare de sistem, vă rugăm contactați echipa de suport.',
],
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Dışa Aktar :label',
'modal' => [
'heading' => 'Dışa Aktar :label',
'form' => [
'columns' => [
'label' => 'Sütunlar',
'form' => [
'is_enabled' => [
'label' => ':column etkin',
],
'label' => [
'label' => ':column etiketi',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Dışa Aktar',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Dışa Aktarım Tamamlandı',
'actions' => [
'download_csv' => [
'label' => '.csv Olarak İndir',
],
'download_xlsx' => [
'label' => '.xlsx Olarak İndir',
],
],
],
'max_rows' => [
'title' => 'Maksimum Satır Sayısı Aşıldı',
'body' => 'Birden fazla satırı dışa aktaramazsınız.|:count satırı dışa aktaramazsınız.',
],
'started' => [
'title' => 'Dışa Aktarım Başladı',
'body' => 'Dışa aktarım başladı ve 1 satır arka planda işlenecek.|Dışa aktarım başladı ve :count satır arka planda işlenecek.',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'İçe Aktar: :label',
'modal' => [
'heading' => 'İçe Aktar: :label',
'form' => [
'file' => [
'label' => 'Dosya',
'placeholder' => 'Bir CSV dosyası seçin',
],
'columns' => [
'label' => 'Kolonlar',
'placeholder' => 'Kolonları eşleştirin',
],
],
'actions' => [
'download_example' => [
'label' => 'Örnek CSV Dosyasını İndir',
],
'import' => [
'label' => 'İçe Aktar',
],
],
],
'notifications' => [
'completed' => [
'title' => 'İçe Aktarım Tamamlandı',
'actions' => [
'download_failed_rows_csv' => [
'label' => 'Başarısız satır hakkında bilgileri indir|Başarısız satırlar hakkında bilgileri indir',
],
],
],
'max_rows' => [
'title' => 'Yüklenen Dosya Çok Büyük',
'body' => 'Aynı anda 1\'den fazla satır içeren dosyaları içe aktaramazsınız.|Aynı anda :count\'den fazla satır içeren dosyaları içe aktaramazsınız.',
],
'started' => [
'title' => 'İçe Aktarım Başladı',
'body' => 'İçe aktarım başladı ve 1 satır arka planda işlenecek.|İçe aktarım başladı ve :count satır arka planda işlenecek.',
],
],
'example_csv' => [
'file_name' => ':importer-example',
],
'failure_csv' => [
'file_name' => 'import-:import_id-:csv_name-failed-rows',
'error_header' => 'hata',
'system_error' => 'Sistem Hatası',
],
];
@@ -25,11 +25,10 @@ use Illuminate\Foundation\Bus\PendingChain;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Number;
use Illuminate\Support\Str;
use Livewire\Component;
use function Filament\Support\format_number;
trait CanExportRecords
{
/**
@@ -61,6 +60,8 @@ trait CanExportRecords
protected ?Closure $modifyQueryUsing = null;
protected bool | Closure $hasColumnMapping = true;
protected function setUp(): void
{
parent::setUp();
@@ -73,8 +74,8 @@ trait CanExportRecords
$this->groupedIcon(FilamentIcon::resolve('actions::export-action.grouped') ?? 'heroicon-m-arrow-down-tray');
$this->form(fn (ExportAction | ExportTableAction | ExportTableBulkAction $action): array => array_merge([
Fieldset::make(__('filament-actions::export.modal.form.columns.label'))
$this->form(fn (ExportAction | ExportTableAction | ExportTableBulkAction $action): array => [
...($action->hasColumnMapping() ? [Fieldset::make(__('filament-actions::export.modal.form.columns.label'))
->columns(1)
->inlineLabel()
->schema(function () use ($action): array {
@@ -99,8 +100,9 @@ trait CanExportRecords
$action->getExporter()::getColumns(),
);
})
->statePath('columnMap'),
], $action->getExporter()::getOptionsFormComponents()));
->statePath('columnMap')] : []),
...$action->getExporter()::getOptionsFormComponents(),
]);
$this->action(function (ExportAction | ExportTableAction | ExportTableBulkAction $action, array $data, Component $livewire) {
if ($livewire instanceof HasTable) {
@@ -124,7 +126,7 @@ trait CanExportRecords
Notification::make()
->title(__('filament-actions::export.notifications.max_rows.title'))
->body(trans_choice('filament-actions::export.notifications.max_rows.body', $maxRows, [
'count' => format_number($maxRows),
'count' => Number::format($maxRows),
]))
->danger()
->send();
@@ -139,14 +141,20 @@ trait CanExportRecords
Arr::except($data, ['columnMap']),
);
$columnMap = collect($data['columnMap'])
->dot()
->reduce(fn (Collection $carry, mixed $value, string $key): Collection => $carry->mergeRecursive([
Str::beforeLast($key, '.') => [Str::afterLast($key, '.') => $value],
]), collect())
->filter(fn (array $column): bool => $column['isEnabled'] ?? false)
->mapWithKeys(fn (array $column, string $columnName): array => [$columnName => $column['label']])
->all();
if ($action->hasColumnMapping()) {
$columnMap = collect($data['columnMap'])
->dot()
->reduce(fn (Collection $carry, mixed $value, string $key): Collection => $carry->mergeRecursive([
Str::beforeLast($key, '.') => [Str::afterLast($key, '.') => $value],
]), collect())
->filter(fn (array $column): bool => $column['isEnabled'] ?? false)
->mapWithKeys(fn (array $column, string $columnName): array => [$columnName => $column['label']])
->all();
} else {
$columnMap = collect($action->getExporter()::getColumns())
->mapWithKeys(fn (ExportColumn $column): array => [$column->getName() => $column->getLabel()])
->all();
}
$export = app(Export::class);
$export->user()->associate($user);
@@ -230,7 +238,7 @@ trait CanExportRecords
Notification::make()
->title($action->getSuccessNotificationTitle())
->body(trans_choice('filament-actions::export.notifications.started.body', $export->total_rows, [
'count' => format_number($export->total_rows),
'count' => Number::format($export->total_rows),
]))
->success()
->send();
@@ -387,4 +395,16 @@ trait CanExportRecords
return $this;
}
public function columnMapping(bool | Closure $condition = true): static
{
$this->hasColumnMapping = $condition;
return $this;
}
public function hasColumnMapping(): bool
{
return (bool) $this->evaluate($this->hasColumnMapping);
}
}
@@ -26,6 +26,7 @@ use Illuminate\Filesystem\AwsS3V3Adapter;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Number;
use League\Csv\Info;
use League\Csv\Reader as CsvReader;
use League\Csv\Statement;
@@ -34,8 +35,6 @@ use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use SplTempFileObject;
use Symfony\Component\HttpFoundation\StreamedResponse;
use function Filament\Support\format_number;
trait CanImportRecords
{
/**
@@ -181,7 +180,7 @@ trait CanImportRecords
Notification::make()
->title(__('filament-actions::import.notifications.max_rows.title'))
->body(trans_choice('filament-actions::import.notifications.max_rows.body', $maxRows, [
'count' => format_number($maxRows),
'count' => Number::format($maxRows),
]))
->danger()
->send();
@@ -271,7 +270,7 @@ trait CanImportRecords
fn (Notification $notification) => $notification->actions([
NotificationAction::make('downloadFailedRowsCsv')
->label(trans_choice('filament-actions::import.notifications.completed.actions.download_failed_rows_csv.label', $failedRowsCount, [
'count' => format_number($failedRowsCount),
'count' => Number::format($failedRowsCount),
]))
->color('danger')
->url(route('filament.imports.failed-rows.download', ['import' => $import], absolute: false), shouldOpenInNewTab: true)
@@ -285,7 +284,7 @@ trait CanImportRecords
Notification::make()
->title($action->getSuccessNotificationTitle())
->body(trans_choice('filament-actions::import.notifications.started.body', $import->total_rows, [
'count' => format_number($import->total_rows),
'count' => Number::format($import->total_rows),
]))
->success()
->send();
@@ -0,0 +1,50 @@
<?php
namespace Filament\Actions\Concerns;
use Closure;
use Illuminate\Support\Facades\DB;
trait CanUseDatabaseTransactions
{
protected bool | Closure $hasDatabaseTransactions = false;
public function databaseTransaction(bool | Closure $condition = true): static
{
$this->hasDatabaseTransactions = $condition;
return $this;
}
public function hasDatabaseTransactions(): bool
{
return (bool) $this->evaluate($this->hasDatabaseTransactions);
}
public function beginDatabaseTransaction(): void
{
if (! $this->hasDatabaseTransactions()) {
return;
}
DB::beginTransaction();
}
public function commitDatabaseTransaction(): void
{
if (! $this->hasDatabaseTransactions()) {
return;
}
DB::commit();
}
public function rollBackDatabaseTransaction(): void
{
if (! $this->hasDatabaseTransactions()) {
return;
}
DB::rollBack();
}
}
@@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Livewire\Attributes\Url;
use Throwable;
use function Livewire\store;
@@ -79,6 +80,8 @@ trait InteractsWithActions
$originallyMountedActions = $this->mountedActions;
try {
$action->beginDatabaseTransaction();
if ($this->mountedActionHasForm()) {
$action->callBeforeFormValidated();
@@ -96,10 +99,21 @@ trait InteractsWithActions
$result = $action->callAfter() ?? $result;
$this->afterActionCalled();
$action->commitDatabaseTransaction();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
$action->rollBackDatabaseTransaction() :
$action->commitDatabaseTransaction();
return null;
} catch (Cancel $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
$action->rollBackDatabaseTransaction() :
$action->commitDatabaseTransaction();
} catch (ValidationException $exception) {
$action->rollBackDatabaseTransaction();
if (! $this->mountedActionShouldOpenModal()) {
$action->resetArguments();
$action->resetFormData();
@@ -107,6 +121,10 @@ trait InteractsWithActions
$this->unmountAction();
}
throw $exception;
} catch (Throwable $exception) {
$action->rollBackDatabaseTransaction();
throw $exception;
}
+2
View File
@@ -47,6 +47,8 @@ class CreateAction extends Action
$this->record(null);
$this->databaseTransaction();
$this->action(function (array $arguments, Form $form): void {
$model = $this->getModel();
+2
View File
@@ -47,6 +47,8 @@ class EditAction extends Action
return $data;
});
$this->databaseTransaction();
$this->action(function (): void {
$this->process(function (array $data, HasActions $livewire, Model $record) {
if ($translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver()) {
+12 -1
View File
@@ -5,6 +5,7 @@ namespace Filament\Actions\Imports;
use Closure;
use Filament\Forms\Components\Select;
use Filament\Support\Components\Component;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
@@ -70,6 +71,8 @@ class ImportColumn extends Component
protected string $evaluationIdentifier = 'column';
protected string | Htmlable | Closure | null $helperText = null;
final public function __construct(string $name)
{
$this->name($name);
@@ -88,7 +91,8 @@ class ImportColumn extends Component
return Select::make($this->getName())
->label($this->getLabel())
->placeholder(__('filament-actions::import.modal.form.columns.placeholder'))
->required($this->isMappingRequired());
->required($this->isMappingRequired())
->helperText($this->helperText);
}
public function name(string $name): static
@@ -134,6 +138,13 @@ class ImportColumn extends Component
return $this;
}
public function helperText(string | Htmlable | Closure | null $text): static
{
$this->helperText = $text;
return $this;
}
public function integer(bool | Closure $condition = true): static
{
$this->numeric($condition, decimalPlaces: 0);
+21 -2
View File
@@ -80,7 +80,12 @@ abstract class Importer
foreach ($this->getCachedColumns() as $column) {
$columnName = $column->getName();
$rowColumnName = $this->columnMap[$columnName] ?? null;
if (blank($this->columnMap[$columnName] ?? null)) {
continue;
}
$rowColumnName = $this->columnMap[$columnName];
if (! array_key_exists($rowColumnName, $this->data)) {
continue;
@@ -141,6 +146,10 @@ abstract class Importer
foreach ($this->getCachedColumns() as $column) {
$columnName = $column->getName();
if (blank($this->columnMap[$columnName] ?? null)) {
continue;
}
$rules[$columnName] = $column->getDataValidationRules();
if (
@@ -170,13 +179,19 @@ abstract class Importer
$attributes = [];
foreach ($this->getCachedColumns() as $column) {
$columnName = $column->getName();
if (blank($this->columnMap[$columnName] ?? null)) {
continue;
}
$validationAttribute = $column->getValidationAttribute();
if (blank($validationAttribute)) {
continue;
}
$attributes[$column->getName()] = $validationAttribute;
$attributes[$columnName] = $validationAttribute;
}
return $attributes;
@@ -187,6 +202,10 @@ abstract class Importer
foreach ($this->getCachedColumns() as $column) {
$columnName = $column->getName();
if (blank($this->columnMap[$columnName] ?? null)) {
continue;
}
if (! array_key_exists($columnName, $this->data)) {
continue;
}
+5 -4
View File
@@ -15,6 +15,7 @@ abstract class MountableAction extends StaticAction implements HasLivewire
use Concerns\CanOpenModal;
use Concerns\CanRedirect;
use Concerns\CanRequireConfirmation;
use Concerns\CanUseDatabaseTransactions;
use Concerns\HasForm;
use Concerns\HasInfolist;
use Concerns\HasLifecycleHooks;
@@ -39,14 +40,14 @@ abstract class MountableAction extends StaticAction implements HasLivewire
return $this->evaluate($this->getActionFunction(), $parameters);
}
public function cancel(): void
public function cancel(bool $shouldRollBackDatabaseTransaction = false): void
{
throw new Cancel();
throw (new Cancel())->rollBackDatabaseTransaction($shouldRollBackDatabaseTransaction);
}
public function halt(): void
public function halt(bool $shouldRollBackDatabaseTransaction = false): void
{
throw new Halt();
throw (new Halt())->rollBackDatabaseTransaction($shouldRollBackDatabaseTransaction);
}
/**
+1 -2
View File
@@ -1,10 +1,9 @@
<?php
use Closure;
namespace Livewire\Features\SupportTesting {
use Illuminate\Support\Collection;
use Closure;
class Testable {
public function fillForm(array $state = [], string $formName = 'form'): static {}
+7 -7
View File
@@ -12,13 +12,13 @@
"danharrin/date-format-converter": "^0.3",
"filament/actions": "self.version",
"filament/support": "self.version",
"illuminate/console": "^10.0",
"illuminate/contracts": "^10.0",
"illuminate/database": "^10.0",
"illuminate/filesystem": "^10.0",
"illuminate/support": "^10.0",
"illuminate/validation": "^10.0",
"illuminate/view": "^10.0",
"illuminate/console": "^10.45",
"illuminate/contracts": "^10.45",
"illuminate/database": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/support": "^10.45",
"illuminate/validation": "^10.45",
"illuminate/view": "^10.45",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
@@ -264,6 +264,21 @@ Select::make('author_id')
->searchable(['first_name', 'last_name'])
```
### Saving pivot data to the relationship
If you're using a `multiple()` relationship and your pivot table has additional columns, you can use the `pivotData()` method to specify the data that should be saved in them:
```php
use Filament\Forms\Components\Select;
Select::make('primaryTechnologies')
->relationship(name: 'technologies', titleAttribute: 'name')
->multiple()
->pivotData([
'is_primary' => true,
])
```
### Creating a new option in a modal
You may define a custom form that can be used to create a new record and attach it to the `BelongsTo` relationship:
@@ -240,6 +240,20 @@ CheckboxList::make('authors')
->getOptionLabelFromRecordUsing(fn (Model $record) => "{$record->first_name} {$record->last_name}")
```
### Saving pivot data to the relationship
If your pivot table has additional columns, you can use the `pivotData()` method to specify the data that should be saved in them:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('primaryTechnologies')
->relationship(name: 'technologies', titleAttribute: 'name')
->pivotData([
'is_primary' => true,
])
```
## Setting a custom no search results message
When you're using a searchable checkbox list, you may want to display a custom message when no search results are found. You can do this using the `noSearchResultsMessage()` method:
+3 -3
View File
@@ -234,7 +234,7 @@ Repeater::make('qualifications')
### Reordering items in a relationship
By default, [reordering](#reordering-items) relationship repeater items is disabled. This is because your related model needs an `sort` column to store the order of related records. To enable reordering, you may use the `orderColumn()` method, passing in a name of the column on your related model to store the order in:
By default, [reordering](#reordering-items) relationship repeater items is disabled. This is because your related model needs a `sort` column to store the order of related records. To enable reordering, you may use the `orderColumn()` method, passing in a name of the column on your related model to store the order in:
```php
use Filament\Forms\Components\Repeater;
@@ -287,7 +287,7 @@ class OrderProduct extends Pivot
{
return $this->belongsTo(Order::class);
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
@@ -634,7 +634,7 @@ Repeater::make('members')
->icon('heroicon-m-envelope')
->action(function (array $arguments, Repeater $component): void {
$itemData = $component->getItemState($arguments['item']);
Mail::to($itemData['email'])
->send(
// ...
+1 -1
View File
@@ -26,7 +26,7 @@ This assumes that you have a `resources/views/filament/forms/components/wizard.b
You may create your own custom component classes and views, which you can reuse across your project, and even release as a plugin to the community.
> If you're just creating a simple custom component to use once, you could instead use a [view component](#view) to render any custom Blade file.
> If you're just creating a simple custom component to use once, you could instead use a [view component](#view-components) to render any custom Blade file.
To create a custom column class and view, you may use the following command:
@@ -189,7 +189,7 @@ return [
'messages' => [
'confirmation' => 'Não é recomendado editar ficheiros SVG, pois pode resultar em perda de qualidade ao redimensionar.\n Tem a certeza de que deseja prosseguir?',
'disabled' => 'A edição de ficheiros SVG está desativada, pois pode resultar em perda de qualidade ao redimensionar.',
'disabled' => 'A edição de ficheiros SVG está desactivada, pois pode resultar em perda de qualidade ao redimensionar.',
],
],
@@ -155,6 +155,7 @@ trait CanGenerateForms
$parameterValue = match (true) {
/** @phpstan-ignore-next-line */
is_bool($parameterValue) => $parameterValue ? 'true' : 'false',
/** @phpstan-ignore-next-line */
is_null($parameterValue) => 'null',
is_numeric($parameterValue) => $parameterValue,
default => "'{$parameterValue}'",
+10 -1
View File
@@ -24,6 +24,7 @@ class CheckboxList extends Field implements Contracts\CanDisableOptions, Contrac
use Concerns\HasGridDirection;
use Concerns\HasNestedRecursiveValidationRules;
use Concerns\HasOptions;
use Concerns\HasPivotData;
/**
* @var view-string
@@ -185,7 +186,15 @@ class CheckboxList extends Field implements Contracts\CanDisableOptions, Contrac
});
$this->saveRelationshipsUsing(static function (CheckboxList $component, ?array $state) {
$component->getRelationship()->sync($state ?? []);
$pivotData = $component->getPivotData();
if ($pivotData === []) {
$component->getRelationship()->sync($state ?? []);
return;
}
$component->getRelationship()->syncWithPivotValues($state ?? [], $pivotData);
});
$this->dehydrated(false);
@@ -0,0 +1,31 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasPivotData
{
/**
* @var array<string, mixed> | Closure
*/
protected array | Closure $pivotData = [];
/**
* @param array<string, mixed> | Closure $data
*/
public function pivotData(array | Closure $data): static
{
$this->pivotData = $data;
return $this;
}
/**
* @return array<string, mixed>
*/
public function getPivotData(): array
{
return $this->evaluate($this->pivotData) ?? [];
}
}
+19 -6
View File
@@ -18,8 +18,8 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
@@ -46,6 +46,7 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
use Concerns\HasLoadingMessage;
use Concerns\HasNestedRecursiveValidationRules;
use Concerns\HasOptions;
use Concerns\HasPivotData;
use Concerns\HasPlaceholder;
use HasExtraAlpineAttributes;
@@ -829,7 +830,7 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
}
if (
($relationship instanceof HasOneThrough) ||
($relationship instanceof HasManyThrough) ||
($relationship instanceof \Znck\Eloquent\Relations\BelongsToThrough)
) {
$relatedModel = $relationship->getResults();
@@ -952,7 +953,11 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
$this->saveRelationshipsUsing(static function (Select $component, Model $record, $state) {
$relationship = $component->getRelationship();
if ($relationship instanceof \Znck\Eloquent\Relations\BelongsToThrough) {
if (
($relationship instanceof HasOneOrMany) ||
($relationship instanceof HasManyThrough) ||
($relationship instanceof \Znck\Eloquent\Relations\BelongsToThrough)
) {
return;
}
@@ -963,7 +968,15 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
return;
}
$relationship->sync($state ?? []);
$pivotData = $component->getPivotData();
if ($pivotData === []) {
$relationship->sync($state ?? []);
return;
}
$relationship->syncWithPivotValues($state ?? [], $pivotData);
});
$this->createOptionUsing(static function (Select $component, array $data, Form $form) {
@@ -1063,7 +1076,7 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
return parent::getLabel();
}
public function getRelationship(): BelongsTo | BelongsToMany | HasOneOrMany | HasOneThrough | \Znck\Eloquent\Relations\BelongsToThrough | null
public function getRelationship(): BelongsTo | BelongsToMany | HasOneOrMany | HasManyThrough | \Znck\Eloquent\Relations\BelongsToThrough | null
{
if (blank($this->getRelationshipName())) {
return null;
@@ -1188,7 +1201,7 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
return $relationship->getQualifiedRelatedKeyName();
}
if ($relationship instanceof HasOneThrough) {
if ($relationship instanceof HasManyThrough) {
return $relationship->getQualifiedForeignKeyName();
}
@@ -9,6 +9,7 @@ use Filament\Forms\Form;
use Filament\Support\Exceptions\Cancel;
use Filament\Support\Exceptions\Halt;
use Illuminate\Validation\ValidationException;
use Throwable;
use function Livewire\store;
@@ -59,6 +60,8 @@ trait HasFormComponentActions
$result = null;
try {
$action->beginDatabaseTransaction();
if ($this->mountedFormComponentActionHasForm()) {
$action->callBeforeFormValidated();
@@ -74,10 +77,21 @@ trait HasFormComponentActions
]);
$result = $action->callAfter() ?? $result;
$action->commitDatabaseTransaction();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
$action->rollBackDatabaseTransaction() :
$action->commitDatabaseTransaction();
return null;
} catch (Cancel $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
$action->rollBackDatabaseTransaction() :
$action->commitDatabaseTransaction();
} catch (ValidationException $exception) {
$action->rollBackDatabaseTransaction();
if (! $this->mountedFormComponentActionShouldOpenModal()) {
$action->resetArguments();
$action->resetFormData();
@@ -85,6 +99,10 @@ trait HasFormComponentActions
$this->unmountFormComponentAction();
}
throw $exception;
} catch (Throwable $exception) {
$action->rollBackDatabaseTransaction();
throw $exception;
}
+6 -6
View File
@@ -11,12 +11,12 @@
"php": "^8.1",
"filament/actions": "self.version",
"filament/support": "self.version",
"illuminate/console": "^10.0",
"illuminate/contracts": "^10.0",
"illuminate/database": "^10.0",
"illuminate/filesystem": "^10.0",
"illuminate/support": "^10.0",
"illuminate/view": "^10.0",
"illuminate/console": "^10.45",
"illuminate/contracts": "^10.45",
"illuminate/database": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/support": "^10.45",
"illuminate/view": "^10.45",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
+45 -6
View File
@@ -58,17 +58,39 @@ TextEntry::make('created_at')
## Number formatting
The `numeric()` method allows you to format an entry as a number, using PHP's `number_format()`:
The `numeric()` method allows you to format an entry as a number:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('stock')
->numeric(
decimalPlaces: 0,
decimalSeparator: '.',
thousandsSeparator: ',',
)
->numeric()
```
If you would like to customize the number of decimal places used to format the number with, you can use the `decimalPlaces` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('stock')
->numeric(decimalPlaces: 0)
```
By default, your app's locale will be used to format the number suitably. If you would like to customize the locale used, you can pass it to the `locale` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('stock')
->numeric(locale: 'nl')
```
Alternatively, you can set the default locale used across your app using the `Number::useLocale()` method in the `boot()` method of a service provider:
```php
use Illuminate\Support\Number;
Number::useLocale('nl');
```
## Currency formatting
@@ -91,6 +113,23 @@ TextEntry::make('price')
->money('EUR', divideBy: 100)
```
By default, your app's locale will be used to format the money suitably. If you would like to customize the locale used, you can pass it to the `locale` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('price')
->money(currency: 'EUR', locale: 'nl')
```
Alternatively, you can set the default locale used across your app using the `Number::useLocale()` method in the `boot()` method of a service provider:
```php
use Illuminate\Support\Number;
Number::useLocale('nl');
```
## Limiting text length
You may `limit()` the length of the entry's value:
@@ -11,10 +11,15 @@
$height = $getHeight() ?? ($isStacked ? '2.5rem' : '8rem');
$width = $getWidth() ?? (($isCircular || $isSquare) ? $height : null);
$stateCount = count($state);
$limitedStateCount = count($limitedState);
$defaultImageUrl = $getDefaultImageUrl();
if ((! count($limitedState)) && filled($defaultImageUrl)) {
if ((! $limitedStateCount) && filled($defaultImageUrl)) {
$limitedState = [null];
$limitedStateCount = 1;
}
$ringClasses = \Illuminate\Support\Arr::toCssClasses([
@@ -29,7 +34,7 @@
},
]);
$hasLimitedRemainingText = $hasLimitedRemainingText();
$hasLimitedRemainingText = $hasLimitedRemainingText() && ($limitedStateCount < $stateCount);
$isLimitedRemainingTextSeparate = $isLimitedRemainingTextSeparate();
$limitedRemainingTextSizeClasses = match ($getLimitedRemainingTextSize()) {
@@ -50,7 +55,7 @@
])
}}
>
@if (count($limitedState))
@if ($limitedStateCount)
<div
@class([
'flex flex-wrap',
@@ -86,7 +91,7 @@
/>
@endforeach
@if ($hasLimitedRemainingText && ($loop->iteration < count($limitedState)) && (! $isLimitedRemainingTextSeparate) && $isCircular)
@if ($hasLimitedRemainingText && (! $isLimitedRemainingTextSeparate) && $isCircular)
<div
style="
@if ($height) height: {{ $height }}; @endif
@@ -104,20 +109,20 @@
])
>
<span class="-ms-0.5">
+{{ count($state) - count($limitedState) }}
+{{ $stateCount - $limitedStateCount }}
</span>
</div>
@endif
</div>
@if ($hasLimitedRemainingText && ($loop->iteration < count($limitedState)) && ($isLimitedRemainingTextSeparate || (! $isCircular)))
@if ($hasLimitedRemainingText && ($isLimitedRemainingTextSeparate || (! $isCircular)))
<div
@class([
'font-medium text-gray-500 dark:text-gray-400',
$limitedRemainingTextSizeClasses,
])
>
+{{ count($state) - count($limitedState) }}
+{{ $stateCount - $limitedStateCount }}
</div>
@endif
@elseif (($placeholder = $getPlaceholder()) !== null)
@@ -6,13 +6,13 @@ use Closure;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Infolist;
use Filament\Support\Contracts\HasLabel as LabelInterface;
use Filament\Support\Enums\ArgumentValue;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Number;
use Illuminate\Support\Str;
use function Filament\Support\format_money;
use function Filament\Support\format_number;
trait CanFormatState
{
protected ?Closure $formatStateUsing = null;
@@ -99,28 +99,11 @@ trait CanFormatState
return $this;
}
public function money(string | Closure | null $currency = null, int $divideBy = 0): static
public function money(string | Closure | null $currency = null, int $divideBy = 0, string | Closure | null $locale = null): static
{
$this->isMoney = true;
$this->formatStateUsing(static function (TextEntry $component, $state) use ($currency, $divideBy): ?string {
if (blank($state)) {
return null;
}
$currency = $component->evaluate($currency) ?? Infolist::$defaultCurrency;
return format_money($state, $currency, $divideBy);
});
return $this;
}
public function numeric(int | Closure | null $decimalPlaces = null, string | Closure | null $decimalSeparator = '.', string | Closure | null $thousandsSeparator = ','): static
{
$this->isNumeric = true;
$this->formatStateUsing(static function (TextEntry $component, $state) use ($decimalPlaces, $decimalSeparator, $thousandsSeparator): ?string {
$this->formatStateUsing(static function (TextEntry $component, $state) use ($currency, $divideBy, $locale): ?string {
if (blank($state)) {
return null;
}
@@ -129,16 +112,47 @@ trait CanFormatState
return $state;
}
if ($decimalPlaces === null) {
return format_number($state);
$currency = $component->evaluate($currency) ?? Table::$defaultCurrency;
if ($divideBy) {
$state /= $divideBy;
}
return number_format(
$state,
$component->evaluate($decimalPlaces),
$component->evaluate($decimalSeparator),
$component->evaluate($thousandsSeparator),
);
return Number::currency($state, $currency, $component->evaluate($locale));
});
return $this;
}
public function numeric(int | Closure | null $decimalPlaces = null, string | Closure | null | ArgumentValue $decimalSeparator = ArgumentValue::Default, string | Closure | null | ArgumentValue $thousandsSeparator = ArgumentValue::Default, int | Closure | null $maxDecimalPlaces = null, string | Closure | null $locale = null): static
{
$this->isNumeric = true;
$this->formatStateUsing(static function (TextEntry $component, $state) use ($decimalPlaces, $decimalSeparator, $locale, $maxDecimalPlaces, $thousandsSeparator): ?string {
if (blank($state)) {
return null;
}
if (! is_numeric($state)) {
return $state;
}
$decimalSeparator = $component->evaluate($decimalSeparator);
$thousandsSeparator = $component->evaluate($thousandsSeparator);
if (
($decimalSeparator !== ArgumentValue::Default) ||
($thousandsSeparator !== ArgumentValue::Default)
) {
return number_format(
$state,
$component->evaluate($decimalPlaces),
$decimalSeparator === ArgumentValue::Default ? '.' : $decimalSeparator,
$thousandsSeparator === ArgumentValue::Default ? ',' : $thousandsSeparator,
);
}
return Number::format($state, $decimalPlaces, $maxDecimalPlaces, $component->evaluate($locale));
});
return $this;
@@ -14,6 +14,7 @@ use Filament\Support\Exceptions\Cancel;
use Filament\Support\Exceptions\Halt;
use Filament\Tables\Contracts\HasTable;
use Illuminate\Validation\ValidationException;
use Throwable;
use function Livewire\store;
@@ -103,6 +104,8 @@ trait InteractsWithInfolists
$result = null;
try {
$action->beginDatabaseTransaction();
if ($this->mountedInfolistActionHasForm()) {
$action->callBeforeFormValidated();
@@ -118,10 +121,21 @@ trait InteractsWithInfolists
]);
$result = $action->callAfter() ?? $result;
$action->commitDatabaseTransaction();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
$action->rollBackDatabaseTransaction() :
$action->commitDatabaseTransaction();
return null;
} catch (Cancel $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
$action->rollBackDatabaseTransaction() :
$action->commitDatabaseTransaction();
} catch (ValidationException $exception) {
$action->rollBackDatabaseTransaction();
if (! $this->mountedInfolistActionShouldOpenModal()) {
$action->resetArguments();
$action->resetFormData();
@@ -129,6 +143,10 @@ trait InteractsWithInfolists
$this->unmountInfolistAction();
}
throw $exception;
} catch (Throwable $exception) {
$action->rollBackDatabaseTransaction();
throw $exception;
}
+4 -4
View File
@@ -11,10 +11,10 @@
"php": "^8.1",
"filament/actions": "self.version",
"filament/support": "self.version",
"illuminate/contracts": "^10.0",
"illuminate/filesystem": "^10.0",
"illuminate/notifications": "^10.0",
"illuminate/support": "^10.0",
"illuminate/contracts": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/notifications": "^10.45",
"illuminate/support": "^10.45",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
+10 -10
View File
@@ -17,16 +17,16 @@
"filament/support": "self.version",
"filament/tables": "self.version",
"filament/widgets": "self.version",
"illuminate/auth": "^10.0",
"illuminate/console": "^10.0",
"illuminate/contracts": "^10.0",
"illuminate/cookie": "^10.0",
"illuminate/database": "^10.0",
"illuminate/http": "^10.0",
"illuminate/routing": "^10.0",
"illuminate/session": "^10.0",
"illuminate/support": "^10.0",
"illuminate/view": "^10.0",
"illuminate/auth": "^10.45",
"illuminate/console": "^10.45",
"illuminate/contracts": "^10.45",
"illuminate/cookie": "^10.45",
"illuminate/database": "^10.45",
"illuminate/http": "^10.45",
"illuminate/routing": "^10.45",
"illuminate/session": "^10.45",
"illuminate/support": "^10.45",
"illuminate/view": "^10.45",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
+1 -1
View File
File diff suppressed because one or more lines are too long
+10
View File
@@ -229,6 +229,16 @@ public function panel(Panel $panel): Panel
}
```
### Navigation groups in a collapsible sidebar on desktop
> This section only applies to `sidebarCollapsibleOnDesktop()`, not `sidebarFullyCollapsibleOnDesktop()`, since the fully collapsible UI just hides the entire sidebar instead of changing its design.
When using a collapsible sidebar on desktop, you will also often be using [navigation groups](#grouping-navigation-items). By default, the labels of each navigation group will be hidden when the sidebar is collapsed, since there is no space to display them. Even if the navigation group itself is [collapsible](#making-navigation-groups-not-collapsible), all items will still be visible in the collapsed sidebar, since there is no group label to click on to expand the group.
These issues can be solved, to achieve a very minimal sidebar design, by [passing an `icon()`](#customizing-navigation-groups) to the navigation group objects. When an icon is defined, the icon will be displayed in the collapsed sidebar instead of the items at all times. When the icon is clicked, a dropdown will open to the side of the icon, revealing the items in the group.
When passing an icon to a navigation group, even if the items also have icons, the expanded sidebar UI will not show the item icons. This is to keep the navigation hierarchy clear, and the design minimal. However, the icons for the items will be shown in the collapsed sidebar's dropdowns though, since the hierarchy is already clear from the fact that the dropdown is open.
## Registering custom navigation items
To register new navigation items, you can use the [configuration](configuration):
+5 -1
View File
@@ -37,7 +37,11 @@ document.addEventListener('alpine:init', () => {
},
})
const theme = localStorage.getItem('theme') ?? 'system'
const theme =
localStorage.getItem('theme') ??
getComputedStyle(document.documentElement).getPropertyValue(
'--default-theme-mode',
)
window.Alpine.store(
'theme',
@@ -43,7 +43,7 @@ return [
'actions' => [
'cancel' => [
'label' => 'indietro',
'label' => 'Indietro',
],
],
@@ -4,4 +4,30 @@ return [
'title' => 'Panoul de control',
'actions' => [
'filter' => [
'label' => 'Filtru',
'modal' => [
'heading' => 'Filtru',
'actions' => [
'apply' => [
'label' => 'Aplică',
],
],
],
],
],
];
@@ -24,7 +24,7 @@
<title>
{{ filled($title = strip_tags(($livewire ?? null)?->getTitle() ?? '')) ? "{$title} - " : null }}
{{ filament()->getBrandName() }}
{{ strip_tags(filament()->getBrandName()) }}
</title>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::STYLES_BEFORE, scopes: $livewire->getRenderHookScopes()) }}
@@ -59,6 +59,7 @@
--font-family: '{!! filament()->getFontFamily() !!}';
--sidebar-width: {{ filament()->getSidebarWidth() }};
--collapsed-sidebar-width: {{ filament()->getCollapsedSidebarWidth() }};
--default-theme-mode: {{ filament()->getDefaultThemeMode()->value }};
}
</style>
@@ -5,20 +5,10 @@
@endphp
<x-filament-panels::layout.base :livewire="$livewire">
<div class="fi-layout flex min-h-screen w-full overflow-x-clip">
@if (filament()->hasNavigation())
<div
x-cloak
x-data="{}"
x-on:click="$store.sidebar.close()"
x-show="$store.sidebar.isOpen"
x-transition.opacity.300ms
class="fi-sidebar-close-overlay fixed inset-0 z-30 bg-gray-950/50 transition duration-500 dark:bg-gray-950/75 lg:hidden"
></div>
<x-filament-panels::sidebar :navigation="$navigation" />
@endif
{{-- The sidebar is after the page content in the markup to fix issues with page content overlapping dropdown content from the sidebar. --}}
<div
class="fi-layout flex min-h-screen w-full flex-row-reverse overflow-x-clip"
>
<div
@if (filament()->isSidebarCollapsibleOnDesktop())
x-data="{}"
@@ -89,5 +79,18 @@
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire->getRenderHookScopes()) }}
</div>
@if (filament()->hasNavigation())
<div
x-cloak
x-data="{}"
x-on:click="$store.sidebar.close()"
x-show="$store.sidebar.isOpen"
x-transition.opacity.300ms
class="fi-sidebar-close-overlay fixed inset-0 z-30 bg-gray-950/50 transition duration-500 dark:bg-gray-950/75 lg:hidden"
></div>
<x-filament-panels::sidebar :navigation="$navigation" />
@endif
</div>
</x-filament-panels::layout.base>
@@ -8,6 +8,7 @@
>
@foreach ($navigation as $navigationGroup)
<x-filament-panels::sidebar.group
:active="$navigationGroup->isActive()"
:collapsible="$navigationGroup->isCollapsible()"
:icon="$navigationGroup->getIcon()"
:items="$navigationGroup->getItems()"
@@ -1,4 +1,5 @@
@props([
'active' => false,
'collapsible' => true,
'icon' => null,
'items' => [],
@@ -7,6 +8,11 @@
'subNavigation' => false,
])
@php
$sidebarCollapsible = $sidebarCollapsible && filament()->isSidebarCollapsibleOnDesktop();
$hasDropdown = filled($label) && filled($icon) && $sidebarCollapsible;
@endphp
<li
x-data="{ label: @js($subNavigation ? "sub_navigation_{$label}" : $label) }"
data-group-label="{{ $label }}"
@@ -17,7 +23,7 @@
@if ($collapsible)
x-on:click="$store.sidebar.toggleCollapsedGroup(label)"
@endif
@if (filament()->isSidebarCollapsibleOnDesktop())
@if ($sidebarCollapsible)
x-show="$store.sidebar.isOpen"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
@@ -56,17 +62,118 @@
</div>
@endif
@if ($hasDropdown)
<x-filament::dropdown
:placement="(__('filament-panels::layout.direction') === 'rtl') ? 'left-start' : 'right-start'"
teleport
x-show="! $store.sidebar.isOpen"
>
<x-slot name="trigger">
<button
x-data="{ tooltip: false }"
x-effect="
tooltip = $store.sidebar.isOpen
? false
: {
content: @js($label),
placement: document.dir === 'rtl' ? 'left' : 'right',
theme: $store.theme,
}
"
x-tooltip.html="tooltip"
class="relative flex flex-1 items-center justify-center gap-x-3 rounded-lg px-2 py-2 outline-none transition duration-75 hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<x-filament::icon
:icon="$icon"
@class([
'h-6 w-6',
'text-gray-400 dark:text-gray-500' => ! $active,
'text-primary-600 dark:text-primary-400' => $active,
])
/>
</button>
</x-slot>
@php
$lists = [];
foreach ($items as $item) {
if ($childItems = $item->getChildItems()) {
$lists[] = [
$item,
...$childItems,
];
$lists[] = [];
continue;
}
if (empty($lists)) {
$lists[] = [$item];
continue;
}
$lists[count($lists) - 1][] = $item;
}
if (empty($lists[count($lists) - 1])) {
array_pop($lists);
}
@endphp
@if (filled($label))
<x-filament::dropdown.header>
{{ $label }}
</x-filament::dropdown.header>
@endif
@foreach ($lists as $list)
<x-filament::dropdown.list>
@foreach ($list as $item)
@php
$itemIsActive = $item->isActive();
@endphp
<x-filament::dropdown.list.item
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:color="$itemIsActive ? 'primary' : 'gray'"
:href="$item->getUrl()"
:icon="$itemIsActive ? ($item->getActiveIcon() ?? $item->getIcon()) : $item->getIcon()"
tag="a"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
@endforeach
</x-filament::dropdown>
@endif
<ul
x-show="! ($store.sidebar.groupIsCollapsed(label) && ($store.sidebar.isOpen || @js(! filament()->isSidebarCollapsibleOnDesktop())))"
@if (filament()->isSidebarCollapsibleOnDesktop())
@if (filled($label))
@if ($sidebarCollapsible)
x-show="$store.sidebar.isOpen ? ! $store.sidebar.groupIsCollapsed(label) : ! @js($hasDropdown)"
@else
x-show="! $store.sidebar.groupIsCollapsed(label)"
@endif
x-collapse.duration.200ms
@endif
@if ($sidebarCollapsible)
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
x-collapse.duration.200ms
class="fi-sidebar-group-items flex flex-col gap-y-1"
>
@foreach ($items as $item)
@php
$itemIcon = $item->getIcon();
@endphp
<x-filament-panels::sidebar.item
:active="$item->isActive()"
:active-child-items="$item->isChildItemsActive()"
@@ -77,7 +184,7 @@
:child-items="$item->getChildItems()"
:first="$loop->first"
:grouped="filled($label)"
:icon="$item->getIcon()"
:icon="$icon ? (($hasDropdown || blank($itemIcon)) ? null : throw new \Exception('Navigation group [' . $label . '] has an icon but one or more of its items also have icons. Either the group or its items can have icons, but not both. This is to ensure a proper user experience.')) : $itemIcon"
:last="$loop->last"
:should-open-url-in-new-tab="$item->shouldOpenUrlInNewTab()"
:sidebar-collapsible="$sidebarCollapsible"
@@ -117,6 +117,7 @@
<ul class="fi-sidebar-nav-groups -mx-2 flex flex-col gap-y-7">
@foreach ($navigation as $group)
<x-filament-panels::sidebar.group
:active="$group->isActive()"
:collapsible="$group->isCollapsible()"
:icon="$group->getIcon()"
:items="$group->getItems()"
@@ -51,10 +51,10 @@
'bg-gray-100 dark:bg-white/5' => $active,
])
>
@if (filled($icon) && ((! $subGrouped) || filament()->isSidebarCollapsibleOnDesktop()))
@if (filled($icon) && ((! $subGrouped) || $sidebarCollapsible))
<x-filament::icon
:icon="($active && $activeIcon) ? $activeIcon : $icon"
:x-show="($subGrouped && filament()->isSidebarCollapsibleOnDesktop()) ? '! $store.sidebar.isOpen' : false"
:x-show="($subGrouped && $sidebarCollapsible) ? '! $store.sidebar.isOpen' : false"
@class([
'fi-sidebar-item-icon h-6 w-6',
'text-gray-400 dark:text-gray-500' => ! $active,
@@ -65,7 +65,7 @@
@if ((blank($icon) && $grouped) || $subGrouped)
<div
@if (filled($icon) && $subGrouped && filament()->isSidebarCollapsibleOnDesktop())
@if (filled($icon) && $subGrouped && $sidebarCollapsible)
x-show="$store.sidebar.isOpen"
@endif
class="fi-sidebar-item-grouped-border relative flex h-6 w-6 items-center justify-center"
@@ -79,27 +79,56 @@
</x-filament-panels::topbar.item>
</x-slot>
<x-filament::dropdown.list>
@foreach ($group->getItems() as $item)
@php
$icon = $item->getIcon();
$isActive = $item->isActive();
@endphp
@php
$lists = [];
<x-filament::dropdown.list.item
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:color="$isActive ? 'primary' : 'gray'"
:href="$item->getUrl()"
:icon="$isActive ? ($item->getActiveIcon() ?? $icon) : $icon"
tag="a"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
foreach ($group->getItems() as $item) {
if ($childItems = $item->getChildItems()) {
$lists[] = [
$item,
...$childItems,
];
$lists[] = [];
continue;
}
if (empty($lists)) {
$lists[] = [$item];
continue;
}
$lists[count($lists) - 1][] = $item;
}
if (empty($lists[count($lists) - 1])) {
array_pop($lists);
}
@endphp
@foreach ($lists as $list)
<x-filament::dropdown.list>
@foreach ($list as $item)
@php
$itemIsActive = $item->isActive();
@endphp
<x-filament::dropdown.list.item
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:color="$itemIsActive ? 'primary' : 'gray'"
:href="$item->getUrl()"
:icon="$itemIsActive ? ($item->getActiveIcon() ?? $item->getIcon()) : $item->getIcon()"
tag="a"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
@endforeach
</x-filament::dropdown>
@else
@foreach ($group->getItems() as $item)
@@ -1,62 +0,0 @@
<?php
namespace Filament\Commands;
use Composer\InstalledVersions;
use Illuminate\Console\Command;
class MakeIssueCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:filament-issue';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generates a link to the Filament issue page and pre-fills the version numbers.';
/**
* Execute the console command.
*/
public function handle(): void
{
$filament = InstalledVersions::getPrettyVersion('filament/filament');
$laravel = InstalledVersions::getPrettyVersion('laravel/framework');
$livewire = InstalledVersions::getPrettyVersion('livewire/livewire');
$php = PHP_VERSION;
$issueTemplate = 'bug_report.yml';
$baseGithubUrl = 'https://github.com/filamentphp/filament/issues/new?';
$queryParams = [
'template' => $issueTemplate,
'package-version' => $filament,
'laravel-version' => $laravel,
'livewire-version' => $livewire,
'php-version' => $php,
];
$fullUrl = "{$baseGithubUrl}" . http_build_query($queryParams);
$this->openGithubIssueInBrowser($fullUrl);
}
public function openGithubIssueInBrowser(string $url): void
{
if (PHP_OS_FAMILY === 'Darwin') {
exec('open "' . $url . '"');
}
if (PHP_OS_FAMILY === 'Linux') {
exec('xdg-open "' . $url . '"');
}
if (PHP_OS_FAMILY === 'Windows') {
exec('start "" "' . $url . '"');
}
}
}
@@ -266,7 +266,7 @@ class MakePageCommand extends Command
$this->copyStubToApp('ResourceManageRelatedRecordsPage', $path, [
'baseResourcePage' => "Filament\\Resources\\Pages\\{$resourcePage}",
'baseResourcePageClass' => $resourcePage,
'modifyQueryUsing' => filled($modifyQueryUsing ?? null) ? PHP_EOL . $this->indentString($modifyQueryUsing ?? '', 3) : $modifyQueryUsing ?? '',
'modifyQueryUsing' => filled($modifyQueryUsing ?? null) ? PHP_EOL . $this->indentString($modifyQueryUsing, 3) : $modifyQueryUsing ?? '',
'namespace' => "{$resourceNamespace}\\{$resource}\\Pages" . ($pageNamespace !== '' ? "\\{$pageNamespace}" : ''),
'recordTitleAttribute' => $recordTitleAttribute ?? null,
'relationship' => $relationship ?? null,
@@ -108,7 +108,6 @@ class FilamentServiceProvider extends PackageServiceProvider
Commands\CacheComponentsCommand::class,
Commands\ClearCachedComponentsCommand::class,
Commands\MakeClusterCommand::class,
Commands\MakeIssueCommand::class,
Commands\MakePageCommand::class,
Commands\MakePanelCommand::class,
Commands\MakeRelationManagerCommand::class,
@@ -3,7 +3,6 @@
namespace Filament\Navigation;
use Closure;
use Exception;
use Filament\Support\Components\Component;
use Filament\Support\Concerns\HasExtraSidebarAttributes;
use Filament\Support\Concerns\HasExtraTopbarAttributes;
@@ -82,13 +81,7 @@ class NavigationGroup extends Component
public function getIcon(): ?string
{
$icon = $this->evaluate($this->icon);
if ($this->hasItemIcons() && filled($icon)) {
throw new Exception("Navigation group [{$this->getLabel()}] has an icon but one or more of its items also have icons. Either the group or its items can have icons, but not both. This is to ensure a proper user experience.");
}
return $icon;
return $this->evaluate($this->icon);
}
/**
@@ -126,30 +119,4 @@ class NavigationGroup extends Component
return false;
}
public function hasItemIcons(): bool
{
$hasIconCount = 0;
$hasNoIconCount = 0;
foreach ($this->getItems() as $item) {
if (! $item instanceof NavigationItem) {
continue;
}
if (blank($item->getIcon())) {
$hasNoIconCount++;
continue;
}
$hasIconCount++;
}
if (($hasIconCount > 0) && ($hasNoIconCount > 0) && filled($label = $this->getLabel())) {
throw new Exception("Navigation group [{$label}] has items with and without icons. All items must have icons or none of them can have icons. This is to ensure a proper user experience.");
}
return $hasIconCount > 0;
}
}
@@ -21,9 +21,11 @@ use Filament\Support\Facades\FilamentView;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\Rules\Password;
use Throwable;
use function Filament\Support\is_app_url;
@@ -143,6 +145,8 @@ class EditProfile extends Page
public function save(): void
{
try {
DB::beginTransaction();
$this->callHook('beforeValidate');
$data = $this->form->getState();
@@ -156,8 +160,18 @@ class EditProfile extends Page
$this->handleRecordUpdate($this->getUser(), $data);
$this->callHook('afterSave');
DB::commit();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
DB::rollBack() :
DB::commit();
return;
} catch (Throwable $exception) {
DB::rollBack();
throw $exception;
}
if (request()->hasSession() && array_key_exists('password', $data)) {
+5 -2
View File
@@ -22,6 +22,7 @@ use Illuminate\Auth\SessionGuard;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
@@ -74,9 +75,11 @@ class Register extends SimplePage
return null;
}
$data = $this->form->getState();
$user = DB::transaction(function () {
$data = $this->form->getState();
$user = $this->getUserModel()::create($data);
return $this->getUserModel()::create($data);
});
event(new Registered($user));
@@ -15,7 +15,9 @@ use Filament\Support\Facades\FilamentView;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
use Throwable;
use function Filament\authorize;
use function Filament\Support\is_app_url;
@@ -108,6 +110,8 @@ abstract class EditTenantProfile extends Page
public function save(): void
{
try {
DB::beginTransaction();
$this->callHook('beforeValidate');
$data = $this->form->getState();
@@ -121,8 +125,18 @@ abstract class EditTenantProfile extends Page
$this->handleRecordUpdate($this->tenant, $data);
$this->callHook('afterSave');
DB::commit();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
DB::rollBack() :
DB::commit();
return;
} catch (Throwable $exception) {
DB::rollBack();
throw $exception;
}
$this->getSavedNotification()?->send();
@@ -15,6 +15,8 @@ use Filament\Support\Facades\FilamentView;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Throwable;
use function Filament\authorize;
use function Filament\Support\is_app_url;
@@ -70,6 +72,8 @@ abstract class RegisterTenant extends SimplePage
public function register(): void
{
try {
DB::beginTransaction();
$this->callHook('beforeValidate');
$data = $this->form->getState();
@@ -85,8 +89,18 @@ abstract class RegisterTenant extends SimplePage
$this->form->model($this->tenant)->saveRelationships();
$this->callHook('afterRegister');
DB::commit();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
DB::rollBack() :
DB::commit();
return;
} catch (Throwable $exception) {
DB::rollBack();
throw $exception;
}
if ($redirectUrl = $this->getRedirectUrl()) {
@@ -671,6 +671,6 @@ trait HasComponents
public function getComponentCachePath(): string
{
return (config('filament.cache_path') ?? base_path('bootstrap/cache/filament')) . DIRECTORY_SEPARATOR . "panels/{$this->getId()}.php";
return (config('filament.cache_path') ?? base_path('bootstrap/cache/filament')) . DIRECTORY_SEPARATOR . 'panels' . DIRECTORY_SEPARATOR . "{$this->getId()}.php";
}
}
@@ -14,6 +14,8 @@ use Filament\Support\Facades\FilamentView;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Support\Facades\DB;
use Throwable;
use function Filament\Support\is_app_url;
@@ -74,6 +76,8 @@ class CreateRecord extends Page
$this->authorizeAccess();
try {
DB::beginTransaction();
$this->callHook('beforeValidate');
$data = $this->form->getState();
@@ -89,8 +93,18 @@ class CreateRecord extends Page
$this->form->model($this->getRecord())->saveRelationships();
$this->callHook('afterCreate');
DB::commit();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
DB::rollBack() :
DB::commit();
return;
} catch (Throwable $exception) {
DB::rollBack();
throw $exception;
}
$this->rememberData();
@@ -20,6 +20,8 @@ use Filament\Support\Facades\FilamentView;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Throwable;
use function Filament\Support\is_app_url;
@@ -82,22 +84,23 @@ class EditRecord extends Page
protected function fillForm(): void
{
$data = $this->getRecord()->attributesToArray();
/** @internal Read the DocBlock above the following method. */
$this->fillFormWithDataAndCallHooks($data);
$this->fillFormWithDataAndCallHooks($this->getRecord());
}
/**
* @internal Never override or call this method. If you completely override `fillForm()`, copy the contents of this method into your override.
*
* @param array<string, mixed> $data
* @param array<string, mixed> $extraData
*/
protected function fillFormWithDataAndCallHooks(array $data): void
protected function fillFormWithDataAndCallHooks(Model $record, array $extraData = []): void
{
$this->callHook('beforeFill');
$data = $this->mutateFormDataBeforeFill($data);
$data = $this->mutateFormDataBeforeFill([
...$record->attributesToArray(),
...$extraData,
]);
$this->form->fill($data);
@@ -129,6 +132,8 @@ class EditRecord extends Page
$this->authorizeAccess();
try {
DB::beginTransaction();
$this->callHook('beforeValidate');
$data = $this->form->getState();
@@ -142,8 +147,18 @@ class EditRecord extends Page
$this->handleRecordUpdate($this->getRecord(), $data);
$this->callHook('afterSave');
DB::commit();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
DB::rollBack() :
DB::commit();
return;
} catch (Throwable $exception) {
DB::rollBack();
throw $exception;
}
$this->rememberData();
@@ -13,6 +13,7 @@ use Filament\Infolists\Infolist;
use Filament\Pages\Concerns\InteractsWithFormActions;
use Filament\Support\Facades\FilamentIcon;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
/**
@@ -76,22 +77,23 @@ class ViewRecord extends Page
protected function fillForm(): void
{
$data = $this->getRecord()->attributesToArray();
/** @internal Read the DocBlock above the following method. */
$this->fillFormWithDataAndCallHooks($data);
$this->fillFormWithDataAndCallHooks($this->getRecord());
}
/**
* @internal Never override or call this method. If you completely override `fillForm()`, copy the contents of this method into your override.
*
* @param array<string, mixed> $data
* @param array<string, mixed> $extraData
*/
protected function fillFormWithDataAndCallHooks(array $data): void
protected function fillFormWithDataAndCallHooks(Model $record, array $extraData = []): void
{
$this->callHook('beforeFill');
$data = $this->mutateFormDataBeforeFill($data);
$data = $this->mutateFormDataBeforeFill([
...$record->attributesToArray(),
...$extraData,
]);
$this->form->fill($data);
@@ -9,8 +9,8 @@
},
"require": {
"php": "^8.1",
"illuminate/contracts": "^10.0",
"illuminate/support": "^10.0",
"illuminate/contracts": "^10.45",
"illuminate/support": "^10.45",
"spatie/laravel-google-fonts": "^1.0"
},
"autoload": {
@@ -9,7 +9,7 @@
},
"require": {
"php": "^8.1",
"illuminate/support": "^10.0",
"illuminate/support": "^10.45",
"spatie/laravel-medialibrary": "^10.0|^11.0"
},
"autoload": {
@@ -3,6 +3,7 @@
namespace Filament\Infolists\Components;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Throwable;
@@ -46,34 +47,38 @@ class SpatieMediaLibraryImageEntry extends ImageEntry
}
if ($this->hasRelationship($record)) {
$record = $record->getRelationValue($this->getRelationshipName());
$record = $this->getRelationshipResults($record);
}
if (! $record) {
return null;
}
$records = Arr::wrap($record);
/** @var ?Media $media */
$media = $record->media->first(fn (Media $media): bool => $media->uuid === $state);
foreach ($records as $record) {
/** @var Model $record */
if (! $media) {
return null;
}
/** @var ?Media $media */
$media = $record->getRelationValue('media')->first(fn (Media $media): bool => $media->uuid === $state);
$conversion = $this->getConversion();
if ($this->getVisibility() === 'private') {
try {
return $media->getTemporaryUrl(
now()->addMinutes(5),
$conversion ?? '',
);
} catch (Throwable $exception) {
// This driver does not support creating temporary URLs.
if (! $media) {
continue;
}
$conversion = $this->getConversion();
if ($this->getVisibility() === 'private') {
try {
return $media->getTemporaryUrl(
now()->addMinutes(5),
$conversion ?? '',
);
} catch (Throwable $exception) {
// This driver does not support creating temporary URLs.
}
}
return $media->getAvailableUrl(Arr::wrap($conversion));
}
return $media->getAvailableUrl(Arr::wrap($conversion));
return null;
}
/**
@@ -86,13 +91,25 @@ class SpatieMediaLibraryImageEntry extends ImageEntry
$record = $this->getRecord();
if ($this->hasRelationship($record)) {
$record = $record->getRelationValue($this->getRelationshipName());
$record = $this->getRelationshipResults($record);
}
return $record?->getRelationValue('media')
->filter(fn (Media $media): bool => blank($collection) || ($media->getAttributeValue('collection_name') === $collection))
->sortBy('order_column')
->map(fn (Media $media): string => $media->uuid)
->all() ?? [];
$records = Arr::wrap($record);
$state = [];
foreach ($records as $record) {
/** @var Model $record */
$state = [
...$state,
...$record->getRelationValue('media')
->filter(fn (Media $media): bool => blank($collection) || ($media->getAttributeValue('collection_name') === $collection))
->sortBy('order_column')
->pluck('uuid')
->all(),
];
}
return array_unique($state);
}
}
@@ -4,6 +4,7 @@ namespace Filament\Tables\Columns;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
@@ -44,34 +45,38 @@ class SpatieMediaLibraryImageColumn extends ImageColumn
$record = $this->getRecord();
if ($this->hasRelationship($record)) {
$record = $record->getRelationValue($this->getRelationshipName());
$record = $this->getRelationshipResults($record);
}
if (! $record) {
return null;
}
$records = Arr::wrap($record);
/** @var ?Media $media */
$media = $record->media->first(fn (Media $media): bool => $media->uuid === $state);
foreach ($records as $record) {
/** @var Model $record */
if (! $media) {
return null;
}
/** @var ?Media $media */
$media = $record->getRelationValue('media')->first(fn (Media $media): bool => $media->uuid === $state);
$conversion = $this->getConversion();
if ($this->getVisibility() === 'private') {
try {
return $media->getTemporaryUrl(
now()->addMinutes(5),
$conversion ?? '',
);
} catch (Throwable $exception) {
// This driver does not support creating temporary URLs.
if (! $media) {
continue;
}
$conversion = $this->getConversion();
if ($this->getVisibility() === 'private') {
try {
return $media->getTemporaryUrl(
now()->addMinutes(5),
$conversion ?? '',
);
} catch (Throwable $exception) {
// This driver does not support creating temporary URLs.
}
}
return $media->getAvailableUrl(Arr::wrap($conversion));
}
return $media->getAvailableUrl(Arr::wrap($conversion));
return null;
}
/**
@@ -79,19 +84,28 @@ class SpatieMediaLibraryImageColumn extends ImageColumn
*/
public function getState(): array
{
$collection = $this->getCollection();
$record = $this->getRecord();
if ($this->hasRelationship($record)) {
$record = $record->getRelationValue($this->getRelationshipName());
$record = $this->getRelationshipResults($record);
}
return $record?->getRelationValue('media')
->filter(fn (Media $media): bool => blank($collection) || ($media->getAttributeValue('collection_name') === $collection))
->sortBy('order_column')
->map(fn (Media $media): string => $media->uuid)
->all() ?? [];
$records = Arr::wrap($record);
$state = [];
foreach ($records as $record) {
/** @var Model $record */
$state = [
...$state,
...$record->getRelationValue('media')
->sortBy('order_column')
->pluck('uuid')
->all(),
];
}
return array_unique($state);
}
public function applyEagerLoading(Builder | Relation $query): Builder | Relation
@@ -100,18 +114,23 @@ class SpatieMediaLibraryImageColumn extends ImageColumn
return $query;
}
/** @phpstan-ignore-next-line */
$modifyMediaQuery = fn (Builder | Relation $query) => $query
->ordered()
->when(
$this->getCollection(),
fn (Builder | Relation $query, string $collection) => $query->where(
'collection_name',
$collection,
),
);
if ($this->hasRelationship($query->getModel())) {
return $query->with([
"{$this->getRelationshipName()}.media" => fn (Builder | Relation $query) => $query->when(
$this->getCollection(),
fn (Builder | Relation $query, string $collection) => $query->where(
'collection_name',
$collection,
),
),
"{$this->getRelationshipName()}.media" => $modifyMediaQuery,
]);
}
return $query->with(['media']);
return $query->with(['media' => $modifyMediaQuery]);
}
}
@@ -10,9 +10,9 @@
"require": {
"php": "^8.1",
"filament/filament": "self.version",
"illuminate/console": "^10.0",
"illuminate/filesystem": "^10.0",
"illuminate/support": "^10.0",
"illuminate/console": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/support": "^10.45",
"spatie/laravel-settings": "^2.2|^3.0"
},
"autoload": {
@@ -9,6 +9,8 @@ use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
use Filament\Support\Facades\FilamentView;
use Illuminate\Support\Facades\DB;
use Throwable;
use function Filament\Support\is_app_url;
@@ -58,6 +60,8 @@ class SettingsPage extends Page
public function save(): void
{
try {
DB::beginTransaction();
$this->callHook('beforeValidate');
$data = $this->form->getState();
@@ -74,8 +78,18 @@ class SettingsPage extends Page
$settings->save();
$this->callHook('afterSave');
DB::commit();
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
DB::rollBack() :
DB::commit();
return;
} catch (Throwable $exception) {
DB::rollBack();
throw $exception;
}
$this->getSavedNotification()?->send();
@@ -9,7 +9,7 @@
},
"require": {
"php": "^8.1",
"illuminate/database": "^10.0",
"illuminate/database": "^10.45",
"spatie/laravel-tags": "^4.0"
},
"autoload": {
@@ -5,6 +5,8 @@ namespace Filament\Infolists\Components;
use Closure;
use Filament\SpatieLaravelTagsPlugin\Types\AllTagTypes;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
class SpatieTagsEntry extends TextEntry
{
@@ -37,27 +39,34 @@ class SpatieTagsEntry extends TextEntry
}
if ($this->hasRelationship($record)) {
$record = $record->getRelationValue($this->getRelationshipName());
$record = $this->getRelationshipResults($record);
}
if (! $record) {
return [];
$records = Arr::wrap($record);
$state = [];
foreach ($records as $record) {
/** @var Model $record */
if (! (method_exists($record, 'tags') && method_exists($record, 'tagsWithType'))) {
continue;
}
$type = $this->getType();
if ($this->isAnyTagTypeAllowed()) {
$tags = $record->getRelationValue('tags');
} else {
$tags = $record->tagsWithType($type);
}
$state = [
...$state,
...$tags->pluck('name')->all(),
];
}
if (! (method_exists($record, 'tags') && method_exists($record, 'tagsWithType'))) {
return [];
}
$type = $this->getType();
if ($this->isAnyTagTypeAllowed()) {
/** @phpstan-ignore-next-line */
$tags = $record->tags;
} else {
$tags = $record->tagsWithType($type);
}
return $tags->pluck('name')->all();
return array_unique($state);
}
public function type(string | Closure | AllTagTypes | null $type): static
@@ -5,10 +5,12 @@ namespace Filament\Tables\Columns;
use Closure;
use Filament\SpatieLaravelTagsPlugin\Types\AllTagTypes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class SpatieTagsColumn extends TagsColumn
class SpatieTagsColumn extends TextColumn
{
protected string | Closure | AllTagTypes | null $type;
@@ -17,6 +19,8 @@ class SpatieTagsColumn extends TagsColumn
parent::setUp();
$this->type(new AllTagTypes());
$this->badge();
}
/**
@@ -33,27 +37,34 @@ class SpatieTagsColumn extends TagsColumn
$record = $this->getRecord();
if ($this->hasRelationship($record)) {
$record = $record->getRelationValue($this->getRelationshipName());
$record = $this->getRelationshipResults($record);
}
if (! $record) {
return [];
$records = Arr::wrap($record);
$state = [];
foreach ($records as $record) {
/** @var Model $record */
if (! (method_exists($record, 'tags') && method_exists($record, 'tagsWithType'))) {
continue;
}
$type = $this->getType();
if ($this->isAnyTagTypeAllowed()) {
$tags = $record->getRelationValue('tags');
} else {
$tags = $record->tagsWithType($type);
}
$state = [
...$state,
...$tags->pluck('name')->all(),
];
}
if (! (method_exists($record, 'tags') && method_exists($record, 'tagsWithType'))) {
return [];
}
$type = $this->getType();
if ($this->isAnyTagTypeAllowed()) {
/** @phpstan-ignore-next-line */
$tags = $record->tags;
} else {
$tags = $record->tagsWithType($type);
}
return $tags->pluck('name')->all();
return array_unique($state);
}
public function type(string | Closure | AllTagTypes | null $type): static
@@ -10,7 +10,7 @@
"require": {
"php": "^8.1",
"filament/support": "self.version",
"illuminate/support": "^10.0",
"illuminate/support": "^10.45",
"spatie/laravel-translatable": "^6.0"
},
"autoload": {
@@ -30,10 +30,7 @@ trait HasTranslatableFormWithExistingRecordData
}
/** @internal Read the DocBlock above the following method. */
$this->fillFormWithDataAndCallHooks([
...$record->attributesToArray(),
...$translatedData,
]);
$this->fillFormWithDataAndCallHooks($record, $translatedData);
}
}
+3 -3
View File
@@ -12,9 +12,9 @@
"blade-ui-kit/blade-heroicons": "^2.2.1",
"doctrine/dbal": "^3.2",
"ext-intl": "*",
"illuminate/contracts": "^10.0",
"illuminate/support": "^10.0",
"illuminate/view": "^10.0",
"illuminate/contracts": "^10.45",
"illuminate/support": "^10.45",
"illuminate/view": "^10.45",
"livewire/livewire": "^3.2.3",
"ryangjchandler/blade-capture-directive": "^0.2|^0.3",
"spatie/color": "^1.5",
@@ -79,6 +79,32 @@ You can add an [icon](https://blade-ui-kit.com/blade-icons?set=1#search) to a dr
</x-filament::dropdown.list.item>
```
### Changing the icon color of a dropdown item
By default, the icon color uses the [same color as the item itself](#changing-the-color-of-a-dropdown-item). You can override it to be `danger`, `info`, `primary`, `success` or `warning` by using the `icon-color` attribute:
```blade
<x-filament::dropdown.list.item icon="heroicon-m-pencil" icon-color="danger">
Edit
</x-filament::dropdown.list.item>
<x-filament::dropdown.list.item icon="heroicon-m-pencil" icon-color="info">
Edit
</x-filament::dropdown.list.item>
<x-filament::dropdown.list.item icon="heroicon-m-pencil" icon-color="primary">
Edit
</x-filament::dropdown.list.item>
<x-filament::dropdown.list.item icon="heroicon-m-pencil" icon-color="success">
Edit
</x-filament::dropdown.list.item>
<x-filament::dropdown.list.item icon="heroicon-m-pencil" icon-color="warning">
Edit
</x-filament::dropdown.list.item>
```
## Adding an image to a dropdown item
You can add a circular image to a dropdown item by using the `image` attribute:
@@ -22,12 +22,20 @@ return [
'actions' => [
'first' => [
'label' => 'Primeira',
],
'go_to_page' => [
'label' => 'Ir para a página :page',
],
'last' => [
'label' => 'Última',
],
'next' => [
'label' => 'Próximo',
'label' => 'Próxima',
],
'previous' => [
@@ -22,10 +22,18 @@ return [
'actions' => [
'first' => [
'label' => 'Prima pagină',
],
'go_to_page' => [
'label' => 'Mergi la pagina :page',
],
'last' => [
'label' => 'Ultima pagină',
],
'next' => [
'label' => 'Pagina următoare',
],
@@ -1,7 +1,7 @@
<div
{{
$attributes->class([
'grid grid-flow-col rounded-lg shadow-sm ring-1 ring-gray-950/10 dark:ring-white/20',
'fi-btn-group grid grid-flow-col rounded-lg shadow-sm ring-1 ring-gray-950/10 dark:ring-white/20',
])
}}
>
@@ -11,6 +11,7 @@
'href' => null,
'icon' => null,
'iconAlias' => null,
'iconColor' => null,
'iconSize' => IconSize::Medium,
'image' => null,
'keyBindings' => null,
@@ -44,6 +45,8 @@
) => $color !== 'gray',
]);
$iconColor ??= $color;
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-dropdown-list-item-icon',
match ($iconSize) {
@@ -52,7 +55,7 @@
IconSize::Large, 'lg' => 'h-6 w-6',
default => $iconSize,
},
match ($color) {
match ($iconColor) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500 dark:text-custom-400',
},
@@ -60,10 +63,10 @@
$iconStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
$iconColor,
shades: [400, 500],
alias: 'dropdown.list.item.icon',
) => $color !== 'gray',
) => $iconColor !== 'gray',
]);
$imageClasses = 'fi-dropdown-list-item-image h-5 w-5 rounded-full bg-cover bg-center';
@@ -30,6 +30,10 @@
])
@php
$hasDescription = filled($description);
$hasHeading = filled($heading);
$hasIcon = filled($icon);
if (! $alignment instanceof Alignment) {
$alignment = filled($alignment) ? (Alignment::tryFrom($alignment) ?? $alignment) : null;
}
@@ -207,6 +211,7 @@
Alignment::Center => 'flex-col',
default => null,
},
'items-center' => $hasIcon && $hasHeading && (! $hasDescription) && in_array($alignment, [Alignment::Start, Alignment::Left]),
])
>
@if ($closeButton)
@@ -233,7 +238,7 @@
@if ($header)
{{ $header }}
@else
@if ($icon)
@if ($hasIcon)
<div
@class([
'mb-5 flex items-center justify-center' => $alignment === Alignment::Center,
@@ -284,7 +289,7 @@
{{ $heading }}
</x-filament::modal.heading>
@if (filled($description))
@if ($hasDescription)
<x-filament::modal.description
class="mt-2"
>
@@ -301,8 +306,8 @@
@class([
'fi-modal-content flex flex-col gap-y-4 py-6',
'flex-1' => ($width === MaxWidth::Screen) || $slideOver,
'pe-6 ps-[5.25rem]' => $icon && ($alignment === Alignment::Start),
'px-6' => ! ($icon && ($alignment === Alignment::Start)),
'pe-6 ps-[5.25rem]' => $hasIcon && ($alignment === Alignment::Start),
'px-6' => ! ($hasIcon && ($alignment === Alignment::Start)),
])
>
{{ $slot }}
@@ -313,8 +318,8 @@
<div
@class([
'fi-modal-footer w-full',
'pe-6 ps-[5.25rem]' => $icon && ($alignment === Alignment::Start) && ($footerActionsAlignment !== Alignment::Center) && (! $stickyFooter),
'px-6' => ! ($icon && ($alignment === Alignment::Start) && ($footerActionsAlignment !== Alignment::Center) && (! $stickyFooter)),
'pe-6 ps-[5.25rem]' => $hasIcon && ($alignment === Alignment::Start) && ($footerActionsAlignment !== Alignment::Center) && (! $stickyFooter),
'px-6' => ! ($hasIcon && ($alignment === Alignment::Start) && ($footerActionsAlignment !== Alignment::Center) && (! $stickyFooter)),
'fi-sticky sticky bottom-0 border-t border-gray-200 bg-white py-5 dark:border-white/10 dark:bg-gray-900' => $stickyFooter,
'rounded-b-xl' => $stickyFooter && ! ($slideOver || ($width === MaxWidth::Screen)),
'pb-6' => ! $stickyFooter,
@@ -51,9 +51,9 @@
'filament::components/pagination.overview',
$paginator->total(),
[
'first' => \Filament\Support\format_number($paginator->firstItem() ?? 0),
'last' => \Filament\Support\format_number($paginator->lastItem() ?? 0),
'total' => \Filament\Support\format_number($paginator->total()),
'first' => \Illuminate\Support\Number::format($paginator->firstItem() ?? 0),
'last' => \Illuminate\Support\Number::format($paginator->lastItem() ?? 0),
'total' => \Illuminate\Support\Number::format($paginator->total()),
],
)
}}
@@ -1,8 +1,8 @@
<?php
namespace Filament\Commands\Aliases;
namespace Filament\Support\Commands\Aliases;
use Filament\Commands;
use Filament\Support\Commands;
class MakeIssueCommand extends Commands\MakeIssueCommand
{
@@ -0,0 +1,37 @@
<?php
namespace Filament\Support\Commands;
use Composer\InstalledVersions;
use Illuminate\Console\Command;
class MakeIssueCommand extends Command
{
protected $signature = 'make:filament-issue';
protected $description = 'Generates a link to the Filament issue page and pre-fills the version numbers.';
public function handle(): void
{
$this->openUrlInBrowser('https://github.com/filamentphp/filament/issues/new?' . http_build_query([
'template' => 'bug_report.yml',
'package-version' => InstalledVersions::getPrettyVersion('filament/support'),
'laravel-version' => InstalledVersions::getPrettyVersion('laravel/framework'),
'livewire-version' => InstalledVersions::getPrettyVersion('livewire/livewire'),
'php-version' => PHP_VERSION,
]));
}
public function openUrlInBrowser(string $url): void
{
if (PHP_OS_FAMILY === 'Darwin') {
exec('open "' . $url . '"');
}
if (PHP_OS_FAMILY === 'Linux') {
exec('xdg-open "' . $url . '"');
}
if (PHP_OS_FAMILY === 'Windows') {
exec('start "" "' . $url . '"');
}
}
}
@@ -0,0 +1,8 @@
<?php
namespace Filament\Support\Enums;
enum ArgumentValue
{
case Default;
}
@@ -6,4 +6,17 @@ use Exception;
class Cancel extends Exception
{
protected bool $shouldRollbackDatabaseTransaction = false;
public function rollBackDatabaseTransaction(bool $condition = true): static
{
$this->shouldRollbackDatabaseTransaction = $condition;
return $this;
}
public function shouldRollbackDatabaseTransaction(): bool
{
return $this->shouldRollbackDatabaseTransaction;
}
}
+13
View File
@@ -6,4 +6,17 @@ use Exception;
class Halt extends Exception
{
protected bool $shouldRollbackDatabaseTransaction = false;
public function rollBackDatabaseTransaction(bool $condition = true): static
{
$this->shouldRollbackDatabaseTransaction = $condition;
return $this;
}
public function shouldRollbackDatabaseTransaction(): bool
{
return $this->shouldRollbackDatabaseTransaction;
}
}
@@ -7,9 +7,11 @@ use Filament\Support\Assets\AssetManager;
use Filament\Support\Assets\Css;
use Filament\Support\Assets\Js;
use Filament\Support\Colors\ColorManager;
use Filament\Support\Commands\Aliases\MakeIssueCommand as MakeIssueCommandAlias;
use Filament\Support\Commands\AssetsCommand;
use Filament\Support\Commands\CheckTranslationsCommand;
use Filament\Support\Commands\InstallCommand;
use Filament\Support\Commands\MakeIssueCommand;
use Filament\Support\Commands\UpgradeCommand;
use Filament\Support\Components\ComponentManager;
use Filament\Support\Components\Contracts\ScopedComponentManager;
@@ -39,6 +41,9 @@ class SupportServiceProvider extends PackageServiceProvider
CheckTranslationsCommand::class,
InstallCommand::class,
UpgradeCommand::class,
MakeIssueCommand::class,
MakeIssueCommandAlias::class,
])
->hasConfigFile()
->hasTranslations()
+15 -19
View File
@@ -8,30 +8,33 @@ use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Connection;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Number;
use Illuminate\Support\Str;
use Illuminate\Translation\MessageSelector;
use Illuminate\View\ComponentAttributeBag;
use NumberFormatter;
use Illuminate\View\ComponentSlot;
if (! function_exists('Filament\Support\format_money')) {
/**
* @deprecated Use `Illuminate\Support\Number::currency()` instead.
*/
function format_money(float | int $money, string $currency, int $divideBy = 0): string
{
$formatter = new NumberFormatter(app()->getLocale(), NumberFormatter::CURRENCY);
if ($divideBy) {
$money /= $divideBy;
}
return $formatter->formatCurrency($money, $currency);
return Number::currency($money, $currency);
}
}
if (! function_exists('Filament\Support\format_number')) {
/**
* @deprecated Use `Illuminate\Support\Number::format()` instead.
*/
function format_number(float | int $number): string
{
$formatter = new NumberFormatter(app()->getLocale(), NumberFormatter::DECIMAL);
return $formatter->format($number);
return Number::format($number);
}
}
@@ -118,18 +121,11 @@ if (! function_exists('Filament\Support\is_slot_empty')) {
return true;
}
return trim(
str_replace(
[
'<!-- __BLOCK__ -->',
'<!-- __ENDBLOCK__ -->',
'<!--[if BLOCK]><![endif]-->',
'<!--[if ENDBLOCK]><![endif]-->',
],
'',
$slot->toHtml()
),
) === '';
if (! $slot instanceof ComponentSlot) {
$slot = new ComponentSlot($slot->toHtml());
}
return ! $slot->hasActualContent();
}
}
+6 -6
View File
@@ -12,12 +12,12 @@
"filament/actions": "self.version",
"filament/forms": "self.version",
"filament/support": "self.version",
"illuminate/console": "^10.0",
"illuminate/contracts": "^10.0",
"illuminate/database": "^10.0",
"illuminate/filesystem": "^10.0",
"illuminate/support": "^10.0",
"illuminate/view": "^10.0",
"illuminate/console": "^10.45",
"illuminate/contracts": "^10.45",
"illuminate/database": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/support": "^10.45",
"illuminate/view": "^10.45",
"kirschbaum-development/eloquent-power-joins": "^3.0",
"spatie/laravel-package-tools": "^1.9"
},
+45 -6
View File
@@ -84,17 +84,39 @@ TextColumn::make('created_at')
## Number formatting
The `numeric()` method allows you to format a column as a number, using PHP's `number_format()`:
The `numeric()` method allows you to format an entry as a number:
```php
use Filament\Tables\Columns\TextColumn;
TextColumn::make('stock')
->numeric(
decimalPlaces: 0,
decimalSeparator: '.',
thousandsSeparator: ',',
)
->numeric()
```
If you would like to customize the number of decimal places used to format the number with, you can use the `decimalPlaces` argument:
```php
use Filament\Tables\Columns\TextColumn;
TextColumn::make('stock')
->numeric(decimalPlaces: 0)
```
By default, your app's locale will be used to format the number suitably. If you would like to customize the locale used, you can pass it to the `locale` argument:
```php
use Filament\Tables\Columns\TextColumn;
TextColumn::make('stock')
->numeric(locale: 'nl')
```
Alternatively, you can set the default locale used across your app using the `Number::useLocale()` method in the `boot()` method of a service provider:
```php
use Illuminate\Support\Number;
Number::useLocale('nl');
```
## Currency formatting
@@ -117,6 +139,23 @@ TextColumn::make('price')
->money('EUR', divideBy: 100)
```
By default, your app's locale will be used to format the money suitably. If you would like to customize the locale used, you can pass it to the `locale` argument:
```php
use Filament\Tables\Columns\TextColumn;
TextColumn::make('price')
->money('EUR', locale: 'nl')
```
Alternatively, you can set the default locale used across your app using the `Number::useLocale()` method in the `boot()` method of a service provider:
```php
use Illuminate\Support\Number;
Number::useLocale('nl');
```
## Limiting text length
You may `limit()` the length of the cell's value:
+49 -3
View File
@@ -221,7 +221,17 @@ In this example, the table will calculate how many posts are published.
### Number formatting
The `numeric()` method allows you to format a summary as a number, using PHP's `number_format()`:
The `numeric()` method allows you to format an entry as a number:
```php
use Filament\Tables\Columns\Summarizers\Average;
use Filament\Tables\Columns\TextColumn;
TextColumn::make('rating')
->summarize(Average::make()->numeric())
```
If you would like to customize the number of decimal places used to format the number with, you can use the `decimalPlaces` argument:
```php
use Filament\Tables\Columns\Summarizers\Average;
@@ -230,11 +240,29 @@ use Filament\Tables\Columns\TextColumn;
TextColumn::make('rating')
->summarize(Average::make()->numeric(
decimalPlaces: 0,
decimalSeparator: '.',
thousandsSeparator: ',',
))
```
By default, your app's locale will be used to format the number suitably. If you would like to customize the locale used, you can pass it to the `locale` argument:
```php
use Filament\Tables\Columns\Summarizers\Average;
use Filament\Tables\Columns\TextColumn;
TextColumn::make('rating')
->summarize(Average::make()->numeric(
locale: 'nl',
))
```
Alternatively, you can set the default locale used across your app using the `Number::useLocale()` method in the `boot()` method of a service provider:
```php
use Illuminate\Support\Number;
Number::useLocale('nl');
```
### Currency formatting
The `money()` method allows you to easily format monetary values, in any currency:
@@ -257,6 +285,24 @@ TextColumn::make('price')
->summarize(Sum::make()->money('EUR', divideBy: 100))
```
By default, your app's locale will be used to format the money suitably. If you would like to customize the locale used, you can pass it to the `locale` argument:
```php
use Filament\Tables\Columns\Summarizers\Average;
use Filament\Tables\Columns\TextColumn;
TextColumn::make('price')
->summarize(Sum::make()->money(currency: 'EUR', locale: 'nl'))
```
Alternatively, you can set the default locale used across your app using the `Number::useLocale()` method in the `boot()` method of a service provider:
```php
use Illuminate\Support\Number;
Number::useLocale('nl');
```
### Limiting text length
You may `limit()` the length of the summary's value:
@@ -134,7 +134,7 @@ return [
'heading' => 'Filtros',
'indicator' => 'Filtros ativos',
'indicator' => 'Filtros activos',
'multi_select' => [
'placeholder' => 'Todos',
@@ -26,6 +26,10 @@ return [
'label' => 'Selectează/Deselectează elementul :key pentru operațiuni in masă.',
],
'bulk_select_group' => [
'label' => 'Selectează/Deselectează grupul :title pentru operațiuni in masă.',
],
'search' => [
'label' => 'Căutare',
'placeholder' => 'Căutare',
@@ -102,6 +106,10 @@ return [
'actions' => [
'apply' => [
'label' => 'Aplică filtrele',
],
'remove' => [
'label' => 'Elimină filtru',
],
@@ -10,10 +10,15 @@
$height = $getHeight() ?? ($isStacked ? '2rem' : '2.5rem');
$width = $getWidth() ?? (($isCircular || $isSquare) ? $height : null);
$stateCount = count($state);
$limitedStateCount = count($limitedState);
$defaultImageUrl = $getDefaultImageUrl();
if ((! count($limitedState)) && filled($defaultImageUrl)) {
if ((! $limitedStateCount) && filled($defaultImageUrl)) {
$limitedState = [null];
$limitedStateCount = 1;
}
$ringClasses = \Illuminate\Support\Arr::toCssClasses([
@@ -28,7 +33,7 @@
},
]);
$hasLimitedRemainingText = $hasLimitedRemainingText();
$hasLimitedRemainingText = $hasLimitedRemainingText() && ($limitedStateCount < $stateCount);
$isLimitedRemainingTextSeparate = $isLimitedRemainingTextSeparate();
$limitedRemainingTextSizeClasses = match ($getLimitedRemainingTextSize()) {
@@ -50,7 +55,7 @@
])
}}
>
@if (count($limitedState))
@if ($limitedStateCount)
<div class="flex items-center gap-x-2.5">
<div
@class([
@@ -88,7 +93,7 @@
/>
@endforeach
@if ($hasLimitedRemainingText && ($loop->iteration < count($limitedState)) && (! $isLimitedRemainingTextSeparate) && $isCircular)
@if ($hasLimitedRemainingText && (! $isLimitedRemainingTextSeparate) && $isCircular)
<div
style="
@if ($height) height: {{ $height }}; @endif
@@ -106,20 +111,20 @@
])
>
<span class="-ms-0.5">
+{{ count($state) - count($limitedState) }}
+{{ $stateCount - $limitedStateCount }}
</span>
</div>
@endif
</div>
@if ($hasLimitedRemainingText && ($loop->iteration < count($limitedState)) && ($isLimitedRemainingTextSeparate || (! $isCircular)))
@if ($hasLimitedRemainingText && ($isLimitedRemainingTextSeparate || (! $isCircular)))
<div
@class([
'font-medium text-gray-500 dark:text-gray-400',
$limitedRemainingTextSizeClasses,
])
>
+{{ count($state) - count($limitedState) }}
+{{ $stateCount - $limitedStateCount }}
</div>
@endif
</div>
@@ -3,13 +3,19 @@
$isDisabled = $isDisabled();
$state = $getState();
$type = $getType();
$mask = $getMask();
$alignment = $getAlignment() ?? Alignment::Start;
if (! $alignment instanceof Alignment) {
$alignment = filled($alignment) ? (Alignment::tryFrom($alignment) ?? $alignment) : null;
}
if (filled($mask)) {
$type = 'text';
} else {
$type = $getType();
}
@endphp
<div
@@ -115,6 +121,7 @@
isLoading = false
',
'x-mask' . ($mask instanceof \Filament\Support\RawJs ? ':dynamic' : '') => filled($mask) ? $mask : null,
])
->class([
match ($alignment) {
@@ -42,6 +42,8 @@ class CreateAction extends Action
$this->successNotificationTitle(__('filament-actions::create.single.notifications.created.title'));
$this->databaseTransaction();
$this->action(function (array $arguments, Form $form, HasTable $livewire): void {
$model = $this->getModel();
@@ -49,6 +49,8 @@ class EditAction extends Action
return $data;
});
$this->databaseTransaction();
$this->action(function (): void {
$this->process(function (array $data, Model $record, Table $table) {
$relationship = $table->getRelationship();

Some files were not shown because too many files have changed in this diff Show More