mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
Merge branch '3.x' into shades-registration
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 |
@@ -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',
|
||||
|
||||
Generated
-46
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
@@ -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,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() }}
|
||||
|
||||
@@ -84,4 +84,9 @@ class Action extends MountableAction implements Contracts\Groupable, Contracts\H
|
||||
|
||||
$this->record(null);
|
||||
}
|
||||
|
||||
public function getInfolistName(): string
|
||||
{
|
||||
return 'mountedActionInfolist';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
Vendored
+1
-1
@@ -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)});})();
|
||||
(()=>{})();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' => '请稍后再查看。',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
Vendored
+1
-1
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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user