Merge branch '3.x' into shades-registration

This commit is contained in:
Zep Fietje
2023-09-18 10:19:58 +02:00
274 changed files with 3368 additions and 1403 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
name: Bug report
description: Report a problem you're experiencing
labels: bug,unconfirmed
labels: bug,unconfirmed,low priority
body:
- type: markdown
attributes:
+1 -1
View File
@@ -158,7 +158,7 @@ jobs:
Hey @${user}! We're sorry to hear that you've hit this issue. 💛
`.trim()
const NO_REPRO_URL = (! url.includes('https://github.com/')) || url.includes('https://github.com/filamentphp/') || url === 'https://github.com/filamentphp'
const NO_REPRO_URL = ((! url.includes('https://github.com/')) || url.includes('https://github.com/filamentphp/') || url === 'https://github.com/filamentphp') && (! url.includes('https://github.com/filamentphp/demo'))
const NO_REPRO_STEPS = reproSteps.length < 25
if (NO_REPRO_URL || NO_REPRO_STEPS) {
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
- name: Monorepo Split of ${{ matrix.package }}
uses: danharrin/monorepo-split-github-action@v2.3.0
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
with:
package_directory: 'packages/${{ matrix.package }}'
repository_organization: 'filamentphp'
+6
View File
@@ -120,6 +120,12 @@ formComponents.forEach((componentName) => {
})
})
compile({
...defaultOptions,
entryPoints: [`./packages/tables/resources/js/components/table.js`],
outfile: `./packages/tables/dist/components/table.js`,
})
compile({
...defaultOptions,
entryPoints: [`./packages/widgets/resources/js/components/chart.js`],
@@ -839,6 +839,24 @@ class FieldsDemo extends Component implements HasForms
])
->itemLabel(fn (array $state): ?string => $state['name'] ?? null),
]),
Group::make()
->id('simpleRepeater')
->extraAttributes([
'class' => 'p-16 max-w-5xl',
])
->schema([
Repeater::make('simpleRepeater')
->label('Invitations')
->simple(
TextInput::make('email')
->email()
->required(),
)
->default([
'dan@filamentphp.com',
'ryan@filamentphp.com',
]),
]),
Group::make()
->id('builder')
->extraAttributes([
@@ -38,6 +38,15 @@ class EntriesDemo extends Component implements HasInfolists
TextEntry::make('author.name')
->state('Dan Harrin'),
]),
Group::make()
->id('placeholder')
->extraAttributes([
'class' => 'p-16 max-w-xl',
])
->schema([
TextEntry::make('title')
->placeholder('Dan Harrin'),
]),
Group::make()
->id('helperText')
->extraAttributes([
@@ -46,6 +46,7 @@ use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
use stdClass;
class TablesDemo extends Component implements HasForms, HasTable
{
@@ -232,6 +233,17 @@ class TablesDemo extends Component implements HasForms, HasTable
]);
}
public function placeholderColumns(Table $table): Table
{
return $this->postsTable($table)
->columns([
TextColumn::make('title'),
TextColumn::make('description')
->getStateUsing(fn (Post $record, stdClass $rowLoop): ?string => $rowLoop->odd ? $record->description : null)
->placeholder('No description.'),
]);
}
public function toggleableColumns(Table $table): Table
{
return $this->usersTable($table)
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

+27
View File
@@ -873,6 +873,15 @@ export default {
deviceScaleFactor: 3,
},
},
'forms/fields/repeater/simple-one-field': {
url: 'forms/fields',
selector: '#simpleRepeater',
viewport: {
width: 1920,
height: 640,
deviceScaleFactor: 3,
},
},
'forms/fields/builder/simple': {
url: 'forms/fields',
selector: '#builder',
@@ -1175,6 +1184,15 @@ export default {
deviceScaleFactor: 3,
},
},
'infolists/entries/placeholder': {
url: 'infolists/entries',
selector: '#placeholder',
viewport: {
width: 1920,
height: 640,
deviceScaleFactor: 3,
},
},
'infolists/entries/helper-text': {
url: 'infolists/entries',
selector: '#helperText',
@@ -1823,6 +1841,15 @@ export default {
deviceScaleFactor: 3,
},
},
'tables/columns/placeholder': {
url: 'tables?table=placeholderColumns',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/columns/toggleable': {
url: 'tables?table=toggleableColumns',
selector: 'body',
-46
View File
@@ -6,10 +6,6 @@
"": {
"name": "filament",
"devDependencies": {
"@alpinejs/collapse": "^3.10.3",
"@alpinejs/focus": "^3.10.3",
"@alpinejs/mask": "^3.10.3",
"@alpinejs/persist": "^3.10.3",
"@awcodes/alpine-floating-ui": "^3.4.0",
"@babel/runtime": "^7.15.3",
"@danharrin/alpine-mousetrap": "^0.1.0",
@@ -66,33 +62,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@alpinejs/collapse": {
"version": "3.12.3",
"resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.12.3.tgz",
"integrity": "sha512-9yMuFeAeVQh+hDhoyx1ArLI5VzgOIHTjLmRrzKsJdHNKsaGNwDm0Bg/dKMyFxGM9HNTp3JDY159+txVYwK+53w==",
"dev": true
},
"node_modules/@alpinejs/focus": {
"version": "3.12.3",
"resolved": "https://registry.npmjs.org/@alpinejs/focus/-/focus-3.12.3.tgz",
"integrity": "sha512-kIQwvvUPCfCO2REpceQ3uHNdN3oqDLvvvQNaHVgHoYqk+RrL3EcR6uOHyvHJUgOhaIjN5Uc3b7BaRNrKZbDGew==",
"dev": true,
"dependencies": {
"focus-trap": "^6.6.1"
}
},
"node_modules/@alpinejs/mask": {
"version": "3.12.3",
"resolved": "https://registry.npmjs.org/@alpinejs/mask/-/mask-3.12.3.tgz",
"integrity": "sha512-fQpWoR8zpXol7wI4XzsSGZ2urJ4JHyebTXmqdHbHUkRpok2J6goWA/1tGUCLhK8FvuLy+bSGSdzjU7Nh9W/xWQ==",
"dev": true
},
"node_modules/@alpinejs/persist": {
"version": "3.12.3",
"resolved": "https://registry.npmjs.org/@alpinejs/persist/-/persist-3.12.3.tgz",
"integrity": "sha512-TyDzwkFePki6vc+pCdxdNs0cYdBYAJ6W5szXnoQys8iUyshov/6Q9UhKxlXoCE4jvmmXvL+U4vNnnCcnmVD5dg==",
"dev": true
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@@ -5618,15 +5587,6 @@
"node": ">=8"
}
},
"node_modules/focus-trap": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz",
"integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==",
"dev": true,
"dependencies": {
"tabbable": "^5.3.3"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
@@ -10297,12 +10257,6 @@
"node": ">=10.13.0"
}
},
"node_modules/tabbable": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==",
"dev": true
},
"node_modules/tailwindcss": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
-4
View File
@@ -13,10 +13,6 @@
"prettier": "npx prettier --write ."
},
"devDependencies": {
"@alpinejs/collapse": "^3.10.3",
"@alpinejs/focus": "^3.10.3",
"@alpinejs/mask": "^3.10.3",
"@alpinejs/persist": "^3.10.3",
"@awcodes/alpine-floating-ui": "^3.4.0",
"@babel/runtime": "^7.15.3",
"@danharrin/alpine-mousetrap": "^0.1.0",
+37 -21
View File
@@ -12,8 +12,10 @@ Actions may require additional confirmation or input from the user before they r
You may require confirmation before an action is run using the `requiresConfirmation()` method. This is useful for particularly destructive actions, such as those that delete records.
```php
use App\Models\Post;
Action::make('delete')
->action(fn () => $this->record->delete())
->action(fn (Post $record) => $record->delete())
->requiresConfirmation()
```
@@ -28,6 +30,7 @@ You may also render a form in the modal to collect extra information from the us
You may use components from the [Form Builder](../forms) to create custom action modal forms. The data from the form is available in the `$data` array of the `action()` closure:
```php
use App\Models\Post;
use App\Models\User;
use Filament\Forms\Components\Select;
@@ -38,9 +41,9 @@ Action::make('updateAuthor')
->options(User::query()->pluck('name', 'id'))
->required(),
])
->action(function (array $data): void {
$this->record->author()->associate($data['authorId']);
$this->record->save();
->action(function (array $data, Post $record): void {
$record->author()->associate($data['authorId']);
$record->save();
})
```
@@ -51,13 +54,14 @@ Action::make('updateAuthor')
You may fill the form with existing data, using the `fillForm()` method:
```php
use App\Models\Post;
use App\Models\User;
use Filament\Forms\Components\Select;
use Filament\Forms\Form;
Action::make('updateAuthor')
->fillForm([
'authorId' => $this->record->author->id,
->fillForm(fn (Post $record): array => [
'authorId' => $record->author->id,
])
->form([
Select::make('authorId')
@@ -65,9 +69,9 @@ Action::make('updateAuthor')
->options(User::query()->pluck('name', 'id'))
->required(),
])
->action(function (array $data): void {
$this->record->author()->associate($data['authorId']);
$this->record->save();
->action(function (array $data, Post $record): void {
$record->author()->associate($data['authorId']);
$record->save();
})
```
@@ -118,6 +122,7 @@ Action::make('create')
You may wish to disable all form fields in the modal, ensuring the user cannot edit them. You may do so using the `disabledForm()` method:
```php
use App\Models\Post;
use App\Models\User;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
@@ -127,13 +132,13 @@ Action::make('approvePost')
TextInput::make('title'),
Textarea::make('content'),
])
->fillForm([
'title' => $this->record->title,
'content' => $this->record->content,
->fillForm(fn (Post $record): array => [
'title' => $record->title,
'content' => $record->content,
])
->disabledForm()
->action(function (): void {
$this->record->approve();
->action(function (Post $record): void {
$record->approve();
})
```
@@ -142,8 +147,10 @@ Action::make('approvePost')
You may customize the heading, description and label of the submit button in the modal:
```php
use App\Models\Post;
Action::make('delete')
->action(fn () => $this->record->delete())
->action(fn (Post $record) => $record->delete())
->requiresConfirmation()
->modalHeading('Delete post')
->modalDescription('Are you sure you\'d like to delete this post? This cannot be undone.')
@@ -157,8 +164,10 @@ Action::make('delete')
You may add an [icon](https://blade-ui-kit.com/blade-icons?set=1#search) inside the modal using the `modalIcon()` method:
```php
use App\Models\Post;
Action::make('delete')
->action(fn () => $this->record->delete())
->action(fn (Post $record) => $record->delete())
->requiresConfirmation()
->modalIcon('heroicon-o-trash')
```
@@ -168,8 +177,10 @@ Action::make('delete')
By default, the icon will inherit the color of the action button. You may customize the color of the icon using the `modalIconColor()` method:
```php
use App\Models\Post;
Action::make('delete')
->action(fn () => $this->record->delete())
->action(fn (Post $record) => $record->delete())
->requiresConfirmation()
->color('danger')
->modalIcon('heroicon-o-trash')
@@ -198,8 +209,10 @@ Action::make('updateAuthor')
You may define custom content to be rendered inside your modal, which you can specify by passing a Blade view into the `modalContent()` method:
```php
use App\Models\Post;
Action::make('advance')
->action(fn () => $this->record->advance())
->action(fn (Post $record) => $record->advance())
->modalContent(view('filament.pages.actions.advance'))
```
@@ -223,8 +236,10 @@ Action::make('advance')
By default, the custom content is displayed above the modal form if there is one, but you can add content below using `modalContentFooter()` if you wish:
```php
use App\Models\Post;
Action::make('advance')
->action(fn () => $this->record->advance())
->action(fn (Post $record) => $record->advance())
->modalContentFooter(view('filament.pages.actions.advance'))
```
@@ -233,15 +248,16 @@ Action::make('advance')
You can add an action button to your custom modal content, which is useful if you want to add a button that performs an action other than the main action. You can do this by registering an action with the `registerModalActions()` method, and then passing it to the view:
```php
use App\Models\Post;
use Illuminate\Contracts\View\View;
Action::make('advance')
->registerModalActions([
Action::make('report')
->requiresConfirmation()
->action(fn () => $this->record->report()),
->action(fn (Post $record) => $record->report()),
])
->action(fn () => $this->record->advance())
->action(fn (Post $record) => $record->advance())
->modalContent(fn (Action $action): View => view(
'filament.pages.actions.advance',
['action' => $action],
@@ -155,12 +155,13 @@ CreateAction::make()
At any time, you may call `$action->halt()` from inside a lifecycle hook or mutation method, which will halt the entire creation process:
```php
use App\Models\Post;
use Filament\Notifications\Actions\Action;
use Filament\Notifications\Notification;
CreateAction::make()
->before(function (CreateAction $action) {
if (! $this->record->team->subscribed()) {
->before(function (CreateAction $action, Post $record) {
if (! $record->team->subscribed()) {
Notification::make()
->warning()
->title('You don\'t have an active subscription!')
@@ -168,13 +168,14 @@ EditAction::make()
At any time, you may call `$action->halt()` from inside a lifecycle hook or mutation method, which will halt the entire saving process:
```php
use App\Models\Post;
use Filament\Notifications\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\EditAction;
EditAction::make()
->before(function (EditAction $action) {
if (! $this->record->team->subscribed()) {
->before(function (EditAction $action, Post $record) {
if (! $record->team->subscribed()) {
Notification::make()
->warning()
->title('You don\'t have an active subscription!')
@@ -196,4 +197,4 @@ If you'd like the action modal to close too, you can completely `cancel()` the a
```php
$action->cancel();
```
```
@@ -120,12 +120,13 @@ ReplicateAction::make()
At any time, you may call `$action->halt()` from inside a lifecycle hook, which will halt the entire replication process:
```php
use App\Models\Post;
use Filament\Notifications\Actions\Action;
use Filament\Notifications\Notification;
ReplicateAction::make()
->before(function (ReplicateAction $action) {
if (! $this->record->team->subscribed()) {
->before(function (ReplicateAction $action, Post $record) {
if (! $record->team->subscribed()) {
Notification::make()
->warning()
->title('You don\'t have an active subscription!')
@@ -32,11 +32,11 @@ return [
'multiple' => [
'label' => 'Kiválasztottak törlése',
'label' => 'Kijelöltek törlése',
'modal' => [
'heading' => 'Kiválasztott :label törlése',
'heading' => 'Kijelölt :label törlése',
'actions' => [
@@ -32,16 +32,16 @@ return [
'multiple' => [
'label' => 'Kiválasztottak leválasztása',
'label' => 'Kijelöltek leválasztása',
'modal' => [
'heading' => 'Kiválasztott :label leválasztása',
'heading' => 'Kijelölt :label leválasztása',
'actions' => [
'detach' => [
'label' => 'Kiválasztottak leválasztása',
'label' => 'Kijelöltek leválasztása',
],
],
@@ -32,16 +32,16 @@ return [
'multiple' => [
'label' => 'Kiválasztottak szétválasztása',
'label' => 'Kijelöltek szétválasztása',
'modal' => [
'heading' => 'Kiválasztott :label szétválasztása',
'heading' => 'Kijelölt :label szétválasztása',
'actions' => [
'dissociate' => [
'label' => 'Kiválasztottak szétválasztása',
'label' => 'Kijelöltek szétválasztása',
],
],
@@ -32,11 +32,11 @@ return [
'multiple' => [
'label' => 'A kiválasztottak törlése mindenképp',
'label' => 'Kijelöltek törlése mindenképp',
'modal' => [
'heading' => 'A kiválasztott :label törlése mindenképp',
'heading' => 'Kijelölt :label törlése mindenképp',
'actions' => [
@@ -32,11 +32,11 @@ return [
'multiple' => [
'label' => 'Kiválasztottak visszaállítása',
'label' => 'Kijelöltek visszaállítása',
'modal' => [
'heading' => 'Kiválasztott :label visszaállítása',
'heading' => 'Kijelölt :label visszaállítása',
'actions' => [
@@ -4,7 +4,7 @@ return [
'single' => [
'label' => 'Novo :label',
'label' => 'Adicionar :label',
'modal' => [
@@ -13,7 +13,7 @@ return [
'fields' => [
'record_id' => [
'label' => 'Danh sách',
'label' => 'Bản ghi',
],
],
@@ -25,7 +25,7 @@ return [
],
'associate_another' => [
'label' => 'Liên kết & tiếp tục liên kết với mục khác',
'label' => 'Liên kết & liên kết thêm',
],
],
@@ -13,7 +13,7 @@ return [
'fields' => [
'record_id' => [
'label' => 'Danh sách',
'label' => 'Bản ghi',
],
],
@@ -25,7 +25,7 @@ return [
],
'attach_another' => [
'label' => 'Đính kèm & tiếp tục đính kèm với mục khác',
'label' => 'Đính kèm & đính kèm thêm',
],
],
@@ -4,7 +4,7 @@ return [
'single' => [
'label' => 'Tạo :label mới',
'label' => 'Tạo mới :label',
'modal' => [
@@ -17,7 +17,7 @@ return [
],
'create_another' => [
'label' => 'Tạo & tiếp tục tạo mới',
'label' => 'Tạo & tạo thêm',
],
],
@@ -4,16 +4,16 @@ return [
'single' => [
'label' => 'Huỷ đính kèm',
'label' => 'Tách',
'modal' => [
'heading' => 'Huỷ đính kèm :label',
'heading' => 'Tách :label',
'actions' => [
'detach' => [
'label' => 'Huỷ đính kèm',
'label' => 'Tách',
],
],
@@ -23,7 +23,7 @@ return [
'notifications' => [
'detached' => [
'title' => 'Đã huỷ đính kèm',
'title' => 'Đã tách',
],
],
@@ -32,16 +32,16 @@ return [
'multiple' => [
'label' => 'Huỷ đính kèm với những mục đã chọn',
'label' => 'Tách các mục đã chọn',
'modal' => [
'heading' => 'Huỷ đính kèm :label đã chọn',
'heading' => 'Tách các mục :label đã chọn',
'actions' => [
'detach' => [
'label' => 'Huỷ đính kèm với những mục đã chọn',
'label' => 'Tách',
],
],
@@ -51,7 +51,7 @@ return [
'notifications' => [
'detached' => [
'title' => 'Đã huỷ đính kèm',
'title' => 'Đã tách',
],
],
+2 -2
View File
@@ -4,11 +4,11 @@ return [
'single' => [
'label' => 'Sửa',
'label' => 'Chỉnh sửa',
'modal' => [
'heading' => 'Sửa :label',
'heading' => 'Chỉnh sửa :label',
'actions' => [
@@ -36,7 +36,7 @@ return [
'modal' => [
'heading' => 'Xóa vĩnh viễn :label đã chọn',
'heading' => 'Xóa vĩnh viễn các mục :label đã chọn',
'actions' => [
+2 -2
View File
@@ -2,12 +2,12 @@
return [
'confirmation' => 'Bạn có chắc chắn khi thực hiện hành động này?',
'confirmation' => 'Bạn có chắc chắn khi thực hiện thao tác này?',
'actions' => [
'cancel' => [
'label' => 'Huỷ',
'label' => 'Huỷ bỏ',
],
'confirm' => [
@@ -4,16 +4,16 @@ return [
'single' => [
'label' => 'Bản sao',
'label' => 'Sao chép',
'modal' => [
'heading' => 'Bản sao :label',
'heading' => 'Sao chép :label',
'actions' => [
'replicate' => [
'label' => 'Bản sao',
'label' => 'Sao chép',
],
],
@@ -23,7 +23,7 @@ return [
'notifications' => [
'replicated' => [
'title' => 'Đã tạo bản sao',
'title' => 'Đã sao chép',
],
],
@@ -36,7 +36,7 @@ return [
'modal' => [
'heading' => 'Khôi phục :label đã chọn',
'heading' => 'Khôi phục các mục :label đã chọn',
'actions' => [
@@ -27,6 +27,7 @@
match ($alignment) {
Alignment::Center, 'center' => 'justify-center',
Alignment::End, Alignment::Right, 'end', 'right' => 'flex-row-reverse',
Alignment::Between, 'between' => 'justify-between',
default => 'justify-start',
} => ! $fullWidth,
'grid grid-cols-[repeat(auto-fit,minmax(0,1fr))]' => $fullWidth,
@@ -57,65 +57,6 @@
@endphp
@endif
@if ($this instanceof \Filament\Infolists\Contracts\HasInfolists && (! $this->hasInfolistsModalRendered))
<form wire:submit.prevent="callMountedInfolistAction">
@php
$action = $this->getMountedInfolistAction();
@endphp
<x-filament::modal
:alignment="$action?->getModalAlignment()"
:close-button="$action?->hasModalCloseButton()"
:close-by-clicking-away="$action?->isModalClosedByClickingAway()"
:description="$action?->getModalDescription()"
display-classes="block"
:footer-actions="$action?->getVisibleModalFooterActions()"
:footer-actions-alignment="$action?->getModalFooterActionsAlignment()"
:heading="$action?->getModalHeading()"
:icon="$action?->getModalIcon()"
:icon-color="$action?->getModalIconColor()"
:id="$this->getId() . '-infolist-action'"
:slide-over="$action?->isModalSlideOver()"
:sticky-footer="$action?->isModalFooterSticky()"
:sticky-header="$action?->isModalHeaderSticky()"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:wire:key="$action ? $this->getId() . '.infolist.actions.' . $action->getName() . '.modal' : null"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedInfolistActions.length) open()"
x-on:modal-closed.stop="
const mountedInfolistActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedInfolistActionShouldOpenModal()) }}
if (! mountedInfolistActionShouldOpenModal) {
return
}
if ($wire.mountedFormComponentActions.length) {
return
}
$wire.unmountInfolistAction(false)
"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->getId() }}') close()"
>
@if ($action)
{{ $action->getModalContent() }}
@if (count(($infolist = $action->getInfolist())?->getComponents() ?? []))
{{ $infolist }}
@elseif ($this->mountedInfolistActionHasForm())
{{ $this->getMountedInfolistActionForm() }}
@endif
{{ $action->getModalContentFooter() }}
@endif
</x-filament::modal>
</form>
@php
$this->hasInfolistsModalRendered = true;
@endphp
@endif
@if ($this instanceof \Filament\Tables\Contracts\HasTable && (! $this->hasTableModalRendered))
<form wire:submit.prevent="callMountedTableAction">
@php
@@ -228,6 +169,65 @@
@endphp
@endif
@if ($this instanceof \Filament\Infolists\Contracts\HasInfolists && (! $this->hasInfolistsModalRendered))
<form wire:submit.prevent="callMountedInfolistAction">
@php
$action = $this->getMountedInfolistAction();
@endphp
<x-filament::modal
:alignment="$action?->getModalAlignment()"
:close-button="$action?->hasModalCloseButton()"
:close-by-clicking-away="$action?->isModalClosedByClickingAway()"
:description="$action?->getModalDescription()"
display-classes="block"
:footer-actions="$action?->getVisibleModalFooterActions()"
:footer-actions-alignment="$action?->getModalFooterActionsAlignment()"
:heading="$action?->getModalHeading()"
:icon="$action?->getModalIcon()"
:icon-color="$action?->getModalIconColor()"
:id="$this->getId() . '-infolist-action'"
:slide-over="$action?->isModalSlideOver()"
:sticky-footer="$action?->isModalFooterSticky()"
:sticky-header="$action?->isModalHeaderSticky()"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:wire:key="$action ? $this->getId() . '.infolist.actions.' . $action->getName() . '.modal' : null"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedInfolistActions.length) open()"
x-on:modal-closed.stop="
const mountedInfolistActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedInfolistActionShouldOpenModal()) }}
if (! mountedInfolistActionShouldOpenModal) {
return
}
if ($wire.mountedFormComponentActions.length) {
return
}
$wire.unmountInfolistAction(false)
"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->getId() }}') close()"
>
@if ($action)
{{ $action->getModalContent() }}
@if (count(($infolist = $action->getInfolist())?->getComponents() ?? []))
{{ $infolist }}
@elseif ($this->mountedInfolistActionHasForm())
{{ $this->getMountedInfolistActionForm() }}
@endif
{{ $action->getModalContentFooter() }}
@endif
</x-filament::modal>
</form>
@php
$this->hasInfolistsModalRendered = true;
@endphp
@endif
@if (! $this->hasFormsModalRendered)
@php
$action = $this->getMountedFormComponentAction();
@@ -3,6 +3,7 @@
dynamic-component="filament::link"
:icon-position="$getIconPosition()"
:icon-size="$getIconSize()"
tag="button"
class="fi-ac-link-group"
>
{{ $getLabel() }}
+5
View File
@@ -84,4 +84,9 @@ class Action extends MountableAction implements Contracts\Groupable, Contracts\H
$this->record(null);
}
public function getInfolistName(): string
{
return 'mountedActionInfolist';
}
}
+11 -2
View File
@@ -3,6 +3,7 @@
namespace Filament\Actions\Concerns;
use Closure;
use Filament\Actions\Contracts\HasRecord;
use Filament\Actions\MountableAction;
use Filament\Actions\StaticAction;
use Filament\Support\Enums\Alignment;
@@ -401,8 +402,16 @@ trait CanOpenModal
return $action;
}
return $action
->livewire($this->getLivewire());
$action->livewire($this->getLivewire());
if (
($this instanceof HasRecord) &&
($action instanceof HasRecord)
) {
$action->record($this->getRecord());
}
return $action;
}
/**
+13 -1
View File
@@ -26,7 +26,14 @@ trait HasInfolist
public function getInfolist(): ?Infolist
{
$infolist = Infolist::make($this->getLivewire());
$infolistName = $this->getInfolistName();
if (blank($infolistName)) {
return null;
}
$infolist = Infolist::make($this->getLivewire())
->name($infolistName);
if ($this instanceof HasRecord) {
$infolist->record($this->getRecord());
@@ -50,4 +57,9 @@ trait HasInfolist
return $modifiedInfolist;
}
public function getInfolistName(): ?string
{
return null;
}
}
@@ -6,6 +6,7 @@ use Closure;
use Filament\Actions\Action;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Infolists\Infolist;
use Filament\Support\Exceptions\Cancel;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Model;
@@ -419,4 +420,9 @@ trait InteractsWithActions
{
return null;
}
public function mountedActionInfolist(): Infolist
{
return $this->getMountedAction()->getInfolist();
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Cancel;
use Filament\Support\Exceptions\Halt;
class MountableAction extends StaticAction implements HasLivewire
abstract class MountableAction extends StaticAction implements HasLivewire
{
use Concerns\BelongsToLivewire;
use Concerns\CanBeMounted;
+1 -1
View File
@@ -1 +1 @@
function i({state:o}){return{state:o,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0&&this.addRow(),this.shouldUpdateRows=!0,this.$watch("state",()=>{if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState(),this.shouldUpdateRows=!0},reorderRows:function(t){let e=Alpine.raw(this.rows),s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.rows=e,this.updateState()},updateRows:function(){let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{i as default};
function r({state:i}){return{state:i,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0&&this.addRow(),this.updateState(),this.$watch("state",(t,e)=>{let s=o=>o===null?0:Array.isArray(o)?o.length:typeof o!="object"?0:Object.keys(o).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows),s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.rows=e,this.updateState()},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default};
+1 -1
View File
@@ -1 +1 @@
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){$nextTick(()=>{let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};
+1 -1
View File
@@ -1 +1 @@
function t({initialHeight:e}){return{render:function(){this.$el.scrollHeight>0&&(this.$el.style.height=e+"rem",this.$el.style.height=this.$el.scrollHeight+"px")}}}export{t as default};
function t({initialHeight:e}){return{init:function(){this.render()},render:function(){this.$el.scrollHeight>0&&(this.$el.style.height=e+"rem",this.$el.style.height=this.$el.scrollHeight+"px")}}}export{t as default};
+1 -1
View File
@@ -1 +1 @@
(()=>{function b(n){n.directive("mask",(e,{value:t,expression:u},{effect:f,evaluateLater:c})=>{let r=()=>u,l="";queueMicrotask(()=>{if(["function","dynamic"].includes(t)){let o=c(u);f(()=>{r=a=>{let s;return n.dontAutoEvaluateFunctions(()=>{o(d=>{s=typeof d=="function"?d(a):d},{scope:{$input:a,$money:I.bind({el:e})}})}),s},i(e,!1)})}else i(e,!1);e._x_model&&e._x_model.set(e.value)}),e.addEventListener("input",()=>i(e)),e.addEventListener("blur",()=>i(e,!1));function i(o,a=!0){let s=o.value,d=r(s);if(!d||d==="false")return!1;if(l.length-o.value.length===1)return l=o.value;let g=()=>{l=o.value=p(s,d)};a?k(o,d,()=>{g()}):g()}function p(o,a){if(o==="")return"";let s=h(a,o);return m(a,s)}}).before("model")}function k(n,e,t){let u=n.selectionStart,f=n.value;t();let c=f.slice(0,u),r=m(e,h(e,c)).length;n.setSelectionRange(r,r)}function h(n,e){let t=e,u="",f={9:/[0-9]/,a:/[a-zA-Z]/,"*":/[a-zA-Z0-9]/},c="";for(let r=0;r<n.length;r++){if(["9","a","*"].includes(n[r])){c+=n[r];continue}for(let l=0;l<t.length;l++)if(t[l]===n[r]){t=t.slice(0,l)+t.slice(l+1);break}}for(let r=0;r<c.length;r++){let l=!1;for(let i=0;i<t.length;i++)if(f[c[r]].test(t[i])){u+=t[i],t=t.slice(0,i)+t.slice(i+1),l=!0;break}if(!l)break}return u}function m(n,e){let t=Array.from(e),u="";for(let f=0;f<n.length;f++){if(!["9","a","*"].includes(n[f])){u+=n[f];continue}if(t.length===0)break;u+=t.shift()}return u}function I(n,e=".",t,u=2){if(n==="-")return"-";if(/^\D+$/.test(n))return"9";t=t??(e===","?".":",");let f=(i,p)=>{let o="",a=0;for(let s=i.length-1;s>=0;s--)i[s]!==p&&(a===3?(o=i[s]+p+o,a=0):o=i[s]+o,a++);return o},c=n.startsWith("-")?"-":"",r=n.replaceAll(new RegExp(`[^0-9\\${e}]`,"g"),""),l=Array.from({length:r.split(e)[0].length}).fill("9").join("");return l=`${c}${f(l,t)}`,u>0&&n.includes(e)&&(l+=`${e}`+"9".repeat(u)),queueMicrotask(()=>{this.el.value.endsWith(e)||this.el.value[this.el.selectionStart-1]===e&&this.el.setSelectionRange(this.el.selectionStart-1,this.el.selectionStart-1)}),l}var v=b;document.addEventListener("alpine:init",()=>{window.Alpine.plugin(v)});})();
(()=>{})();
+1 -1
View File
@@ -391,7 +391,7 @@ Select::make('status')
->disableOptionWhen(fn (string $value): bool => $value === 'published')
```
If you want to retrieve the options that have not been disabled, e.g., for validation purposes, you can do so using `getEnabledOptions()`:
If you want to retrieve the options that have not been disabled, e.g. for validation purposes, you can do so using `getEnabledOptions()`:
```php
use Filament\Forms\Components\Select;
@@ -98,6 +98,40 @@ CheckboxList::make('technologies')
<AutoScreenshot name="forms/fields/checkbox-list/rows" alt="Checkbox list with 2 rows" version="3.x" />
## Disabling specific options
You can disable specific options using the `disableOptionWhen()` method. It accepts a closure, in which you can check if the option with a specific `$value` should be disabled:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
'tailwind' => 'Tailwind CSS',
'alpine' => 'Alpine.js',
'laravel' => 'Laravel',
'livewire' => 'Laravel Livewire',
])
->disableOptionWhen(fn (string $value): bool => $value === 'livewire')
```
If you want to retrieve the options that have not been disabled, e.g. for validation purposes, you can do so using `getEnabledOptions()`:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
'tailwind' => 'Tailwind CSS',
'alpine' => 'Alpine.js',
'laravel' => 'Laravel',
'livewire' => 'Laravel Livewire',
'heroicons' => 'SVG icons',
])
->disableOptionWhen(fn (string $value): bool => $value === 'heroicons')
->in(fn (CheckboxList $component): array => array_keys($component->getEnabledOptions()))
```
## Searching options
You may enable a search input to allow easier access to many options, using the `searchable()` method:
+1 -1
View File
@@ -87,7 +87,7 @@ Radio::make('status')
<AutoScreenshot name="forms/fields/radio/disabled-option" alt="Radio with disabled option" version="3.x" />
If you want to retrieve the options that have not been disabled, e.g., for validation purposes, you can do so using `getEnabledOptions()`:
If you want to retrieve the options that have not been disabled, e.g. for validation purposes, you can do so using `getEnabledOptions()`:
```php
use Filament\Forms\Components\Radio;
@@ -403,6 +403,35 @@ Any fields that you use from `$state` should be `live()` if you wish to see the
<AutoScreenshot name="forms/fields/repeater/labelled" alt="Repeater with item labels" version="3.x" />
## Simple repeaters with one field
You can use the `simple()` method to create a repeater with a single field, using a minimal design
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
Repeater::make('invitations')
->simple(
TextInput::make('email')
->email()
->required(),
)
```
<AutoScreenshot name="forms/fields/repeater/simple-one-field" alt="Simple repeater design with only one field" version="3.x" />
Instead of using a nested array to store data, simple repeaters use a flat array of values. This means that the data structure for the above example could look like this:
```php
[
'invitations' => [
'dan@filamentphp.com',
'ryan@filamentphp.com',
],
],
```
## Using `$get()` to access parent field values
All form components are able to [use `$get()` and `$set()`](../advanced) to access another field's value. However, you might experience unexpected behaviour when using this inside the repeater's schema.
@@ -15,6 +15,23 @@ ViewField::make('rating')
This assumes that you have a `resources/views/filament/forms/components/range-slider.blade.php` file.
### Passing data to view fields
You can pass a simple array of data to to the view using `viewData()`:
```php
use Filament\Forms\Components\ViewField;
ViewField::make('rating')
->view('filament.forms.components.range-slider')
->viewData([
'min' => 1,
'max' => 5,
])
```
However, more complex configuration can be achieved with a [custom field class](#custom-field-classes).
## Custom field classes
You may create your own custom field classes and views, which you can reuse across your project, and even release as a plugin to the community.
@@ -8,10 +8,11 @@ import AutoScreenshot from "@components/AutoScreenshot.astro"
Placeholders can be used to render text-only "fields" within your forms. Each placeholder has `content()`, which cannot be changed by the user.
```php
use App\Models\Post;
use Filament\Forms\Components\Placeholder;
Placeholder::make('created')
->content($this->getRecord()->created_at->toFormattedDateString())
->content(fn (Post $record): string => $record->created_at->toFormattedDateString())
```
<AutoScreenshot name="forms/layout/placeholder/simple" alt="Placeholder" version="3.x" />
@@ -79,7 +79,7 @@ class CreatePost extends Component implements HasForms
public function render(): View
{
return view('create-post');
return view('livewire.create-post');
}
}
```
@@ -13,12 +13,26 @@ export default function keyValueFormComponent({ state }) {
this.addRow()
}
this.shouldUpdateRows = true
this.updateState()
this.$watch('state', () => {
if (!this.shouldUpdateRows) {
this.shouldUpdateRows = true
this.$watch('state', (state, oldState) => {
const getLength = (value) => {
if (value === null) {
return 0
}
if (Array.isArray(value)) {
return value.length
}
if (typeof value !== 'object') {
return 0
}
return Object.keys(value).length
}
if (getLength(state) === 0 && getLength(oldState) === 0) {
return
}
@@ -40,8 +54,6 @@ export default function keyValueFormComponent({ state }) {
}
this.updateState()
this.shouldUpdateRows = true
},
reorderRows: function (event) {
@@ -56,6 +68,12 @@ export default function keyValueFormComponent({ state }) {
},
updateRows: function () {
if (!this.shouldUpdateRows) {
this.shouldUpdateRows = true
return
}
let rows = []
for (let [key, value] of Object.entries(this.state ?? {})) {
@@ -38,7 +38,7 @@ export default function tagsInputFormComponent({ state, splitKeys }) {
}
},
['x-on:paste']() {
$nextTick(() => {
this.$nextTick(() => {
const pattern = splitKeys
.map((key) =>
key.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'),
@@ -1,5 +1,9 @@
export default function textareaFormComponent({ initialHeight }) {
return {
init: function () {
this.render()
},
render: function () {
if (this.$el.scrollHeight > 0) {
this.$el.style.height = initialHeight + 'rem'
-6
View File
@@ -1,12 +1,6 @@
import mask from '@alpinejs/mask'
import '../css/components/date-time-picker.css'
import '../css/components/file-upload.css'
import '../css/components/markdown-editor.css'
import '../css/components/rich-editor.css'
import '../css/components/select.css'
import '../css/components/tags-input.css'
document.addEventListener('alpine:init', () => {
window.Alpine.plugin(mask)
})
@@ -43,11 +43,11 @@ return [
],
'collapse_all' => [
'label' => 'Becsuk minden',
'label' => 'Becsuk mindent',
],
'expand_all' => [
'label' => 'Kibont minden',
'label' => 'Kibont mindent',
],
],
@@ -278,11 +278,11 @@ return [
],
'collapse_all' => [
'label' => 'Becsuk minden',
'label' => 'Becsuk mindent',
],
'expand_all' => [
'label' => 'Kibont minden',
'label' => 'Kibont mindent',
],
],
@@ -298,7 +298,7 @@ return [
'actions' => [
'link' => 'Link',
'unlink' => 'Unlink',
'unlink' => 'Remover link',
],
'label' => 'URL',
@@ -394,7 +394,7 @@ return [
],
'tags_input' => [
'placeholder' => 'Novo rótulo',
'placeholder' => 'Nova tag',
],
'wizard' => [
+25 -25
View File
@@ -7,15 +7,15 @@ return [
'actions' => [
'clone' => [
'label' => 'Nhân bản',
'label' => 'Sao chép',
],
'add' => [
'label' => 'Thêm :label mới',
'label' => 'Thêm vào :label',
],
'add_between' => [
'label' => 'Chèn',
'label' => 'Chèn vào giữa các khối',
],
'delete' => [
@@ -27,11 +27,11 @@ return [
],
'move_down' => [
'label' => 'Chuyển xuống',
'label' => 'Di chuyển xuống',
],
'move_up' => [
'label' => 'Chuyển lên',
'label' => 'Di chuyển lên',
],
'collapse' => [
@@ -117,11 +117,11 @@ return [
],
'rotate_left' => [
'label' => 'Xoay ảnh sang trái',
'label' => 'Xoay hình ảnh sang trái',
],
'rotate_right' => [
'label' => 'Xoay ảnh sang phải',
'label' => 'Xoay hình ảnh sang phải',
],
'set_aspect_ratio' => [
@@ -133,7 +133,7 @@ return [
],
'zoom_100' => [
'label' => 'Thu phóng hình ảnh đến 100%',
'label' => 'Phóng to hình ảnh lên 100%',
],
'zoom_in' => [
@@ -202,7 +202,7 @@ return [
],
'reorder' => [
'label' => 'Xếp lại dòng',
'label' => 'Sắp xếp dòng',
],
],
@@ -228,7 +228,7 @@ return [
'blockquote' => 'Trích dẫn',
'bold' => 'In đậm',
'bullet_list' => 'Danh sách đánh dấu',
'code_block' => 'Code block',
'code_block' => 'Khối mã',
'heading' => 'Tiêu đề',
'italic' => 'In nghiêng',
'link' => 'Liên kết',
@@ -246,27 +246,27 @@ return [
'actions' => [
'add' => [
'label' => 'Thêm :label mới',
'label' => 'Thêm vào :label',
],
'delete' => [
'label' => 'Xóa',
],
'clone' => [
'label' => 'Sao chép',
],
'reorder' => [
'label' => 'Di chuyển',
],
'clone' => [
'label' => 'Nhân bản',
],
'move_down' => [
'label' => 'Chuyển xuống',
'label' => 'Di chuyển xuống',
],
'move_up' => [
'label' => 'Chuyển lên',
'label' => 'Di chuyển lên',
],
'collapse' => [
@@ -313,7 +313,7 @@ return [
'blockquote' => 'Trích dẫn',
'bold' => 'In đậm',
'bullet_list' => 'Danh sách đánh dấu',
'code_block' => 'Code block',
'code_block' => 'Khối mã',
'h1' => 'Tiêu đề chính',
'h2' => 'Tiêu đề',
'h3' => 'Tiêu đề phụ',
@@ -345,7 +345,7 @@ return [
],
'create_another' => [
'label' => 'Tạo và thêm một mục khác',
'label' => 'Tạo & tạo thêm',
],
],
@@ -381,15 +381,15 @@ return [
'loading_message' => 'Đang tải...',
'max_items_message' => 'Chỉ có :count có thể chọn được.',
'max_items_message' => 'Chỉ có thể chọn :count mục.',
'no_search_results_message' => 'Không có tùy chọn nào phù hợp với tìm kiếm của bạn.',
'no_search_results_message' => 'Không có kết quả tìm kiếm phù hợp.',
'placeholder' => 'Chọn một mục',
'placeholder' => 'Chọn một tuỳ chọn',
'searching_message' => 'Đang tìm kiếm...',
'search_prompt' => 'Gõ để tìm kiếm...',
'search_prompt' => 'Bắt đầu gõ để tìm kiếm...',
],
@@ -402,11 +402,11 @@ return [
'actions' => [
'previous_step' => [
'label' => 'Trước',
'label' => 'Quay lại',
],
'next_step' => [
'label' => 'Sau',
'label' => 'Tiếp theo',
],
],
@@ -28,8 +28,13 @@
->class(['fi-fo-builder grid gap-y-4'])
}}
>
@if ((count($containers) > 1) && $isCollapsible)
<div class="flex gap-x-3">
@if ($isCollapsible)
<div
@class([
'flex gap-x-3',
'hidden' => count($containers) < 2,
])
>
<span
x-on:click="$dispatch('builder-collapse', '{{ $statePath }}')"
>
@@ -61,7 +66,6 @@
x-data="{
isCollapsed: @js($isCollapsed($item)),
}"
x-bind:class="isCollapsed && 'fi-collapsed'"
x-on:builder-expand.window="$event.detail === '{{ $statePath }}' && (isCollapsed = false)"
x-on:builder-collapse.window="$event.detail === '{{ $statePath }}' && (isCollapsed = true)"
x-on:expand-concealing-component.window="
@@ -88,6 +92,7 @@
)
"
x-sortable-item="{{ $uuid }}"
x-bind:class="isCollapsed && 'fi-collapsed'"
class="fi-fo-builder-item rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10"
>
@if ($isReorderableWithDragAndDrop || $isReorderableWithButtons || $hasBlockLabels || $isCloneable || $isDeletable || $isCollapsible)
@@ -182,7 +182,7 @@
:attributes="
\Filament\Support\prepare_inherited_attributes($getExtraInputAttributeBag())
->merge([
'disabled' => $isDisabled,
'disabled' => $isDisabled || $isOptionDisabled($value, $label),
'value' => $value,
'wire:loading.attr' => 'disabled',
$applyStateBindingModifiers('wire:model') => $statePath,
@@ -27,8 +27,13 @@
->class(['fi-fo-repeater grid gap-y-4'])
}}
>
@if ((count($containers) > 1) && $isCollapsible)
<div class="flex gap-x-3">
@if ($isCollapsible)
<div
@class([
'flex gap-x-3',
'hidden' => count($containers) < 2,
])
>
<span
x-on:click="$dispatch('repeater-collapse', '{{ $statePath }}')"
>
@@ -66,7 +71,6 @@
x-data="{
isCollapsed: @js($isCollapsed($item)),
}"
x-bind:class="isCollapsed && 'fi-collapsed'"
x-on:expand-concealing-component.window="
error = $el.querySelector('[data-validation-error]')
@@ -93,6 +97,7 @@
x-on:repeater-expand.window="$event.detail === '{{ $statePath }}' && (isCollapsed = false)"
x-on:repeater-collapse.window="$event.detail === '{{ $statePath }}' && (isCollapsed = true)"
x-sortable-item="{{ $uuid }}"
x-bind:class="isCollapsed && 'fi-collapsed'"
class="fi-fo-repeater-item rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10"
>
@if ($isReorderableWithDragAndDrop || $isReorderableWithButtons || filled($itemLabel) || $isCloneable || $isDeletable || $isCollapsible)
@@ -133,7 +138,7 @@
@if ($isCloneable || $isDeletable || $isCollapsible)
<ul class="-me-1.5 ms-auto flex">
@if ($cloneAction->isVisible())
@if ($isCloneable)
<li>
{{ $cloneAction(['item' => $uuid]) }}
</li>
@@ -0,0 +1,99 @@
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
@php
$containers = $getChildComponentContainers();
$addAction = $getAction($getAddActionName());
$cloneAction = $getAction($getCloneActionName());
$deleteAction = $getAction($getDeleteActionName());
$moveDownAction = $getAction($getMoveDownActionName());
$moveUpAction = $getAction($getMoveUpActionName());
$reorderAction = $getAction($getReorderActionName());
$isAddable = $isAddable();
$isCloneable = $isCloneable();
$isDeletable = $isDeletable();
$isReorderableWithButtons = $isReorderableWithButtons();
$isReorderableWithDragAndDrop = $isReorderableWithDragAndDrop();
$statePath = $getStatePath();
@endphp
<div
x-data="{}"
{{
$attributes
->merge($getExtraAttributes(), escape: false)
->class(['fi-fo-simple-repeater grid gap-y-4'])
}}
>
@if (count($containers))
<ul>
<x-filament::grid
:default="$getGridColumns('default')"
:sm="$getGridColumns('sm')"
:md="$getGridColumns('md')"
:lg="$getGridColumns('lg')"
:xl="$getGridColumns('xl')"
:two-xl="$getGridColumns('2xl')"
:wire:end.stop="'mountFormComponentAction(\'' . $statePath . '\', \'reorder\', { items: $event.target.sortable.toArray() })'"
x-sortable
class="gap-4"
>
@foreach ($containers as $uuid => $item)
<li
wire:key="{{ $this->getId() }}.{{ $item->getStatePath() }}.{{ $field::class }}.item"
x-sortable-item="{{ $uuid }}"
class="fi-fo-repeater-item simple flex justify-start gap-x-3"
>
<div class="flex-1">
{{ $item }}
</div>
@if ($isReorderableWithDragAndDrop || $isReorderableWithButtons || $isCloneable || $isDeletable)
<ul class="flex items-center gap-x-1">
@if ($isReorderableWithDragAndDrop)
<li x-sortable-handle>
{{ $reorderAction }}
</li>
@endif
@if ($isReorderableWithButtons)
<li
class="flex items-center justify-center"
>
{{ $moveUpAction(['item' => $uuid])->disabled($loop->first) }}
</li>
<li
class="flex items-center justify-center"
>
{{ $moveDownAction(['item' => $uuid])->disabled($loop->last) }}
</li>
@endif
@if ($isCloneable)
<li>
{{ $cloneAction(['item' => $uuid]) }}
</li>
@endif
@if ($isDeletable)
<li>
{{ $deleteAction(['item' => $uuid]) }}
</li>
@endif
</ul>
@endif
</li>
@endforeach
</x-filament::grid>
</ul>
@endif
@if ($isAddable)
<div class="flex justify-center">
{{ $addAction }}
</div>
@endif
</div>
</x-dynamic-component>
@@ -13,7 +13,7 @@
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('textarea', 'filament/forms') }}"
x-data="textareaFormComponent({ initialHeight: @js($initialHeight) })"
x-ignore
x-on:load.window="render()"
x-intersect.once="render()"
x-on:input="render()"
x-on:resize.window="render()"
style="height: {{ $initialHeight }}rem"
@@ -42,6 +42,7 @@
'role' => 'switch',
'type' => 'button',
'wire:loading.attr' => 'disabled',
'wire:target' => $statePath,
], escape: false)
->merge($getExtraAttributes(), escape: false)
->merge($getExtraAlpineAttributes(), escape: false)
@@ -80,4 +80,9 @@ class Action extends MountableAction
default => parent::resolveDefaultClosureDependencyForEvaluationByType($parameterType),
};
}
public function getInfolistName(): string
{
return 'mountedFormComponentActionInfolist';
}
}
@@ -170,8 +170,7 @@ class BaseFileUpload extends Field
return null;
}
/** @phpstan-ignore-next-line */
if ($component->shouldMoveFiles() && ($component->getDiskName() == $file->disk)) {
if ($component->shouldMoveFiles() && ($component->getDiskName() == invade($file)->disk)) {
$newPath = trim($component->getDirectory() . '/' . $component->getUploadedFileNameForStorage($file), '/');
$component->getDisk()->move($file->path(), $newPath);
@@ -17,6 +17,7 @@ use Illuminate\Support\Str;
class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidationRules
{
use Concerns\CanBeSearchable;
use Concerns\CanDisableOptions;
use Concerns\HasExtraInputAttributes;
use Concerns\HasGridDirection;
use Concerns\HasNestedRecursiveValidationRules;
+114 -13
View File
@@ -24,11 +24,6 @@ class Repeater extends Field implements Contracts\CanConcealComponents
use Concerns\HasContainerGridLayout;
use Concerns\CanBeCloned;
/**
* @var view-string
*/
protected string $view = 'filament-forms::components.repeater';
protected string | Closure | null $addActionLabel = null;
protected bool | Closure $isAddable = true;
@@ -51,6 +46,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
protected string | Closure | null $itemLabel = null;
protected Field | Closure | null $simpleField = null;
protected ?Closure $modifyRelationshipQueryUsing = null;
protected ?Closure $modifyAddActionUsing = null;
@@ -79,6 +76,11 @@ class Repeater extends Field implements Contracts\CanConcealComponents
protected ?Closure $mutateRelationshipDataBeforeSaveUsing = null;
/**
* @var array<string, mixed> | null
*/
protected ?array $hydratedDefaultState = null;
protected function setUp(): void
{
parent::setUp();
@@ -86,10 +88,20 @@ class Repeater extends Field implements Contracts\CanConcealComponents
$this->defaultItems(1);
$this->afterStateHydrated(static function (Repeater $component, ?array $state): void {
if (is_array($component->hydratedDefaultState)) {
$component->mergeHydratedDefaultStateWithChildComponentContainerState();
return;
}
$items = [];
$simpleField = $component->getSimpleField();
foreach ($state ?? [] as $itemData) {
$items[(string) Str::uuid()] = $itemData;
$items[(string) Str::uuid()] = $simpleField ?
[$simpleField->getName() => $itemData] :
$itemData;
}
$component->state($items);
@@ -108,7 +120,14 @@ class Repeater extends Field implements Contracts\CanConcealComponents
fn (Repeater $component): Action => $component->getReorderAction(),
]);
$this->mutateDehydratedStateUsing(static function (?array $state): array {
$this->mutateDehydratedStateUsing(static function (Repeater $component, ?array $state): array {
if ($simpleField = $component->getSimpleField()) {
return collect($state ?? [])
->values()
->pluck($simpleField->getName())
->all();
}
return array_values($state ?? []);
});
}
@@ -487,16 +506,35 @@ class Repeater extends Field implements Contracts\CanConcealComponents
public function defaultItems(int | Closure $count): static
{
$this->default(static function (Repeater $component) use ($count): array {
$items = [];
$count = $component->evaluate($count);
if (! $count) {
return $items;
return [];
}
foreach (range(1, $count) as $index) {
$items[(string) Str::uuid()] = [];
return array_fill(0, $count, $component->isSimple() ? null : []);
}, shouldPreserveChildState: false);
return $this;
}
public function default(mixed $state, bool $shouldPreserveChildState = true): static
{
parent::default(function (Repeater $component) use ($shouldPreserveChildState, $state) {
$state = $component->evaluate($state);
$simpleField = $component->getSimpleField();
$items = [];
foreach ($state ?? [] as $itemData) {
$items[(string) Str::uuid()] = $simpleField ?
[$simpleField->getName() => $itemData] :
$itemData;
}
if ($shouldPreserveChildState) {
$component->hydratedDefaultState = $items;
}
return $items;
@@ -578,6 +616,15 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return $this;
}
public function getChildComponents(): array
{
if ($simpleField = $this->getSimpleField()) {
return [$simpleField];
}
return parent::getChildComponents();
}
/**
* @return array<ComponentContainer>
*/
@@ -672,7 +719,13 @@ class Repeater extends Field implements Contracts\CanConcealComponents
$this->relationship = $name ?? $this->getName();
$this->modifyRelationshipQueryUsing = $modifyQueryUsing;
$this->afterStateHydrated(null);
$this->afterStateHydrated(function (Repeater $component) {
if (! is_array($component->hydratedDefaultState)) {
return;
}
$component->mergeHydratedDefaultStateWithChildComponentContainerState();
});
$this->loadStateFromRelationshipsUsing(static function (Repeater $component) {
$component->clearCachedExistingRecords();
@@ -761,6 +814,28 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return $this;
}
/**
* After hydrating the state of child component containers, the default state
* of fields inside the repeater can be lost, if it was defined on the repeater
* itself. This method merges the hydrated default state with the state of the
* child component containers, so that the default state of the fields inside
* the repeater is preserved.
*/
protected function mergeHydratedDefaultStateWithChildComponentContainerState(): void
{
$state = $this->getState();
$items = $this->hydratedDefaultState;
foreach ($items as $itemKey => $itemData) {
$items[$itemKey] = [
...$state[$itemKey] ?? [],
...$itemData,
];
}
$this->state($items);
}
public function itemLabel(string | Closure | null $label): static
{
$this->itemLabel = $label;
@@ -877,6 +952,23 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return $this->itemLabel !== null;
}
public function simple(Field | Closure | null $field): static
{
$this->simpleField = $field;
return $this;
}
public function isSimple(): bool
{
return $this->simpleField !== null;
}
public function getSimpleField(): ?Field
{
return $this->evaluate($this->simpleField)?->hiddenLabel();
}
public function clearCachedExistingRecords(): void
{
$this->cachedExistingRecords = null;
@@ -970,4 +1062,13 @@ class Repeater extends Field implements Contracts\CanConcealComponents
{
return $this->isCollapsible();
}
public function getView(): string
{
if ($this->isSimple()) {
return 'filament-forms::components.simple-repeater';
}
return 'filament-forms::components.repeater';
}
}
+28 -5
View File
@@ -869,6 +869,18 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
return;
}
if ($relationship instanceof \Znck\Eloquent\Relations\BelongsToThrough) {
$relatedModel = $relationship->getResults();
$component->state(
$relatedModel->getAttribute(
$relationship->getRelated()->getKeyName(),
),
);
return;
}
/** @var BelongsTo $relationship */
$relatedModel = $relationship->getResults();
@@ -918,10 +930,15 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relationshipQuery->where(
$relationship instanceof BelongsToMany ? $relationship->getRelatedKeyName() : $relationship->getOwnerKeyName(),
$state,
);
if ($relationship instanceof BelongsToMany) {
$relatedKeyName = $relationship->getRelatedKeyName();
} elseif ($relationship instanceof \Znck\Eloquent\Relations\BelongsToThrough) {
$relatedKeyName = $relationship->getRelated()->getQualifiedKeyName();
} else {
$relatedKeyName = $relationship->getOwnerKeyName();
}
$relationshipQuery->where($relatedKeyName, $state);
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
@@ -953,7 +970,13 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
->select($relationshipQuery->getModel()->getTable() . '.*');
}
$relatedKeyName = $relationship instanceof BelongsToMany ? $relationship->getQualifiedRelatedKeyName() : $relationship->getQualifiedOwnerKeyName();
if ($relationship instanceof BelongsToMany) {
$relatedKeyName = $relationship->getQualifiedRelatedKeyName();
} elseif ($relationship instanceof \Znck\Eloquent\Relations\BelongsToThrough) {
$relatedKeyName = $relationship->getRelated()->getQualifiedKeyName();
} else {
$relatedKeyName = $relationship->getQualifiedOwnerKeyName();
}
$relationshipQuery->whereIn($relatedKeyName, $values);
@@ -6,6 +6,7 @@ use Closure;
use Exception;
use Filament\Forms\Components\Component;
use Filament\Forms\Form;
use Filament\Infolists\Infolist;
use Filament\Support\Concerns\ResolvesDynamicLivewireProperties;
use Filament\Support\Contracts\TranslatableContentDriver;
use Illuminate\Database\Eloquent\Model;
@@ -440,4 +441,9 @@ trait InteractsWithForms
{
return $this->isCachingForms;
}
public function mountedFormComponentActionInfolist(): Infolist
{
return $this->getMountedFormComponentAction()->getInfolist();
}
}
@@ -5,7 +5,6 @@ namespace Filament\Forms;
use Filament\Forms\Testing\TestsForms;
use Filament\Support\Assets\AlpineComponent;
use Filament\Support\Assets\Css;
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
use Illuminate\Filesystem\Filesystem;
use Livewire\Features\SupportTesting\Testable;
@@ -36,7 +35,6 @@ class FormsServiceProvider extends PackageServiceProvider
AlpineComponent::make('tags-input', __DIR__ . '/../dist/components/tags-input.js'),
AlpineComponent::make('textarea', __DIR__ . '/../dist/components/textarea.js'),
Css::make('forms', __DIR__ . '/../dist/index.css'),
Js::make('forms', __DIR__ . '/../dist/index.js'),
], 'filament/forms');
if ($this->app->runningInConsole()) {
@@ -87,7 +87,7 @@ TextEntry::make('title')
## Setting a default value
To set a default value for fields with a `null` state, you may use the `default()` method:
To set a default value for entries with an empty state, you may use the `default()` method. This method will treat the default state as if it were real, so entries like [image](image) or [color](color) will display the default image or color.
```php
use Filament\Infolists\Components\TextEntry;
@@ -96,6 +96,19 @@ TextEntry::make('title')
->default('Untitled')
```
## Adding placeholder text if an entry is empty
Sometimes you may want to display placeholder text for entries with an empty state, which is styled as a lighter gray text. This differs from the [default value](#setting-a-default-value), as the placeholder is always text and not treated as if it were real state.
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->placeholder('Untitled')
```
<AutoScreenshot name="infolists/entries/placeholder" alt="Entry with a placeholder for empty state" version="3.x" />
## Adding helper text below the entry
Sometimes, you may wish to provide extra information for the user of the infolist. For this purpose, you may add helper text below the entry.
@@ -207,17 +207,6 @@ TextEntry::make('status')
->formatStateUsing(fn (string $state): string => __("statuses.{$state}"))
```
## Adding a placeholder if the entry is empty
Sometimes you may want to display a placeholder if the entry's value is empty:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('updated_at')
->placeholder('Never')
```
## Customizing the color
You may set a color for the text, either `danger`, `gray`, `info`, `primary`, `success` or `warning`:
@@ -0,0 +1,9 @@
<?php
return [
'text_entry' => [
'more_list_items' => 'và :count mục khác',
],
];
@@ -0,0 +1,9 @@
<?php
return [
'text_entry' => [
'more_list_items' => '还有 :count 条记录',
],
];
@@ -8,30 +8,36 @@
])
}}
>
@foreach (\Illuminate\Support\Arr::wrap($getState()) as $state)
@php
$itemIsCopyable = $isCopyable($state);
$copyableState = $getCopyableState($state) ?? $state;
$copyMessage = $getCopyMessage($state);
$copyMessageDuration = $getCopyMessageDuration($state);
@endphp
@if (count($arrayState = \Illuminate\Support\Arr::wrap($getState())))
@foreach ($arrayState as $state)
@php
$itemIsCopyable = $isCopyable($state);
$copyableState = $getCopyableState($state) ?? $state;
$copyMessage = $getCopyMessage($state);
$copyMessageDuration = $getCopyMessageDuration($state);
@endphp
<div
@if ($itemIsCopyable)
x-data="{}"
x-on:click="
window.navigator.clipboard.writeText(@js($copyableState))
$tooltip(@js($copyMessage), { timeout: @js($copyMessageDuration) })
"
@endif
@class([
'fi-in-color-item h-6 w-6 rounded-md',
'cursor-pointer' => $itemIsCopyable,
])
@style([
"background-color: {$state}" => $state,
])
></div>
@endforeach
<div
@if ($itemIsCopyable)
x-data="{}"
x-on:click="
window.navigator.clipboard.writeText(@js($copyableState))
$tooltip(@js($copyMessage), { timeout: @js($copyMessageDuration) })
"
@endif
@class([
'fi-in-color-item h-6 w-6 rounded-md',
'cursor-pointer' => $itemIsCopyable,
])
@style([
"background-color: {$state}" => $state,
])
></div>
@endforeach
@elseif (($placeholder = $getPlaceholder()) !== null)
<x-filament-infolists::entries.placeholder>
{{ $placeholder }}
</x-filament-infolists::entries.placeholder>
@endif
</div>
</x-dynamic-component>
@@ -0,0 +1,3 @@
<div class="fi-in-placeholder text-sm text-gray-400 dark:text-gray-500">
{{ $slot }}
</div>
@@ -117,10 +117,7 @@
>
@if ($url)
<a
href="{{ $url }}"
@if ($shouldOpenUrlInNewTab)
target="_blank"
@endif
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab) }}
class="block"
>
{{ $slot }}
@@ -12,39 +12,41 @@
])
}}
>
@foreach (\Illuminate\Support\Arr::wrap($getState()) as $state)
@if ($icon = $getIcon($state))
@php
$color = $getColor($state) ?? 'gray';
$size = $getSize($state) ?? IconEntrySize::Large;
@endphp
@if (count($arrayState = \Illuminate\Support\Arr::wrap($getState())))
@foreach ($arrayState as $state)
@if ($icon = $getIcon($state))
@php
$color = $getColor($state) ?? 'gray';
$size = $getSize($state) ?? IconEntrySize::Large;
@endphp
<x-filament::icon
:icon="$icon"
@class([
'fi-in-icon-item',
match ($size) {
IconEntrySize::ExtraSmall, 'xs' => 'fi-in-icon-item-size-xs h-3 w-3',
IconEntrySize::Small, 'sm' => 'fi-in-icon-item-size-sm h-4 w-4',
IconEntrySize::Medium, 'md' => 'fi-in-icon-item-size-md h-5 w-5',
IconEntrySize::Large, 'lg' => 'fi-in-icon-item-size-lg h-6 w-6',
IconEntrySize::ExtraLarge, 'xl' => 'fi-in-icon-item-size-xl h-7 w-7',
default => $size,
},
match ($color) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500 dark:text-custom-400',
},
])
@style([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 500],
alias: 'infolists::components.icon-entry.item',
) => $color !== 'gray',
])
/>
@endif
@endforeach
<x-filament::icon
:icon="$icon"
@class([
'fi-in-icon-item',
match ($size) {
IconEntrySize::ExtraSmall, 'xs' => 'fi-in-icon-item-size-xs h-3 w-3',
IconEntrySize::Small, 'sm' => 'fi-in-icon-item-size-sm h-4 w-4',
IconEntrySize::Medium, 'md' => 'fi-in-icon-item-size-md h-5 w-5',
IconEntrySize::Large, 'lg' => 'fi-in-icon-item-size-lg h-6 w-6',
IconEntrySize::ExtraLarge, 'xl' => 'fi-in-icon-item-size-xl h-7 w-7',
default => $size,
},
match ($color) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500 dark:text-custom-400',
},
])
@style([
\Filament\Support\get_color_css_variables($color, shades: [400, 500]) => $color !== 'gray',
])
/>
@endif
@endforeach
@elseif (($placeholder = $getPlaceholder()) !== null)
<x-filament-infolists::entries.placeholder>
{{ $placeholder }}
</x-filament-infolists::entries.placeholder>
@endif
</div>
</x-dynamic-component>
@@ -41,8 +41,16 @@
};
@endphp
@if (count($limitedState))
<div class="fi-in-image flex items-center gap-x-2.5">
<div
{{
$attributes
->merge($getExtraAttributes(), escape: false)
->class([
'fi-in-image flex items-center gap-x-2.5',
])
}}
>
@if (count($limitedState))
<div
@class([
'flex',
@@ -112,6 +120,10 @@
+{{ count($state) - count($limitedState) }}
</div>
@endif
</div>
@endif
@elseif (($placeholder = $getPlaceholder()) !== null)
<x-filament-infolists::entries.placeholder>
{{ $placeholder }}
</x-filament-infolists::entries.placeholder>
@endif
</div>
</x-dynamic-component>
@@ -3,7 +3,7 @@
@endphp
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
<ul
<div
{{
$attributes
->merge([
@@ -16,25 +16,33 @@
])
}}
>
<x-filament::grid
:default="$getGridColumns('default')"
:sm="$getGridColumns('sm')"
:md="$getGridColumns('md')"
:lg="$getGridColumns('lg')"
:xl="$getGridColumns('xl')"
:two-xl="$getGridColumns('2xl')"
class="gap-4"
>
@foreach ($getChildComponentContainers() as $container)
<li
@class([
'fi-in-repeatable-item block',
'rounded-xl bg-white p-4 shadow-sm ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10' => $isContained,
])
@if (count($childComponentContainers = $getChildComponentContainers()))
<ul>
<x-filament::grid
:default="$getGridColumns('default')"
:sm="$getGridColumns('sm')"
:md="$getGridColumns('md')"
:lg="$getGridColumns('lg')"
:xl="$getGridColumns('xl')"
:two-xl="$getGridColumns('2xl')"
class="gap-4"
>
{{ $container }}
</li>
@endforeach
</x-filament::grid>
</ul>
@foreach ($childComponentContainers as $container)
<li
@class([
'fi-in-repeatable-item block',
'rounded-xl bg-white p-4 shadow-sm ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10' => $isContained,
])
>
{{ $container }}
</li>
@endforeach
</x-filament::grid>
</ul>
@elseif (($placeholder = $getPlaceholder()) !== null)
<x-filament-infolists::entries.placeholder>
{{ $placeholder }}
</x-filament-infolists::entries.placeholder>
@endif
</div>
</x-dynamic-component>
@@ -40,159 +40,163 @@
$arrayState = \Illuminate\Support\Arr::wrap($arrayState);
@endphp
<x-filament-infolists::affixes
:prefix-actions="$getPrefixActions()"
:suffix-actions="$getSuffixActions()"
:attributes="
\Filament\Support\prepare_inherited_attributes($getExtraAttributeBag())
->class(['fi-in-text'])
"
<div
{{
$attributes
->merge($getExtraAttributes(), escape: false)
->class([
'fi-in-text',
])
}}
>
<{{ $isListWithLineBreaks ? 'ul' : 'div' }}
@class([
'list-inside list-disc' => $isBulleted(),
'flex flex-wrap items-center gap-1.5' => $isBadge,
])
>
@foreach ($arrayState as $state)
@if (filled($formattedState = $formatState($state)))
@php
$color = $getColor($state);
$copyableState = $getCopyableState($state) ?? $state;
$copyMessage = $getCopyMessage($state);
$copyMessageDuration = $getCopyMessageDuration($state);
$fontFamily = $getFontFamily($state);
$icon = $getIcon($state);
$itemIsCopyable = $isCopyable($state);
$size = $getSize($state);
$weight = $getWeight($state);
@if ($arrayState)
<x-filament-infolists::affixes
:prefix-actions="$getPrefixActions()"
:suffix-actions="$getSuffixActions()"
>
<{{ $isListWithLineBreaks ? 'ul' : 'div' }}
@class([
'list-inside list-disc' => $isBulleted(),
'flex flex-wrap items-center gap-1.5' => $isBadge,
])
>
@foreach ($arrayState as $state)
@if (filled($formattedState = $formatState($state)))
@php
$color = $getColor($state);
$copyableState = $getCopyableState($state) ?? $state;
$copyMessage = $getCopyMessage($state);
$copyMessageDuration = $getCopyMessageDuration($state);
$fontFamily = $getFontFamily($state);
$icon = $getIcon($state);
$itemIsCopyable = $isCopyable($state);
$size = $getSize($state);
$weight = $getWeight($state);
$proseClasses = \Illuminate\Support\Arr::toCssClasses([
'prose max-w-none dark:prose-invert [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
'pt-2' => ! $isLabelHidden(),
match ($size) {
TextEntrySize::ExtraSmall, 'xs' => 'prose-xs',
TextEntrySize::Small, 'sm', null => 'prose-sm',
TextEntrySize::Medium, 'base', 'md' => 'prose-base',
TextEntrySize::Large, 'lg' => 'prose-lg',
default => $size,
},
]);
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-in-text-item-icon h-5 w-5 shrink-0',
match ($color) {
'gray', null => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500',
},
]);
$iconStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [500],
alias: 'infolists::components.text-entry.item.icon',
) => $color !== 'gray',
]);
@endphp
<{{ $isListWithLineBreaks ? 'li' : 'div' }}
@if ($itemIsCopyable)
x-data="{}"
x-on:click="
window.navigator.clipboard.writeText(@js($copyableState))
$tooltip(@js($copyMessage), { timeout: @js($copyMessageDuration) })
"
class="cursor-pointer max-w-max"
@endif
>
@if ($isBadge)
<x-filament::badge
:color="$color"
:icon="$icon"
:icon-position="$iconPosition"
>
{{ $formattedState }}
</x-filament::badge>
@else
<div
@class([
'fi-in-text-item inline-flex items-center gap-1.5',
'transition duration-75 hover:underline focus:underline' => $url,
$proseClasses = \Illuminate\Support\Arr::toCssClasses([
'prose max-w-none dark:prose-invert [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
'pt-2' => ! $isLabelHidden(),
match ($size) {
TextEntrySize::ExtraSmall, 'xs' => 'text-xs',
TextEntrySize::Small, 'sm', null => 'text-sm leading-6',
TextEntrySize::Medium, 'base', 'md' => 'text-base',
TextEntrySize::Large, 'lg' => 'text-lg',
TextEntrySize::ExtraSmall, 'xs' => 'prose-xs',
TextEntrySize::Small, 'sm', null => 'prose-sm',
TextEntrySize::Medium, 'base', 'md' => 'prose-base',
TextEntrySize::Large, 'lg' => 'prose-lg',
default => $size,
},
]);
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-in-text-item-icon h-5 w-5 shrink-0',
match ($color) {
null => 'text-gray-950 dark:text-white',
'gray' => 'text-gray-500 dark:text-gray-400',
default => 'text-custom-600 dark:text-custom-400',
'gray', null => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500',
},
match ($weight) {
FontWeight::Thin, 'thin' => 'font-thin',
FontWeight::ExtraLight, 'extralight' => 'font-extralight',
FontWeight::Light, 'light' => 'font-light',
FontWeight::Medium, 'medium' => 'font-medium',
FontWeight::SemiBold, 'semibold' => 'font-semibold',
FontWeight::Bold, 'bold' => 'font-bold',
FontWeight::ExtraBold, 'extrabold' => 'font-extrabold',
FontWeight::Black, 'black' => 'font-black',
default => $weight,
},
match ($fontFamily) {
FontFamily::Sans, 'sans' => 'font-sans',
FontFamily::Serif, 'serif' => 'font-serif',
FontFamily::Mono, 'mono' => 'font-mono',
default => $fontFamily,
},
])
@style([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 600],
alias: 'infolists::components.text-entry.item',
) => ! in_array($color, [null, 'gray']),
])
]);
$iconStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables($color, shades: [500]) => $color !== 'gray',
]);
@endphp
<{{ $isListWithLineBreaks ? 'li' : 'div' }}
@if ($itemIsCopyable)
x-data="{}"
x-on:click="
window.navigator.clipboard.writeText(@js($copyableState))
$tooltip(@js($copyMessage), { timeout: @js($copyMessageDuration) })
"
class="cursor-pointer max-w-max"
@endif
>
@if ($icon && in_array($iconPosition, [IconPosition::Before, 'before']))
<x-filament::icon
@if ($isBadge)
<x-filament::badge
:color="$color"
:icon="$icon"
:class="$iconClasses"
:style="$iconStyles"
/>
@endif
:icon-position="$iconPosition"
>
{{ $formattedState }}
</x-filament::badge>
@else
<div
@class([
'fi-in-text-item inline-flex items-center gap-1.5',
'transition duration-75 hover:underline focus:underline' => $url,
match ($size) {
TextEntrySize::ExtraSmall, 'xs' => 'text-xs',
TextEntrySize::Small, 'sm', null => 'text-sm leading-6',
TextEntrySize::Medium, 'base', 'md' => 'text-base',
TextEntrySize::Large, 'lg' => 'text-lg',
default => $size,
},
match ($color) {
null => 'text-gray-950 dark:text-white',
'gray' => 'text-gray-500 dark:text-gray-400',
default => 'text-custom-600 dark:text-custom-400',
},
match ($weight) {
FontWeight::Thin, 'thin' => 'font-thin',
FontWeight::ExtraLight, 'extralight' => 'font-extralight',
FontWeight::Light, 'light' => 'font-light',
FontWeight::Medium, 'medium' => 'font-medium',
FontWeight::SemiBold, 'semibold' => 'font-semibold',
FontWeight::Bold, 'bold' => 'font-bold',
FontWeight::ExtraBold, 'extrabold' => 'font-extrabold',
FontWeight::Black, 'black' => 'font-black',
default => $weight,
},
match ($fontFamily) {
FontFamily::Sans, 'sans' => 'font-sans',
FontFamily::Serif, 'serif' => 'font-serif',
FontFamily::Mono, 'mono' => 'font-mono',
default => $fontFamily,
},
])
@style([
\Filament\Support\get_color_css_variables($color, shades: [400, 600]) => ! in_array($color, [null, 'gray']),
])
>
@if ($icon && in_array($iconPosition, [IconPosition::Before, 'before']))
<x-filament::icon
:icon="$icon"
:class="$iconClasses"
:style="$iconStyles"
/>
@endif
<div
@class([
$proseClasses => $isProse || $isMarkdown,
])
>
{{ $formattedState }}
</div>
<div
@class([
$proseClasses => $isProse || $isMarkdown,
])
>
{{ $formattedState }}
</div>
@if ($icon && in_array($iconPosition, [IconPosition::After, 'after']))
<x-filament::icon
:icon="$icon"
:class="$iconClasses"
:style="$iconStyles"
/>
@if ($icon && in_array($iconPosition, [IconPosition::After, 'after']))
<x-filament::icon
:icon="$icon"
:class="$iconClasses"
:style="$iconStyles"
/>
@endif
</div>
@endif
</div>
</{{ $isListWithLineBreaks ? 'li' : 'div' }}>
@endif
</{{ $isListWithLineBreaks ? 'li' : 'div' }}>
@endif
@endforeach
@endforeach
@if ($limitedArrayStateCount = count($limitedArrayState ?? []))
<{{ $isListWithLineBreaks ? 'li' : 'div' }}
class="text-sm text-gray-500 dark:text-gray-400"
>
{{ trans_choice('filament-infolists::components.text_entry.more_list_items', $limitedArrayStateCount) }}
</{{ $isListWithLineBreaks ? 'li' : 'div' }}>
@endif
</{{ $isListWithLineBreaks ? 'ul' : 'div' }}>
</x-filament-infolists::affixes>
@if ($limitedArrayStateCount = count($limitedArrayState ?? []))
<{{ $isListWithLineBreaks ? 'li' : 'div' }}
class="text-sm text-gray-500 dark:text-gray-400"
>
{{ trans_choice('filament-infolists::components.text_entry.more_list_items', $limitedArrayStateCount) }}
</{{ $isListWithLineBreaks ? 'li' : 'div' }}>
@endif
</{{ $isListWithLineBreaks ? 'ul' : 'div' }}>
</x-filament-infolists::affixes>
@elseif (($placeholder = $getPlaceholder()) !== null)
<x-filament-infolists::entries.placeholder>
{{ $placeholder }}
</x-filament-infolists::entries.placeholder>
@endif
</div>
</x-dynamic-component>
@@ -36,8 +36,6 @@ trait CanFormatState
protected bool | Closure $isMarkdown = false;
protected string | Closure | null $placeholder = null;
public function markdown(bool | Closure $condition = true): static
{
$this->isMarkdown = $condition;
@@ -143,13 +141,6 @@ trait CanFormatState
return $this;
}
public function placeholder(string | Closure | null $placeholder): static
{
$this->placeholder = $placeholder;
return $this;
}
public function limit(int | Closure | null $length = 100, string | Closure | null $end = '...'): static
{
$this->characterLimit = $length;
@@ -224,10 +215,6 @@ trait CanFormatState
return $state;
}
if (blank($state)) {
$state = $this->evaluate($this->placeholder);
}
if ($this->isHtml()) {
return str($state)
->when($this->isMarkdown(), fn (Stringable $stringable) => $stringable->markdown())
@@ -79,8 +79,8 @@ trait HasState
$state;
}
if ($state === null) {
$state = value($this->getDefaultState());
if (blank($state)) {
$state = $this->getDefaultState();
}
return $state;
@@ -4,6 +4,7 @@ namespace Filament\Infolists\Components;
use Filament\Infolists\Components\Contracts\HasHintActions;
use Filament\Support\Concerns\HasAlignment;
use Filament\Support\Concerns\HasPlaceholder;
use Illuminate\Contracts\Support\Htmlable;
class Entry extends Component implements HasHintActions
@@ -14,6 +15,7 @@ class Entry extends Component implements HasHintActions
use Concerns\HasName;
use Concerns\HasTooltip;
use HasAlignment;
use HasPlaceholder;
protected string $viewIdentifier = 'entry';
@@ -2,13 +2,17 @@
namespace Filament\Infolists\Concerns;
use Exception;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Infolists\Components\Actions\Action;
use Filament\Infolists\Components\Component;
use Filament\Infolists\Infolist;
use Filament\Support\Exceptions\Cancel;
use Filament\Support\Exceptions\Halt;
use Filament\Tables\Contracts\HasTable;
use function Livewire\store;
@@ -49,6 +53,10 @@ trait InteractsWithInfolists
$infolist = $this->{$name}($this->makeInfolist());
if (! ($infolist instanceof Infolist)) {
return null;
}
return $this->cacheInfolist($name, $infolist);
}
@@ -186,11 +194,25 @@ trait InteractsWithInfolists
$this->resetErrorBag();
$this->dispatch('open-modal', id: "{$this->getId()}-infolist-action");
$this->openInfolistActionModal();
return null;
}
protected function openInfolistActionModal(): void
{
if (
($this instanceof HasActions && count($this->mountedActions)) ||
($this instanceof HasForms && count($this->mountedFormComponentActions)) ||
/** @phpstan-ignore-next-line */
($this instanceof HasTable && (count($this->mountedTableActions) || filled($this->mountedTableBulkAction)))
) {
throw new Exception('Currently, infolist actions cannot open modals while they are nested within other action modals.');
}
$this->dispatch('open-modal', id: "{$this->getId()}-infolist-action");
}
public function mountedInfolistActionShouldOpenModal(): bool
{
$action = $this->getMountedInfolistAction();
@@ -293,7 +315,7 @@ trait InteractsWithInfolists
$this->resetErrorBag();
$this->dispatch('open-modal', id: "{$this->getId()}-infolist-action");
$this->openInfolistActionModal();
}
protected function makeInfolist(): Infolist
@@ -13,14 +13,14 @@ return [
],
'mark_all_as_read' => [
'label' => 'Đánh dấu tất cả đã đọc',
'label' => 'Đánh dấu tất cả là đã đọc',
],
],
'empty' => [
'heading' => 'Không có thông báo nào ở đây',
'description' => 'Vui lòng kiểm tra lại sau',
'heading' => 'Không có thông báo',
'description' => 'Vui lòng kiểm tra lại sau.',
],
],
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => '通知',
'actions' => [
'clear' => [
'label' => '清除',
],
'mark_all_as_read' => [
'label' => '标记为已读',
],
],
'empty' => [
'heading' => '没有通知',
'description' => '请稍后再查看。',
],
],
];
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -76,6 +76,28 @@ Tab::make()
As in the example above, this could be quite useful for showing the number of records that pass that filter.
### Customizing the default tab
To customize the default tab that is selected when the page is loaded, you can return the array key of the tab from the `getDefaultActiveTab()` method:
```php
use Filament\Resources\Pages\ListRecords\Tab;
public function getTabs(): array
{
return [
'all' => Tab::make(),
'active' => Tab::make(),
'inactive' => Tab::make(),
];
}
public function getDefaultActiveTab(): string | int | null
{
return 'active';
}
```
## Authorization
For authorization, Filament will observe any [model policies](https://laravel.com/docs/authorization#creating-policies) that are registered in your app.
@@ -103,6 +103,30 @@ public static function getRelations(): array
Once a table and form have been defined for the relation manager, visit the [Edit](editing-records) or [View](viewing-records) page of your resource to see it in action.
### Read-only mode
Relation managers are usually displayed on either the Edit or View page of a resource. On the View page, Filament will automatically hide all actions that modify the relationship, such as create, edit, and delete. We call this "read-only mode", and it is there by default to preserve the read-only behaviour of the View page. However, you can disable this behaviour, by overriding the `isReadOnly()` method on the relation manager class to return `false` all the time:
```php
public function isReadOnly(): bool
{
return false;
}
```
Alternatively, if you hate this functionality, you can disable it for all relation managers at once in the panel [configuration](../configuration):
```php
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->readOnlyRelationManagersOnResourceViewPagesByDefault(false);
}
```
### Unconventional inverse relationship names
For inverse relationships that do not follow Laravel's naming guidelines, you may wish to use the `inverseRelationship()` method on the table:
@@ -720,17 +744,6 @@ AttachAction::make()
->recordSelectSearchColumns(['title', 'id'])
```
## Read-only mode
Relation managers are usually displayed on either the Edit or View page of a resource. On the View page, Filament will automatically hide all actions that modify the relationship, such as create, edit, and delete. However, you can disable this behaviour, by overriding the `isReadOnly()` method on the relation manager class to return `false` all the time:
```php
public function isReadOnly(): bool
{
return false;
}
```
## Passing properties to relation managers
When registering a relation manager in a resource, you can use the `make()` method to pass an array of [Livewire properties](https://livewire.laravel.com/docs/properties) to it:
+5 -4
View File
@@ -68,11 +68,12 @@ protected function getHeaderActions(): array
If you're using actions on an [Edit](resources/editing-records) or [View](resources/viewing-records) resource page, you can refresh data within the main form using the `refreshFormData()` method:
```php
use App\Models\Post;
use Filament\Actions\Action;
Action::make('approve')
->action(function () {
$this->getRecord()->approve();
->action(function (Post $record) {
$record->approve();
$this->refreshFormData([
'status',
@@ -110,7 +111,7 @@ You may change how many grid columns are used to display widgets.
You may override the `getHeaderWidgetsColumns()` or `getFooterWidgetsColumns()` methods to return a number of grid columns to use:
```php
protected function getHeaderWidgetsColumns(): int | array
public function getHeaderWidgetsColumns(): int | array
{
return 3;
}
@@ -121,7 +122,7 @@ protected function getHeaderWidgetsColumns(): int | array
You may wish to change the number of widget grid columns based on the responsive [breakpoint](https://tailwindcss.com/docs/responsive-design#overview) of the browser. You can do this using an array that contains the number of columns that should be used at each breakpoint:
```php
protected function getHeaderWidgetsColumns(): int | array
public function getHeaderWidgetsColumns(): int | array
{
return [
'md' => 4,
+1 -3
View File
@@ -133,9 +133,7 @@ If you want to customize the dashboard class, for example, to [change the number
namespace App\Filament\Pages;
use Filament\Pages\Dashboard as BasePage;
class Dashboard extends BasePage
class Dashboard extends \Filament\Pages\Dashboard
{
// ...
}
+17
View File
@@ -112,3 +112,20 @@ public function panel(Panel $panel): Panel
->maxContentWidth('full');
}
```
## SPA mode
> Warning: This feature is experimental, and you may encounter bugs while using it. Please report any issues you find to [Livewire](https://github.com/livewire/livewire) with a pull request containing a failing test.
SPA mode utilizes [Livewire's `wire:navigate` feature](https://livewire.laravel.com/docs/navigate) to make your server-rendered panel feel like a single-page-application, with less delay between page loads and a loading bar for longer requests. To enable SPA mode on a panel, you can use the `spa()` method:
```php
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->spa();
}
```

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