Merge branch '3.x' into 3.x

This commit is contained in:
Tony Lea
2024-02-15 17:25:21 -05:00
committed by GitHub
138 changed files with 4082 additions and 1166 deletions
-3
View File
@@ -1,4 +1 @@
@danharrin
*.blade.php @zepfietje
*.css @zepfietje
+1 -1
View File
@@ -89,7 +89,7 @@ body:
id: reproduction
attributes:
label: Reproduction repository
description: The URL of a public GitHub repository which reproduces the problem. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn\'t matter if your real project is private / confidential, since we want a link to a separate, isolated reproduction. This allows us to fix the problem much quicker. **This issue will be automatically closed and not reviewed if this is missing. Please make sure to format the URL starting with `https://github.com` - only repositories hosted on GitHub are accepted.** [Need a headstart? We have a template Filament project for you.](https://filament-issue.unitedbycode.com/)
description: The URL of a public GitHub repository which reproduces the problem. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn\'t matter if your real project is private / confidential, since we want a link to a separate, isolated reproduction. This allows us to fix the problem much quicker. **This issue will be automatically closed and not reviewed if this is missing. Please make sure to format the URL starting with `https://github.com` - only repositories hosted on GitHub are accepted.** [Need a headstart? We have a template Filament project for you.](https://filament-issue.unitedbycode.com)
validations:
required: true
- type: textarea
+4 -7
View File
@@ -55,8 +55,8 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting Ryan Scherler at ryan@eastslope.studio. All
complaints will be reviewed and investigated and will result in a response that
reported by contacting Dan Harrin at [dan@danharrin.com](mailto:dan@danharrin.com).
All complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
@@ -67,10 +67,7 @@ members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html), version 1.4.
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
[the FAQs](https://www.contributor-covenant.org/faq).
+1 -45
View File
@@ -75,51 +75,7 @@ composer require filament/widgets
## Contributing
If you want to contribute to the Filament packages, then you may want to test it in a real Laravel project:
- Fork this repository to your GitHub account.
- Create a Laravel app locally.
- Clone your fork in your Laravel app's root directory.
- In the `/filament` directory, create a branch for your fix, e.g. `fix/error-message`.
Install the packages in your app's `composer.json`:
```jsonc
{
// ...
"require": {
"filament/filament": "*",
},
"minimum-stability": "dev",
"repositories": [
{
"type": "path",
"url": "filament/packages/*"
}
],
// ...
}
```
Now, run `composer update`.
### Checking for missing translations
Set up a Laravel app, and install the [admin panel](https://filamentphp.com/docs/admin/installation).
Now, if you want to check for missing Spanish translations, run:
```bash
php artisan filament:check-translations es
```
This will let you know which translations are missing for this locale. You can make a PR with the changes to this repository.
If you've published the translations into your app and you'd like to check those instead, try:
```bash
php artisan filament:check-translations es --source=app
```
Please see our [contributing guide](https://filamentphp.com/docs/support/contributing).
## Need Help?
+2 -1
View File
@@ -26,7 +26,6 @@ $app = (new Application($basePathLocator::applicationBasePath()))
->createApplication();
$app->register(LivewireServiceProvider::class);
$app->register(AdminPanelProvider::class);
$app->register(FilamentServiceProvider::class);
$app->register(ActionsServiceProvider::class);
$app->register(FormsServiceProvider::class);
@@ -37,4 +36,6 @@ $app->register(SupportServiceProvider::class);
$app->register(TablesServiceProvider::class);
$app->register(WidgetsServiceProvider::class);
$app->register(AdminPanelProvider::class);
return $app;
Generated
+302 -337
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+159 -133
View File
@@ -274,9 +274,9 @@
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.9.tgz",
"integrity": "sha512-B2L9neXTIyPQoXDm+NtovPvG6VOLWnaXu3BIeVDWwdKFgG30oNa6CqVGiJPDWQwIAK49t9gnQI9c6K6RzabiKw==",
"version": "7.23.10",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz",
"integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
@@ -2743,9 +2743,9 @@
}
},
"node_modules/@types/express/node_modules/@types/express-serve-static-core": {
"version": "4.17.42",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.42.tgz",
"integrity": "sha512-ckM3jm2bf/MfB3+spLPWYPUH573plBFwpOhqQ2WottxYV85j1HQFlxmnTq57X1yHY9awZPig06hL/cLMgNWHIQ==",
"version": "4.17.43",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
"integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
"dev": true,
"dependencies": {
"@types/node": "*",
@@ -2850,9 +2850,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.11.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.7.tgz",
"integrity": "sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==",
"version": "20.11.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz",
"integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -3344,13 +3344,16 @@
"dev": true
},
"node_modules/array-buffer-byte-length": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
"integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
"integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"is-array-buffer": "^3.0.1"
"call-bind": "^1.0.5",
"is-array-buffer": "^3.0.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3372,17 +3375,18 @@
}
},
"node_modules/arraybuffer.prototype.slice": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
"integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
"integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
"dev": true,
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"es-abstract": "^1.22.1",
"get-intrinsic": "^1.2.1",
"is-array-buffer": "^3.0.2",
"array-buffer-byte-length": "^1.0.1",
"call-bind": "^1.0.5",
"define-properties": "^1.2.1",
"es-abstract": "^1.22.3",
"es-errors": "^1.2.1",
"get-intrinsic": "^1.2.3",
"is-array-buffer": "^3.0.4",
"is-shared-array-buffer": "^1.0.2"
},
"engines": {
@@ -3479,9 +3483,9 @@
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz",
"integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==",
"dev": true,
"engines": {
"node": ">= 0.4"
@@ -3817,9 +3821,9 @@
}
},
"node_modules/browserslist": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
"integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
"integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
"dev": true,
"funding": [
{
@@ -3836,8 +3840,8 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001565",
"electron-to-chromium": "^1.4.601",
"caniuse-lite": "^1.0.30001580",
"electron-to-chromium": "^1.4.648",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
@@ -3887,14 +3891,18 @@
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz",
"integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
"get-intrinsic": "^1.2.3",
"set-function-length": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3941,9 +3949,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001580",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz",
"integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==",
"version": "1.0.30001585",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001585.tgz",
"integrity": "sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==",
"dev": true,
"funding": [
{
@@ -4013,16 +4021,10 @@
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -4035,6 +4037,9 @@
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
@@ -4725,14 +4730,15 @@
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz",
"integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.1",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.2",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
"has-property-descriptors": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -4998,9 +5004,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.4.647",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.647.tgz",
"integrity": "sha512-Z/fTNGwc45WrYQhPaEcz5tAJuZZ8G7S/DBnhS6Kgp4BxnS40Z/HqlJ0hHg3Z79IGVzuVartIlTcjw/cQbPLgOw==",
"version": "1.4.659",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.659.tgz",
"integrity": "sha512-sRJ3nV3HowrYpBtPF9bASQV7OW49IgZC01Xiq43WfSE3RTCkK0/JidoCmR73Hyc1mN+l/H4Yqx0eNiomvExFZg==",
"dev": true
},
"node_modules/elliptic": {
@@ -5071,9 +5077,9 @@
}
},
"node_modules/envinfo": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz",
"integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==",
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz",
"integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==",
"dev": true,
"bin": {
"envinfo": "dist/cli.js"
@@ -5144,6 +5150,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
@@ -5232,9 +5247,9 @@
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"dev": true,
"engines": {
"node": ">=6"
@@ -5533,9 +5548,9 @@
}
},
"node_modules/fastq": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
"integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
@@ -6022,16 +6037,20 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -6049,13 +6068,13 @@
}
},
"node_modules/get-symbol-description": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
"integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.1.tgz",
"integrity": "sha512-KmuibvwbWaM4BHcBRYwJfZ1JxyJeBwB8ct9YYu67SvYdbEIlcQ2e56dHxfbobqW38GXo8/zDFqJeGtHiVbWyQw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.1"
"call-bind": "^1.0.5",
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
@@ -6230,12 +6249,12 @@
}
},
"node_modules/has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.2"
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -6597,9 +6616,9 @@
]
},
"node_modules/ignore": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
"integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true,
"engines": {
"node": ">= 4"
@@ -6716,12 +6735,12 @@
"dev": true
},
"node_modules/internal-slot": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
"integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
"integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.2",
"es-errors": "^1.3.0",
"hasown": "^2.0.0",
"side-channel": "^1.0.4"
},
@@ -6748,14 +6767,16 @@
}
},
"node_modules/is-array-buffer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
"integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
"integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.2.0",
"is-typed-array": "^1.1.10"
"get-intrinsic": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -7028,12 +7049,12 @@
}
},
"node_modules/is-typed-array": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
"integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
"dev": true,
"dependencies": {
"which-typed-array": "^1.1.11"
"which-typed-array": "^1.1.14"
},
"engines": {
"node": ">= 0.4"
@@ -8513,9 +8534,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"version": "8.4.34",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.34.tgz",
"integrity": "sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==",
"dev": true,
"funding": [
{
@@ -9204,9 +9225,9 @@
"dev": true
},
"node_modules/prettier": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
@@ -9880,13 +9901,13 @@
]
},
"node_modules/safe-regex-test": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
"integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
"integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.5",
"get-intrinsic": "^1.2.2",
"call-bind": "^1.0.6",
"es-errors": "^1.3.0",
"is-regex": "^1.1.4"
},
"engines": {
@@ -9940,9 +9961,9 @@
}
},
"node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -10120,14 +10141,15 @@
}
},
"node_modules/set-function-length": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
"integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
"integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
"dev": true,
"dependencies": {
"define-data-property": "^1.1.1",
"define-data-property": "^1.1.2",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.2",
"get-intrinsic": "^1.2.3",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.1"
},
@@ -10223,14 +10245,18 @@
"dev": true
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
"integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
"call-bind": "^1.0.6",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -11070,14 +11096,14 @@
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
"integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz",
"integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.2.1",
"is-typed-array": "^1.1.10"
"call-bind": "^1.0.6",
"es-errors": "^1.3.0",
"is-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
@@ -11413,9 +11439,9 @@
}
},
"node_modules/webpack": {
"version": "5.90.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.0.tgz",
"integrity": "sha512-bdmyXRCXeeNIePv6R6tGPyy20aUobw4Zy8r0LUS2EWO+U+Ke/gYDgsCh7bl5rB6jPpr4r0SZa6dPxBxLooDT3w==",
"version": "5.90.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz",
"integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -11892,16 +11918,16 @@
}
},
"node_modules/which-typed-array": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
"integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz",
"integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==",
"dev": true,
"dependencies": {
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.4",
"available-typed-arrays": "^1.0.6",
"call-bind": "^1.0.5",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"has-tostringtag": "^1.0.0"
"has-tostringtag": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
+1 -1
View File
@@ -17,7 +17,7 @@
"illuminate/contracts": "^10.0",
"illuminate/database": "^10.0",
"illuminate/support": "^10.0",
"league/csv": "9.11.0",
"league/csv": "^9.14",
"openspout/openspout": "^4.23",
"spatie/laravel-package-tools": "^1.9"
},
@@ -446,6 +446,15 @@ ImportColumn::make('sku')
->example('ABC123')
```
By default, the name of the column is used in the header of the example CSV. You can customize the header per-column using `exampleHeader()`:
```php
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->exampleHeader('SKU')
```
## Using a custom user model
By default, the `imports` table has a `user_id` column. That column is constrained to the `users` table:
@@ -603,6 +612,17 @@ public function getJobTags(): array
If you'd like to customize the tags that are applied to jobs of a certain importer, you may override this method in your importer class.
### Customizing the import job batch name
By default, the import system doesn't define any name for the job batches. If you'd like to customize the name that is applied to job batches of a certain importer, you may override the `getJobBatchName()` method in your importer class:
```php
public function getJobBatchName(): ?string
{
return 'product-import';
}
```
## Customizing import validation messages
The import system will automatically validate the CSV file before it is imported. If there are any errors, the user will be shown a list of them, and the import will not be processed. If you'd like to override any default validation messages, you may do so by overriding the `getValidationMessages()` method on your importer class:
@@ -283,7 +283,6 @@ By default, the export action will allow the user to choose between both CSV and
```php
use App\Filament\Exports\ProductExporter;
use Filament\Actions\ExportAction;
use Filament\Actions\Exports\Enums\ExportFormat;
ExportAction::make()
@@ -315,6 +314,19 @@ public function getFormats(): array
}
```
## Modifying the export query
By default, if you are using the `ExportAction` with a table, the action will use the table's currently filtered and sorted query to export the data. If you don't have a table, it will use the model's default query. To modify the query builder before exporting, you can use the `modifyQueryUsing()` method on the action:
```php
use App\Filament\Exports\ProductExporter;
use Illuminate\Database\Eloquent\Builder;
ExportAction::make()
->exporter(ProductExporter::class)
->modifyQueryUsing(fn (Builder $query) => $query->where('is_active', true))
```
## Configuring the export filesystem
### Customizing the storage disk
@@ -324,8 +336,6 @@ By default, exported files will be uploaded to the storage disk defined in the [
If you want to use a different disk for a specific export, you can pass the disk name to the `disk()` method on the action:
```php
use Filament\Actions\ExportAction;
ExportAction::make()
->exporter(ProductExporter::class)
->fileDisk('s3')
@@ -345,7 +355,6 @@ public function getFileDisk(): string
By default, exported files will have a name generated based on the ID and type of the export. You can also use the `fileName()` method on the action to customize the file name:
```php
use Filament\Actions\ExportAction;
use Filament\Actions\Exports\Models\Export;
ExportAction::make()
@@ -385,8 +394,6 @@ public static function getOptionsFormComponents(): array
Alternatively, you can pass a set of static options to the exporter through the `options()` method on the action:
```php
use Filament\Actions\ExportAction;
ExportAction::make()
->exporter(ProductExporter::class)
->options([
@@ -572,6 +579,17 @@ public function getJobTags(): array
If you'd like to customize the tags that are applied to jobs of a certain exporter, you may override this method in your exporter class.
### Customizing the export job batch name
By default, the export system doesn't define any name for the job batches. If you'd like to customize the name that is applied to job batches of a certain exporter, you may override the `getJobBatchName()` method in your exporter class:
```php
public function getJobBatchName(): ?string
{
return 'product-export';
}
```
## Authorization
By default, only the user who started the export may download files that get generated. If you'd like to customize the authorization logic, you may create an `ExportPolicy` class, and [register it in your `AuthServiceProvider`](https://laravel.com/docs/10.x/authorization#registering-policies):
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Exportovat :label',
'modal' => [
'heading' => 'Exportovat :label',
'form' => [
'columns' => [
'label' => 'Sloupce',
'form' => [
'is_enabled' => [
'label' => ':column povoleno',
],
'label' => [
'label' => ':column popisek',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Exportovat',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Export dokončen',
'actions' => [
'download_csv' => [
'label' => 'Stáhnout .csv',
],
'download_xlsx' => [
'label' => 'Stáhnout .xlsx',
],
],
],
'max_rows' => [
'title' => 'Export překračuje povolenou velikost',
'body' => 'Není možné exportovat více než 1 řádek najednou.|Není možné exportovat více než :count řádků najednou.',
],
'started' => [
'title' => 'Export byl zahájen',
'body' => 'Export byl zahájen a 1 řádek bude zpracován na pozadí.|Váš export byl zahájen a :count řádků bude zpracováno na pozadí.',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Exportiere :label',
'modal' => [
'heading' => 'Exportiere :label',
'form' => [
'columns' => [
'label' => 'Spalten',
'form' => [
'is_enabled' => [
'label' => ':column ist aktiviert',
],
'label' => [
'label' => ':column Beschriftung',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Export',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Export abgeschlossen',
'actions' => [
'download_csv' => [
'label' => 'Lade .csv herunter',
],
'download_xlsx' => [
'label' => 'Lade .xlsx herunter',
],
],
],
'max_rows' => [
'title' => 'Der Export ist zu groß',
'body' => 'Nicht mehr als 1 Zeile auf einmal exportieren.|Nicht mehr als :count Zeilen auf einmal exportieren.',
],
'started' => [
'title' => 'Export gestartet',
'body' => 'Der Export wurde gestartet und 1 Zeile wird im Hintergrund verarbeitet.| Der Export wurde gestartet und :count Zeilen werden im Hintergrund verarbeitet.',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => ':label Import',
'modal' => [
'heading' => ':label Import',
'form' => [
'file' => [
'label' => 'Datei',
'placeholder' => 'CSV Datei hochladen',
],
'columns' => [
'label' => 'Spalten',
'placeholder' => 'Spalte auswählen',
],
],
'actions' => [
'download_example' => [
'label' => 'Lade Beispiel CSV herunter',
],
'import' => [
'label' => 'Importieren',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Import abgeschlossen',
'actions' => [
'download_failed_rows_csv' => [
'label' => 'Informationen über die fehlgeschlagene Zeile herunterladen|Informationen über die fehlgeschlagenen Zeilen herunterladen',
],
],
],
'max_rows' => [
'title' => 'CSV Datei ist zu groß',
'body' => 'Nicht mehr als 1 Zeile auf einmal importieren.|Nicht mehr als :count Zeilen auf einmal importieren.',
],
'started' => [
'title' => 'Import gestartet',
'body' => 'Der Import wurde gestartet und 1 Zeile wird im Hintergrund verarbeitet.|Der Import wurde gestartet und :count Zeilen werden im Hintergrund verarbeitet.',
],
],
'example_csv' => [
'file_name' => ':importer-example',
],
'failure_csv' => [
'file_name' => 'import-:import_id-:csv_name-failed-rows',
'error_header' => 'Fehler',
'system_error' => 'Systemfehler, bitte Supportanfrage stellen.',
],
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Eksport :label',
'modal' => [
'heading' => 'Eksport :label',
'form' => [
'columns' => [
'label' => 'Kolum',
'form' => [
'is_enabled' => [
'label' => ':column diaktifkan',
],
'label' => [
'label' => ':column label',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Eksport',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Eksport selesai',
'actions' => [
'download_csv' => [
'label' => 'Muat turun .csv',
],
'download_xlsx' => [
'label' => 'Muat turun .xlsx',
],
],
],
'max_rows' => [
'title' => 'Eksport terlalu besar',
'body' => 'Anda tidak boleh mengeluarkan lebih dari 1 baris pada satu masa.|Anda tidak boleh mengeluarkan lebih dari :count baris pada satu masa.',
],
'started' => [
'title' => 'Eksport bermula',
'body' => 'Eksport anda telah bermula dan 1 baris akan diproses di belakang tabir.|Eksport anda telah bermula dan :count baris akan diproses di belakang tabir.',
],
],
'file_name' => 'eksport-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Eksportuj :label',
'modal' => [
'heading' => 'Eksportuj :label',
'form' => [
'columns' => [
'label' => 'Kolumny',
'form' => [
'is_enabled' => [
'label' => ':column włączony',
],
'label' => [
'label' => 'Etykieta :column',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Eksportuj',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Eksport zakończony',
'actions' => [
'download_csv' => [
'label' => 'Pobierz .csv',
],
'download_xlsx' => [
'label' => 'Pobierz .xlsx',
],
],
],
'max_rows' => [
'title' => 'Zbyt dużo wierszy',
'body' => 'Nie możesz wyeksportować więcej niż 1 wiersz na raz.|Nie możesz wyeksportować więcej niż :count wierszy na raz.',
],
'started' => [
'title' => 'Eksport rozpoczęty',
'body' => 'Rozpoczęto eksport i 1 wiersz zostanie przetworzony w tle.|Rozpoczęto eksport i :count wierszy zostanie przetworzonych w tle.',
],
],
'file_name' => 'eksport-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Importuj :label',
'modal' => [
'heading' => 'Importuj :label',
'form' => [
'file' => [
'label' => 'Plik',
'placeholder' => 'Wybierz plik CSV',
],
'columns' => [
'label' => 'Kolumny',
'placeholder' => 'Wybierz kolumnę',
],
],
'actions' => [
'download_example' => [
'label' => 'Pobierz przykładowy plik CSV',
],
'import' => [
'label' => 'Importuj',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Import zakończony',
'actions' => [
'download_failed_rows_csv' => [
'label' => 'Pobierz informacje o niepowodzeniach',
],
],
],
'max_rows' => [
'title' => 'Przesłany plik CSV jest zbyt duży',
'body' => 'Nie możesz zaimportować więcej niż 1 wiersz na raz.|Nie możesz zaimportować więcej niż :count wierszy na raz.',
],
'started' => [
'title' => 'Import rozpoczęty',
'body' => 'Import rozpoczęty i 1 wiersz zostanie przetworzony w tle.|Import rozpoczęty i :count wierszy zostanie przetworzonych w tle.',
],
],
'example_csv' => [
'file_name' => ':importer-example',
],
'failure_csv' => [
'file_name' => 'import-:import_id-:csv_name-failed-rows',
'error_header' => 'błąd',
'system_error' => 'Błąd systemu, skontaktuj się z pomocą techniczną.',
],
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Exportă :label',
'modal' => [
'heading' => 'Exportă :label',
'form' => [
'columns' => [
'label' => 'Coloane',
'form' => [
'is_enabled' => [
'label' => ':column activat',
],
'label' => [
'label' => 'Eticheta :column',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Exportă',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Export complet',
'actions' => [
'download_csv' => [
'label' => 'Descarcă .csv',
],
'download_xlsx' => [
'label' => 'Descarcă .xlsx',
],
],
],
'max_rows' => [
'title' => 'Exportul este prea mare',
'body' => 'Nu puteți exporta mai mult de 1 rând odată.|Nu puteți exporta mai mult de :count rânduri odată',
],
'started' => [
'title' => 'Exportul a început',
'body' => 'Exportul dvs. a început și 1 rând va fi procesat în fundal.|Exportul dvs. a început și :count rânduri vor fi procesate în fundal.',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -17,7 +17,7 @@ return [
],
'create_another' => [
'label' => 'Oluştur ve başka bir tane oluştur',
'label' => 'Oluştur & yeni oluştur',
],
],
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Dışa Aktar :label',
'modal' => [
'heading' => 'Dışa Aktar :label',
'form' => [
'columns' => [
'label' => 'Sütunlar',
'form' => [
'is_enabled' => [
'label' => ':column etkin',
],
'label' => [
'label' => ':column etiketi',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Dışa Aktar',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Dışa Aktarım Tamamlandı',
'actions' => [
'download_csv' => [
'label' => '.csv Olarak İndir',
],
'download_xlsx' => [
'label' => '.xlsx Olarak İndir',
],
],
],
'max_rows' => [
'title' => 'Maksimum Satır Sayısı Aşıldı',
'body' => 'Birden fazla satırı dışa aktaramazsınız.|:count satırı dışa aktaramazsınız.',
],
'started' => [
'title' => 'Dışa Aktarım Başladı',
'body' => 'Dışa aktarım başladı ve 1 satır arka planda işlenecek.|Dışa aktarım başladı ve :count satır arka planda işlenecek.',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'İçe Aktar: :label',
'modal' => [
'heading' => 'İçe Aktar: :label',
'form' => [
'file' => [
'label' => 'Dosya',
'placeholder' => 'Bir CSV dosyası seçin',
],
'columns' => [
'label' => 'Kolonlar',
'placeholder' => 'Kolonları eşleştirin',
],
],
'actions' => [
'download_example' => [
'label' => 'Örnek CSV Dosyasını İndir',
],
'import' => [
'label' => 'İçe Aktar',
],
],
],
'notifications' => [
'completed' => [
'title' => 'İçe Aktarım Tamamlandı',
'actions' => [
'download_failed_rows_csv' => [
'label' => 'Başarısız satır hakkında bilgileri indir|Başarısız satırlar hakkında bilgileri indir',
],
],
],
'max_rows' => [
'title' => 'Yüklenen Dosya Çok Büyük',
'body' => 'Aynı anda 1\'den fazla satır içeren dosyaları içe aktaramazsınız.|Aynı anda :count\'den fazla satır içeren dosyaları içe aktaramazsınız.',
],
'started' => [
'title' => 'İçe Aktarım Başladı',
'body' => 'İçe aktarım başladı ve 1 satır arka planda işlenecek.|İçe aktarım başladı ve :count satır arka planda işlenecek.',
],
],
'example_csv' => [
'file_name' => ':importer-example',
],
'failure_csv' => [
'file_name' => 'import-:import_id-:csv_name-failed-rows',
'error_header' => 'hata',
'system_error' => 'Sistem Hatası',
],
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => 'Xuất :label',
'modal' => [
'heading' => 'Xuất :label',
'form' => [
'columns' => [
'label' => 'Cột',
'form' => [
'is_enabled' => [
'label' => ':column đã kích hoạt',
],
'label' => [
'label' => ':column nhãn',
],
],
],
],
'actions' => [
'export' => [
'label' => 'Xuất',
],
],
],
'notifications' => [
'completed' => [
'title' => 'Xuất đã hoàn thành',
'actions' => [
'download_csv' => [
'label' => 'Tải xuống .csv',
],
'download_xlsx' => [
'label' => 'Tải xuống .xlsx',
],
],
],
'max_rows' => [
'title' => 'Xuất quá lớn',
'body' => 'Bạn không thể xuất nhiều hơn 1 hàng cùng lúc.|Bạn không thể xuất nhiều hơn :count hàng cùng lúc.',
],
'started' => [
'title' => 'Xuất đã bắt đầu',
'body' => 'Quá trình xuất của bạn đã bắt đầu và 1 hàng sẽ được xử lý ở nền.|Quá trình xuất của bạn đã bắt đầu và :count hàng sẽ được xử lý ở nền.',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -0,0 +1,77 @@
<?php
return [
'label' => '匯出 :label',
'modal' => [
'heading' => '匯出 :label',
'form' => [
'columns' => [
'label' => '欄位',
'form' => [
'is_enabled' => [
'label' => ':column 開啟',
],
'label' => [
'label' => ':column 標籤',
],
],
],
],
'actions' => [
'export' => [
'label' => '匯出',
],
],
],
'notifications' => [
'completed' => [
'title' => '匯出完成',
'actions' => [
'download_csv' => [
'label' => '下載 .csv 檔案',
],
'download_xlsx' => [
'label' => '下載 .xlsx 檔案',
],
],
],
'max_rows' => [
'title' => '匯出的檔案過大',
'body' => '你不能一次匯出超過 1 行紀錄。|你不能一次匯出超過 :count 行紀錄。',
],
'started' => [
'title' => '匯出開始',
'body' => '你的匯出已經開始,將在背景處理共 1 行。|你的匯出已經開始,將在背景處理共 :count 行。',
],
],
'file_name' => 'export-:export_id-:model',
];
@@ -0,0 +1,76 @@
<?php
return [
'label' => '匯入:label',
'modal' => [
'heading' => '匯入:label',
'form' => [
'file' => [
'label' => '檔案',
'placeholder' => '上傳一個 CSV 檔案',
],
'columns' => [
'label' => '欄位',
'placeholder' => '選擇一個欄位',
],
],
'actions' => [
'download_example' => [
'label' => '下載一個 CSV 模板',
],
'import' => [
'label' => '匯入',
],
],
],
'notifications' => [
'completed' => [
'title' => '匯入完成',
'actions' => [
'download_failed_rows_csv' => [
'label' => '下載匯入失敗的紀錄|下載匯入失敗的紀錄',
],
],
],
'max_rows' => [
'title' => '上傳的 CSV 檔案過大',
'body' => '你不能一次匯入超過 1 行紀錄。|你不能一次匯入超過 :count 行紀錄。',
],
'started' => [
'title' => '匯入開始',
'body' => '你的匯入已經開始,將在後台處理共 1 行。|你的匯入已經開始,將在後台處理共 :count 行。',
],
],
'example_csv' => [
'file_name' => ':importer-example',
],
'failure_csv' => [
'file_name' => 'import-:import_id-:csv_name-failed-rows',
'error_header' => '錯誤',
'system_error' => '系統錯誤,請聯繫官方。',
],
];
@@ -59,6 +59,8 @@ trait CanExportRecords
*/
protected array | Closure | null $formats = null;
protected ?Closure $modifyQueryUsing = null;
protected function setUp(): void
{
parent::setUp();
@@ -107,6 +109,12 @@ trait CanExportRecords
$query = $action->getExporter()::getModel()::query();
}
if ($this->modifyQueryUsing) {
$query = $this->evaluate($this->modifyQueryUsing, [
'query' => $query,
]) ?? $query;
}
$records = $action instanceof ExportTableBulkAction ? $action->getRecords() : null;
$totalRows = $records ? $records->count() : $query->count();
@@ -166,6 +174,7 @@ trait CanExportRecords
$job = $action->getJob();
$jobQueue = $exporter->getJobQueue();
$jobConnection = $exporter->getJobConnection();
$jobBatchName = $exporter->getJobBatchName();
// We do not want to send the loaded user relationship to the queue in job payloads,
// in case it contains attributes that are not serializable, such as binary columns.
@@ -194,6 +203,10 @@ trait CanExportRecords
filled($jobConnection),
fn (PendingBatch $batch) => $batch->onConnection($jobConnection),
)
->when(
filled($jobBatchName),
fn (PendingBatch $batch) => $batch->name($jobBatchName),
)
->allowFailures(),
...(($hasXlsx && (! $hasCsv)) ? [$makeCreateXlsxFileJob()] : []),
app(ExportCompletion::class, [
@@ -367,4 +380,11 @@ trait CanExportRecords
{
return $this->evaluate($this->formats);
}
public function modifyQueryUsing(?Closure $callback): static
{
$this->modifyQueryUsing = $callback;
return $this;
}
}
@@ -238,6 +238,10 @@ trait CanImportRecords
filled($jobConnection = $importer->getJobConnection()),
fn (PendingBatch $batch) => $batch->onConnection($jobConnection),
)
->when(
filled($jobBatchName = $importer->getJobBatchName()),
fn (PendingBatch $batch) => $batch->name($jobBatchName),
)
->finally(function () use ($import) {
$import->touch('completed_at');
@@ -304,7 +308,7 @@ trait CanImportRecords
}
$csv->insertOne(array_map(
fn (ImportColumn $column): string => $column->getName(),
fn (ImportColumn $column): string => $column->getExampleHeader(),
$columns,
));
@@ -24,6 +24,8 @@ class ExportColumn extends Component
protected bool | Closure $isEnabledByDefault = true;
protected string $evaluationIdentifier = 'column';
final public function __construct(string $name)
{
$this->name($name);
@@ -82,7 +84,7 @@ class ExportColumn extends Component
public function getRecord(): ?Model
{
return $this->getExporter()->getRecord();
return $this->getExporter()?->getRecord();
}
public function getLabel(): ?string
@@ -105,6 +105,11 @@ abstract class Exporter
return null;
}
public function getJobBatchName(): ?string
{
return null;
}
/**
* @return array<ExportColumn>
*/
@@ -111,6 +111,7 @@ class PrepareCsvExport implements ShouldQueue
$chunkKeySize = $this->chunkSize * 10;
$baseQuery = $query->toBase();
$baseQuery->distinct($qualifiedKeyName);
/** @phpstan-ignore-next-line */
$baseQueryOrders = $baseQuery->orders ?? [];
+17 -1
View File
@@ -52,6 +52,8 @@ class ImportColumn extends Component
protected mixed $example = null;
protected string | Closure | null $exampleHeader = null;
protected string | Closure | null $relationship = null;
/**
@@ -66,6 +68,8 @@ class ImportColumn extends Component
protected string | Closure | null $validationAttribute = null;
protected string $evaluationIdentifier = 'column';
final public function __construct(string $name)
{
$this->name($name);
@@ -108,6 +112,13 @@ class ImportColumn extends Component
return $this;
}
public function exampleHeader(string | Closure | null $header): static
{
$this->exampleHeader = $header;
return $this;
}
public function requiredMapping(bool | Closure $condition = true): static
{
$this->isMappingRequired = $condition;
@@ -286,6 +297,11 @@ class ImportColumn extends Component
return $this->name;
}
public function getExampleHeader(): string
{
return $this->evaluate($this->exampleHeader) ?? $this->getName();
}
/**
* @return array<mixed>
*/
@@ -439,7 +455,7 @@ class ImportColumn extends Component
public function getRecord(): ?Model
{
return $this->getImporter()->getRecord();
return $this->getImporter()?->getRecord();
}
public function isMappingRequired(): bool
@@ -264,6 +264,11 @@ abstract class Importer
return null;
}
public function getJobBatchName(): ?string
{
return null;
}
/**
* @return array<ImportColumn>
*/
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -367,7 +367,7 @@ MorphToSelect::make('commentable')
])
```
> Many of the same options in the select field are available for `MorphToSelect`, including `searchable()`, `preload()`, `allowHtml()`, and `optionsLimit()`.
> Many of the same options in the select field are available for `MorphToSelect`, including `searchable()`, `preload()`, `native()`, `allowHtml()`, and `optionsLimit()`.
## Allowing HTML in the option labels
@@ -346,6 +346,8 @@ FileUpload::make('attachment')
When the form is submitted, a temporary file upload object will be returned instead of a permanently stored file path. This is perfect for temporary files like imported CSVs.
Please be aware that images, video and audio files will not show the stored file name in the form's preview, unless you use [`previewable(false)`](#previewing-files). This is due to a limitation with the FilePond preview plugin.
## Orienting images from their EXIF data
By default, FilePond will automatically orient images based on their EXIF data. If you wish to disable this behaviour, you can use the `orientImagesFromExif(false)` method:
@@ -50,6 +50,7 @@ export default function fileUploadFormComponent({
isOpenable,
isPreviewable,
isReorderable,
itemPanelAspectRatio,
loadingIndicatorPosition,
locale,
maxFiles,
@@ -119,6 +120,7 @@ export default function fileUploadFormComponent({
minFileSize: minSize,
styleButtonProcessItemPosition: uploadButtonPosition,
styleButtonRemoveItemPosition: removeUploadedFileButtonPosition,
styleItemPanelAspectRatio: itemPanelAspectRatio,
styleLoadIndicatorPosition: loadingIndicatorPosition,
stylePanelAspectRatio: panelAspectRatio,
stylePanelLayout: panelLayout,
@@ -350,7 +352,10 @@ export default function fileUploadFormComponent({
options: {
type: 'local',
...(!uploadedFile.type ||
/^image/.test(uploadedFile.type)
(isPreviewable &&
(/^audio/.test(uploadedFile.type) ||
/^image/.test(uploadedFile.type) ||
/^video/.test(uploadedFile.type)))
? {}
: {
file: {
@@ -419,6 +419,31 @@ return [
'placeholder' => 'كلمة مفتاحية جديدة',
],
'text_input' => [
'actions' => [
'hide_password' => [
'label' => 'إخفاء كلمة المرور',
],
'show_password' => [
'label' => 'عرض كلمة المرور',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'نعم',
'false' => 'لا',
],
],
'wizard' => [
'actions' => [
@@ -70,6 +70,134 @@ return [
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'Abbrechen',
],
'drag_crop' => [
'label' => 'Drag Modus "beschneiden"',
],
'drag_move' => [
'label' => 'Drag Modus "verschieben"',
],
'flip_horizontal' => [
'label' => 'Bild horizontal spiegeln',
],
'flip_vertical' => [
'label' => 'Bild vertikal spiegeln',
],
'move_down' => [
'label' => 'Bild nach unten',
],
'move_left' => [
'label' => 'Bild nach links',
],
'move_right' => [
'label' => 'Bild nach rechts',
],
'move_up' => [
'label' => 'Bild nach oben',
],
'reset' => [
'label' => 'Zurücksetzen',
],
'rotate_left' => [
'label' => 'Bild nach links kippen',
],
'rotate_right' => [
'label' => 'Bild nach rechts kippen',
],
'set_aspect_ratio' => [
'label' => 'Seitenverhältnis festlegen :ratio',
],
'save' => [
'label' => 'Speichern',
],
'zoom_100' => [
'label' => 'Bild Zoom 100%',
],
'zoom_in' => [
'label' => 'Hereinzoomen',
],
'zoom_out' => [
'label' => 'Herauszoomen',
],
],
'fields' => [
'height' => [
'label' => 'Höhe',
'unit' => 'px',
],
'rotation' => [
'label' => 'Drehung',
'unit' => 'deg',
],
'width' => [
'label' => 'Breite',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'Seitenverhältnisse',
'no_fixed' => [
'label' => 'Frei',
],
],
'svg' => [
'messages' => [
'confirmation' => 'Das Bearbeiten von SVG Dateien ist nicht empfohlen, da es Qualitätsverluste beim Verändern der Größe geben kann.\n Wirklich fortfahren?',
'disabled' => 'Das Bearbeiten von SVG Dateien ist deaktiviert, da es Qualitätsverluste beim Verändern der Größe geben kann.',
],
],
],
],
'key_value' => [
'actions' => [
@@ -0,0 +1,10 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'Wenigstens muss das :attribute ausgewählt sein.',
'only_one_must_be_selected' => 'Nur das :attribute Feld darf ausgewählt sein.',
],
];
@@ -419,6 +419,31 @@ return [
'placeholder' => 'Nouveau tag',
],
'text_input' => [
'actions' => [
'hide_password' => [
'label' => 'Masquer le mot de passe',
],
'show_password' => [
'label' => 'Montrer le mot de passe',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'Oui',
'false' => 'Non',
],
],
'wizard' => [
'actions' => [
@@ -419,6 +419,31 @@ return [
'placeholder' => 'Tag baru',
],
'text_input' => [
'actions' => [
'hide_password' => [
'label' => 'Sembunyikan kata laluan',
],
'show_password' => [
'label' => 'Tunjukkan kata laluan',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'Ya',
'false' => 'Tidak',
],
],
'wizard' => [
'actions' => [
@@ -345,7 +345,7 @@ return [
],
'create_another' => [
'label' => 'Oluştur & başka bir tane oluştur',
'label' => 'Oluştur & yeni oluştur',
],
],
@@ -419,6 +419,31 @@ return [
'placeholder' => 'Thêm thẻ mới',
],
'text_input' => [
'actions' => [
'hide_password' => [
'label' => 'Ẩn mật khẩu',
],
'show_password' => [
'label' => 'Hiện mật khẩu',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'Có',
'false' => 'Không',
],
],
'wizard' => [
'actions' => [
@@ -13,13 +13,13 @@
}
$nextTick(() => {
error = $el.querySelector(\'[data-validation-error]\')
let error = $el.querySelector(\'[data-validation-error]\')
if (! error) {
return
}
elementToExpand = error
let elementToExpand = error
while (elementToExpand) {
elementToExpand.dispatchEvent(new CustomEvent(\'expand\'))
@@ -95,22 +95,31 @@
>
@if ($isReorderableWithDragAndDrop || $isReorderableWithButtons || $hasBlockLabels || $isCloneable || $isDeletable || $isCollapsible || count($visibleExtraItemActions))
<div
class="fi-fo-builder-item-header flex items-center gap-x-3 overflow-hidden px-4 py-3"
@if ($isCollapsible)
x-on:click.stop="isCollapsed = !isCollapsed"
@endif
@class([
'fi-fo-builder-item-header flex items-center gap-x-3 overflow-hidden px-4 py-3',
'cursor-pointer select-none' => $isCollapsible,
])
>
@if ($isReorderableWithDragAndDrop || $isReorderableWithButtons)
<ul class="flex items-center gap-x-3">
@if ($isReorderableWithDragAndDrop)
<li x-sortable-handle>
<li
x-sortable-handle
x-on:click.stop
>
{{ $reorderAction }}
</li>
@endif
@if ($isReorderableWithButtons)
<li>
<li x-on:click.stop>
{{ $moveUpAction(['item' => $uuid])->disabled($loop->first) }}
</li>
<li>
<li x-on:click.stop>
{{ $moveDownAction(['item' => $uuid])->disabled($loop->last) }}
</li>
@endif
@@ -119,13 +128,9 @@
@if ($hasBlockLabels)
<h4
@if ($isCollapsible)
x-on:click.stop="isCollapsed = !isCollapsed"
@endif
@class([
'text-sm font-medium text-gray-950 dark:text-white',
'truncate' => $isBlockLabelTruncated(),
'cursor-pointer select-none' => $isCollapsible,
])
>
{{ $item->getParentComponent()->getLabel($item->getRawState(), $uuid) }}
@@ -141,19 +146,19 @@
class="ms-auto flex items-center gap-x-3"
>
@foreach ($visibleExtraItemActions as $extraItemAction)
<li>
<li x-on:click.stop>
{{ $extraItemAction(['item' => $uuid]) }}
</li>
@endforeach
@if ($isCloneable)
<li>
<li x-on:click.stop>
{{ $cloneAction(['item' => $uuid]) }}
</li>
@endif
@if ($isDeletable)
<li>
<li x-on:click.stop>
{{ $deleteAction(['item' => $uuid]) }}
</li>
@endif
@@ -62,6 +62,7 @@
isOpenable: @js($isOpenable()),
isPreviewable: @js($isPreviewable()),
isReorderable: @js($isReorderable()),
itemPanelAspectRatio: @js($getItemPanelAspectRatio()),
loadingIndicatorPosition: @js($getLoadingIndicatorPosition()),
locale: @js(app()->getLocale()),
panelAspectRatio: @js($getPanelAspectRatio()),
@@ -95,24 +95,35 @@
>
@if ($isReorderableWithDragAndDrop || $isReorderableWithButtons || filled($itemLabel) || $isCloneable || $isDeletable || $isCollapsible || count($visibleExtraItemActions))
<div
class="fi-fo-repeater-item-header flex items-center gap-x-3 overflow-hidden px-4 py-3"
@if ($isCollapsible)
x-on:click.stop="isCollapsed = !isCollapsed"
@endif
@class([
'fi-fo-repeater-item-header flex items-center gap-x-3 overflow-hidden px-4 py-3',
'cursor-pointer select-none' => $isCollapsible,
])
>
@if ($isReorderableWithDragAndDrop || $isReorderableWithButtons)
<ul class="flex items-center gap-x-3">
@if ($isReorderableWithDragAndDrop)
<li x-sortable-handle>
<li
x-sortable-handle
x-on:click.stop
>
{{ $reorderAction }}
</li>
@endif
@if ($isReorderableWithButtons)
<li
x-on:click.stop
class="flex items-center justify-center"
>
{{ $moveUpAction(['item' => $uuid])->disabled($loop->first) }}
</li>
<li
x-on:click.stop
class="flex items-center justify-center"
>
{{ $moveDownAction(['item' => $uuid])->disabled($loop->last) }}
@@ -123,13 +134,9 @@
@if (filled($itemLabel))
<h4
@if ($isCollapsible)
x-on:click.stop="isCollapsed = !isCollapsed"
@endif
@class([
'text-sm font-medium text-gray-950 dark:text-white',
'truncate' => $isItemLabelTruncated(),
'cursor-pointer select-none' => $isCollapsible,
])
>
{{ $itemLabel }}
@@ -141,19 +148,19 @@
class="ms-auto flex items-center gap-x-3"
>
@foreach ($visibleExtraItemActions as $extraItemAction)
<li>
<li x-on:click.stop>
{{ $extraItemAction(['item' => $uuid]) }}
</li>
@endforeach
@if ($isCloneable)
<li>
<li x-on:click.stop>
{{ $cloneAction(['item' => $uuid]) }}
</li>
@endif
@if ($isDeletable)
<li>
<li x-on:click.stop>
{{ $deleteAction(['item' => $uuid]) }}
</li>
@endif
@@ -32,8 +32,10 @@
:suffix-icon="$suffixIcon"
:suffix-icon-color="$getSuffixIconColor()"
:valid="! $errors->has($statePath)"
class="fi-fo-select"
:attributes="\Filament\Support\prepare_inherited_attributes($getExtraAttributeBag())"
:attributes="
\Filament\Support\prepare_inherited_attributes($getExtraAttributeBag())
->class(['fi-fo-select'])
"
>
@if ((! ($isSearchable() || $isMultiple()) && $isNative()))
<x-filament::input.select
@@ -141,7 +143,6 @@
x-on:keydown.esc="select.dropdown.isActive && $event.stopPropagation()"
{{
$attributes
->merge($getExtraAttributes(), escape: false)
->merge($getExtraAlpineAttributes(), escape: false)
->class([
'[&_.choices\_\_inner]:ps-0' => $isPrefixInline && (count($prefixActions) || $prefixIcon || filled($prefixLabel)),
@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBeNative
{
protected bool | Closure $isNative = true;
public function native(bool | Closure $condition = true): static
{
$this->isNative = $condition;
return $this;
}
public function isNative(): bool
{
return (bool) $this->evaluate($this->isNative);
}
}
@@ -3,6 +3,7 @@
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Support\Collection;
trait CanDisableOptions
{
@@ -20,11 +21,16 @@ trait CanDisableOptions
*/
public function getEnabledOptions(): array
{
return array_filter(
$this->getOptions(),
fn ($label, $value) => ! $this->isOptionDisabled($value, $label),
ARRAY_FILTER_USE_BOTH,
);
return collect($this->getOptions())
->reduce(function (Collection $carry, $label, $value): Collection {
if (is_array($label)) {
return $carry->merge($label);
}
return $carry->put($value, $label);
}, collect())
->filter(fn ($label, $value) => ! $this->isOptionDisabled($value, $label))
->all();
}
/**
@@ -36,6 +36,8 @@ trait HasState
protected bool | Closure $isDehydrated = true;
protected bool | Closure $isDehydratedWhenHidden = false;
protected ?string $statePath = null;
protected string $cachedAbsoluteStatePath;
@@ -135,6 +137,13 @@ trait HasState
return $this;
}
public function dehydratedWhenHidden(bool | Closure $condition = true): static
{
$this->isDehydratedWhenHidden = $condition;
return $this;
}
public function formatStateUsing(?Closure $callback): static
{
$this->afterStateHydrated(fn (Component $component) => $component->state($component->evaluate($callback)));
@@ -432,6 +441,20 @@ trait HasState
return (bool) $this->evaluate($this->isDehydrated);
}
public function isDehydratedWhenHidden(): bool
{
return (bool) $this->evaluate($this->isDehydratedWhenHidden);
}
public function isHiddenAndNotDehydrated(): bool
{
if (! $this->isHidden()) {
return false;
}
return ! $this->isDehydratedWhenHidden();
}
public function getGetCallback(): Get
{
return new Get($this);
@@ -12,6 +12,7 @@ use Illuminate\View\ComponentAttributeBag;
class DateTimePicker extends Field implements Contracts\HasAffixActions
{
use Concerns\CanBeNative;
use Concerns\CanBeReadOnly;
use Concerns\HasAffixes;
use Concerns\HasDatalistOptions;
@@ -36,8 +37,6 @@ class DateTimePicker extends Field implements Contracts\HasAffixActions
protected string | Closure | null $format = null;
protected bool | Closure $isNative = true;
protected bool | Closure $hasDate = true;
protected bool | Closure $hasSeconds = true;
@@ -274,13 +273,6 @@ class DateTimePicker extends Field implements Contracts\HasAffixActions
return $this;
}
public function native(bool | Closure $condition = true): static
{
$this->isNative = $condition;
return $this;
}
public function date(bool | Closure $condition = true): static
{
$this->hasDate = $condition;
@@ -467,11 +459,6 @@ class DateTimePicker extends Field implements Contracts\HasAffixActions
return (bool) $this->evaluate($this->shouldCloseOnDateSelection);
}
public function isNative(): bool
{
return (bool) $this->evaluate($this->isNative);
}
public function getStep(): int | float | string | null
{
$step = $this->evaluate($this->step);
@@ -36,6 +36,8 @@ class FileUpload extends BaseFileUpload
protected bool | Closure $isAvatar = false;
protected int | float | Closure | null $itemPanelAspectRatio = null;
protected string | Closure $loadingIndicatorPosition = 'right';
protected string | Closure | null $panelAspectRatio = null;
@@ -160,6 +162,13 @@ class FileUpload extends BaseFileUpload
return $this;
}
public function itemPanelAspectRatio(int | float | Closure | null $ratio): static
{
$this->itemPanelAspectRatio = $ratio;
return $this;
}
public function loadingIndicatorPosition(string | Closure | null $position): static
{
$this->loadingIndicatorPosition = $position;
@@ -249,6 +258,20 @@ class FileUpload extends BaseFileUpload
return (bool) $this->evaluate($this->imageResizeUpscale);
}
public function getItemPanelAspectRatio(): int | float | null
{
$itemPanelAspectRatio = $this->evaluate($this->itemPanelAspectRatio);
if (
($this->getPanelLayout() === 'grid') &&
(! $itemPanelAspectRatio)
) {
return 1;
}
return $itemPanelAspectRatio;
}
public function getLoadingIndicatorPosition(): string
{
return $this->evaluate($this->loadingIndicatorPosition);
@@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
class MorphToSelect extends Component
{
use Concerns\CanAllowHtml;
use Concerns\CanBeNative;
use Concerns\CanBePreloaded;
use Concerns\CanBeSearchable;
use Concerns\HasLoadingMessage;
@@ -66,6 +67,7 @@ class MorphToSelect extends Component
fn (Type $type): string => $type->getLabel(),
$types,
))
->native($this->isNative())
->required($isRequired)
->live()
->afterStateUpdated(function (Set $set) use ($keyColumn) {
@@ -78,8 +80,10 @@ class MorphToSelect extends Component
->options($selectedType?->getOptionsUsing)
->getSearchResultsUsing($selectedType?->getSearchResultsUsing)
->getOptionLabelUsing($selectedType?->getOptionLabelUsing)
->required($isRequired)
->hidden(! $selectedType)
->native($this->isNative())
->required(filled($selectedType))
->hidden(blank($selectedType))
->dehydratedWhenHidden()
->searchable($this->isSearchable())
->searchDebounce($this->getSearchDebounce())
->searchPrompt($this->getSearchPrompt())
+2 -14
View File
@@ -33,6 +33,7 @@ use function Filament\Support\generate_search_term_expression;
class Select extends Field implements Contracts\CanDisableOptions, Contracts\HasAffixActions, Contracts\HasNestedRecursiveValidationRules
{
use Concerns\CanAllowHtml;
use Concerns\CanBeNative;
use Concerns\CanBePreloaded;
use Concerns\CanBeSearchable;
use Concerns\CanDisableOptions;
@@ -83,8 +84,6 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
protected bool | Closure $isMultiple = false;
protected bool | Closure $isNative = true;
protected ?Closure $getOptionLabelUsing = null;
protected ?Closure $getOptionLabelsUsing = null;
@@ -552,13 +551,6 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
return $this;
}
public function native(bool | Closure $condition = true): static
{
$this->isNative = $condition;
return $this;
}
public function position(string | Closure | null $position): static
{
$this->position = $position;
@@ -694,11 +686,6 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
return (bool) $this->evaluate($this->isMultiple);
}
public function isNative(): bool
{
return (bool) $this->evaluate($this->isNative);
}
public function isSearchable(): bool
{
return $this->evaluate($this->isSearchable) || $this->isMultiple();
@@ -971,6 +958,7 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
if (! $relationship instanceof BelongsToMany) {
$relationship->associate($state);
$record->wasRecentlyCreated && $record->save();
return;
}
+20 -4
View File
@@ -3,6 +3,7 @@
namespace Filament\Forms\Concerns;
use Filament\Forms\Components;
use Filament\Forms\Components\Component;
trait CanBeValidated
{
@@ -13,7 +14,11 @@ trait CanBeValidated
{
$attributes = [];
foreach ($this->getComponents() as $component) {
foreach ($this->getComponents(withHidden: true) as $component) {
if ($component->isHiddenAndNotDehydrated()) {
continue;
}
if ($component instanceof Components\Contracts\HasValidationRules) {
$component->dehydrateValidationAttributes($attributes);
}
@@ -40,7 +45,11 @@ trait CanBeValidated
{
$messages = [];
foreach ($this->getComponents() as $component) {
foreach ($this->getComponents(withHidden: true) as $component) {
if ($component->isHiddenAndNotDehydrated()) {
continue;
}
if ($component instanceof Components\Contracts\HasValidationRules) {
$component->dehydrateValidationMessages($messages);
}
@@ -67,7 +76,11 @@ trait CanBeValidated
{
$rules = [];
foreach ($this->getComponents() as $component) {
foreach ($this->getComponents(withHidden: true) as $component) {
if ($component->isHiddenAndNotDehydrated()) {
continue;
}
if ($component instanceof Components\Contracts\HasValidationRules) {
$component->dehydrateValidationRules($rules);
}
@@ -92,7 +105,10 @@ trait CanBeValidated
*/
public function validate(): array
{
if (! count($this->getComponents())) {
if (! count(array_filter(
$this->getComponents(withHidden: true),
fn (Component $component): bool => ! $component->isHiddenAndNotDehydrated(),
))) {
return [];
}
+6 -6
View File
@@ -70,8 +70,8 @@ trait HasState
*/
public function dehydrateState(array &$state = [], bool $isDehydrated = true): array
{
foreach ($this->getComponents() as $component) {
if ($component->isHidden()) {
foreach ($this->getComponents(withHidden: true) as $component) {
if ($component->isHiddenAndNotDehydrated()) {
continue;
}
@@ -87,8 +87,8 @@ trait HasState
*/
public function mutateDehydratedState(array &$state = []): array
{
foreach ($this->getComponents() as $component) {
if ($component->isHidden()) {
foreach ($this->getComponents(withHidden: true) as $component) {
if ($component->isHiddenAndNotDehydrated()) {
continue;
}
@@ -130,8 +130,8 @@ trait HasState
*/
public function mutateStateForValidation(array &$state = []): array
{
foreach ($this->getComponents() as $component) {
if ($component->isHidden()) {
foreach ($this->getComponents(withHidden: true) as $component) {
if ($component->isHiddenAndNotDehydrated()) {
continue;
}
@@ -132,6 +132,17 @@ TextEntry::make('description')
->words(10)
```
## Limiting text to a specific number of lines
You may want to limit text to a specific number of lines instead of limiting it to a fixed length. Clamping text to a number of lines is useful in responsive interfaces where you want to ensure a consistent experience across all screen sizes. This can be achieved using the `lineClamp()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->lineClamp(2)
```
## Listing multiple values
By default, if there are multiple values inside your text entry, they will be comma-separated. You may use the `listWithLineBreaks()` method to display them on new lines instead:
@@ -15,6 +15,24 @@ return [
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'المفتاح',
],
'value' => [
'label' => 'القيمة',
],
],
'placeholder' => 'لا توجد مدخلات',
],
],
];
@@ -2,8 +2,37 @@
return [
'text_entry' => [
'more_list_items' => 'und :count weitere',
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Zeige :count weniger',
'expand_list' => 'Zeige :count weitere',
],
'more_list_items' => 'und :count mehr',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Schlüssel',
],
'value' => [
'label' => 'Wert',
],
],
'placeholder' => 'Keine Einträge',
],
],
];
@@ -15,6 +15,24 @@ return [
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Khóa',
],
'value' => [
'label' => 'Giá trị',
],
],
'placeholder' => 'Không có mục',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => '少顯示 :count 筆',
'expand_list' => '多顯示 :count 筆',
],
'more_list_items' => '多 :count 筆',
],
'key_value' => [
'columns' => [
'key' => [
'label' => '索引',
],
'value' => [
'label' => '值',
],
],
'placeholder' => '無項目',
],
],
];
@@ -114,11 +114,12 @@
$icon = $getIcon($state);
$iconColor = $getIconColor($state);
$itemIsCopyable = $isCopyable($state);
$lineClamp = $getLineClamp($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',
'fi-in-text-item-prose 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',
@@ -200,6 +201,7 @@
@class([
'group-hover/item:underline group-focus-visible/item:underline' => $url,
$proseClasses => $isProse || $isMarkdown,
'line-clamp-[--line-clamp]' => $lineClamp,
match ($size) {
TextEntrySize::ExtraSmall, 'xs' => 'text-xs',
TextEntrySize::Small, 'sm', null => 'text-sm leading-6',
@@ -236,6 +238,7 @@
shades: [400, 600],
alias: 'infolists::components.text-entry.item.label',
) => ! in_array($color, [null, 'gray']),
"--line-clamp: {$lineClamp}" => $lineClamp,
])
>
{{ $formattedState }}
@@ -157,6 +157,7 @@ trait HasState
$state = collect($this->getRelationshipResults($record))
->filter(fn (Model $record): bool => array_key_exists($relationshipAttribute, $record->attributesToArray()))
->pluck($relationshipAttribute)
->filter(fn ($state): bool => filled($state))
->when($this->isDistinctList(), fn (Collection $state) => $state->unique())
->values();
@@ -6,6 +6,7 @@ use Closure;
use Filament\Infolists\Components\Contracts\HasAffixActions;
use Filament\Infolists\Components\TextEntry\TextEntrySize;
use Filament\Support\Concerns\CanBeCopied;
use Filament\Support\Concerns\HasLineClamp;
class TextEntry extends Entry implements HasAffixActions
{
@@ -17,6 +18,7 @@ class TextEntry extends Entry implements HasAffixActions
use Concerns\HasIcon;
use Concerns\HasIconColor;
use Concerns\HasWeight;
use HasLineClamp;
/**
* @var view-string
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => '通知',
'actions' => [
'clear' => [
'label' => '清除',
],
'mark_all_as_read' => [
'label' => '標記為已讀',
],
],
'empty' => [
'heading' => '沒有通知',
'description' => '請稍後再查看。',
],
],
];
+1 -1
View File
File diff suppressed because one or more lines are too long
+26 -4
View File
@@ -43,6 +43,32 @@ Not sure where to start? Review the [Getting Started guide](getting-started) to
## Using other Filament packages
The Filament Panel Builder pre-installs the [Form Builder](/docs/forms), [Table Builder](/docs/tables), [Notifications](/docs/notifications), [Actions](/docs/actions), [Infolists](/docs/infolists), and [Widgets](/docs/widgets) packages. No other installation steps are required to use these packages within a panel.
## Improving Filament panel performance
### Caching Blade Icons
You may wish to consider running `php artisan icons:cache` locally, and also in your deployment script. This is because Filament uses the [Blade Icons](https://blade-ui-kit.com/blade-icons) package, which can be much more performant when cached.
### Caching Filament components
You may also wish to consider running `php artisan filament:cache-components` in your deployment script, especially if you have large numbers of components (resources, pages, widgets, relation managers, custom Livewire components, etc.). This will create cache files in the `bootstrap/cache/filament` directory of your application, which contain indexes for each type of component. This can significantly improve the performance of Filament in some apps, as it reduces the number of files that need to be scanned and auto-discovered for components.
However, if you are actively developing your app locally, you should avoid using this command, as it will prevent any new components from being discovered until the cache is cleared or rebuilt.
You can clear the cache at any time without rebuilding it by running `php artisan filament:clear-cached-components`.
### Enabling OPcache on your server
From the [Laravel Forge documentation](https://forge.laravel.com/docs/servers/php.html#opcache):
> Optimizing the PHP OPcache for production will configure OPcache to store your compiled PHP code in memory to greatly improve performance.
Please use a search engine to find the relevant OPcache setup instructions for your environment.
### Optimizing your Laravel app
You should also consider optimizing your Laravel app for production by running `php artisan optimize` in your deployment script. This will cache the configuration files and routes.
## Deploying to production
### Allowing users to access a panel
@@ -73,10 +99,6 @@ class User extends Authenticatable implements FilamentUser
Learn more about [users](users).
### Caching Blade Icons
You may wish to consider using `php artisan icons:cache` in your deployment process, as Filament uses the [Blade Icons](https://blade-ui-kit.com/blade-icons) package, which can be much more performant when cached. This is very hardware-dependant though, and many servers do not require this command for Filament to run smoothly.
## Publishing configuration
You can publish the Filament package configuration (if needed) using the following command:
+10
View File
@@ -229,6 +229,16 @@ public function panel(Panel $panel): Panel
}
```
### Navigation groups in a collapsible sidebar on desktop
> This section only applies to `sidebarCollapsibleOnDesktop()`, not `sidebarFullyCollapsibleOnDesktop()`, since the fully collapsible UI just hides the entire sidebar instead of changing its design.
When using a collapsible sidebar on desktop, you will also often be using [navigation groups](#grouping-navigation-items). By default, the labels of each navigation group will be hidden when the sidebar is collapsed, since there is no space to display them. Even if the navigation group itself is [collapsible](#making-navigation-groups-not-collapsible), all items will still be visible in the collapsed sidebar, since there is no group label to click on to expand the group.
These issues can be solved, to achieve a very minimal sidebar design, by [passing an `icon()`](#customizing-navigation-groups) to the navigation group objects. When an icon is defined, the icon will be displayed in the collapsed sidebar instead of the items at all times. When the icon is clicked, a dropdown will open to the side of the icon, revealing the items in the group.
When passing an icon to a navigation group, even if the items also have icons, the expanded sidebar UI will not show the item icons. This is to keep the navigation hierarchy clear, and the design minimal. However, the icons for the items will be shown in the collapsed sidebar's dropdowns though, since the hierarchy is already clear from the fact that the dropdown is open.
## Registering custom navigation items
To register new navigation items, you can use the [configuration](configuration):
@@ -6,6 +6,10 @@ return [
'actions' => [
'billing' => [
'label' => 'Abonnement verwalten',
],
'logout' => [
'label' => 'Abmelden',
],
@@ -52,7 +52,8 @@ return [
'notifications' => [
'throttled' => [
'title' => 'Zu viele Loginversuche. Versuchen Sie es bitte in :seconds Sekunden nochmal.',
'title' => 'Zu viele Loginversuche.',
'body' => ' Bitte in :seconds Sekunden nochmal versuchen.',
],
],
@@ -34,7 +34,9 @@ return [
'notifications' => [
'throttled' => [
'title' => 'Zu viele Versuche. Versuchen Sie es bitte in :seconds Sekunden nochmal.',
'title' => 'Zu viele Versuche.',
'body' => 'Bitte in :seconds Sekunden nochmal versuchen.',
],
],
@@ -47,7 +47,8 @@ return [
'notifications' => [
'throttled' => [
'title' => 'Zu viele Anmeldeversuche. Versuchen Sie es bitte in :seconds Sekunden nochmal.',
'title' => 'Zu viele Anmeldeversuche.',
'body' => 'Bitte in :seconds Sekunden nochmal versuchen.',
],
],
@@ -4,4 +4,29 @@ return [
'title' => 'Dashboard',
'actions' => [
'filter' => [
'label' => 'Filter',
'modal' => [
'heading' => 'Filter',
'actions' => [
'apply' => [
'label' => 'Übernehmen',
],
],
],
],
],
];
@@ -43,7 +43,7 @@ return [
'actions' => [
'cancel' => [
'label' => 'indietro',
'label' => 'Indietro',
],
],
@@ -1,10 +1,15 @@
@props([
'actions' => [],
'breadcrumbs' => [],
'heading',
'subheading' => null,
])
@php
// These are passed through in the bag otherwise Laravel converts View objects to strings prematurely.
$heading = $attributes->get('heading');
$subheading = $attributes->get('subheading');
$attributes = $attributes->except(['heading', 'subheading']);
@endphp
<header
{{ $attributes->class(['fi-header flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between']) }}
>
@@ -5,20 +5,10 @@
@endphp
<x-filament-panels::layout.base :livewire="$livewire">
<div class="fi-layout flex min-h-screen w-full overflow-x-clip">
@if (filament()->hasNavigation())
<div
x-cloak
x-data="{}"
x-on:click="$store.sidebar.close()"
x-show="$store.sidebar.isOpen"
x-transition.opacity.300ms
class="fi-sidebar-close-overlay fixed inset-0 z-30 bg-gray-950/50 transition duration-500 dark:bg-gray-950/75 lg:hidden"
></div>
<x-filament-panels::sidebar :navigation="$navigation" />
@endif
{{-- The sidebar is after the page content in the markup to fix issues with page content overlapping dropdown content from the sidebar. --}}
<div
class="fi-layout flex min-h-screen w-full flex-row-reverse overflow-x-clip"
>
<div
@if (filament()->isSidebarCollapsibleOnDesktop())
x-data="{}"
@@ -89,5 +79,18 @@
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire->getRenderHookScopes()) }}
</div>
@if (filament()->hasNavigation())
<div
x-cloak
x-data="{}"
x-on:click="$store.sidebar.close()"
x-show="$store.sidebar.isOpen"
x-transition.opacity.300ms
class="fi-sidebar-close-overlay fixed inset-0 z-30 bg-gray-950/50 transition duration-500 dark:bg-gray-950/75 lg:hidden"
></div>
<x-filament-panels::sidebar :navigation="$navigation" />
@endif
</div>
</x-filament-panels::layout.base>
@@ -32,8 +32,13 @@
<x-filament-panels::header
:actions="$this->getCachedHeaderActions()"
:breadcrumbs="filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : []"
:heading="$heading"
:subheading="$this->getSubheading()"
{{-- These are passed through in the bag otherwise Laravel converts View objects to strings prematurely. --}}
:attributes="
new \Illuminate\View\ComponentAttributeBag([
'heading' => $heading,
'subheading' => $this->getSubheading(),
])
"
/>
@endif
@@ -8,6 +8,7 @@
>
@foreach ($navigation as $navigationGroup)
<x-filament-panels::sidebar.group
:active="$navigationGroup->isActive()"
:collapsible="$navigationGroup->isCollapsible()"
:icon="$navigationGroup->getIcon()"
:items="$navigationGroup->getItems()"
@@ -1,4 +1,5 @@
@props([
'active' => false,
'collapsible' => true,
'icon' => null,
'items' => [],
@@ -7,6 +8,11 @@
'subNavigation' => false,
])
@php
$sidebarCollapsible = $sidebarCollapsible && filament()->isSidebarCollapsibleOnDesktop();
$hasDropdown = filled($label) && filled($icon) && $sidebarCollapsible;
@endphp
<li
x-data="{ label: @js($subNavigation ? "sub_navigation_{$label}" : $label) }"
data-group-label="{{ $label }}"
@@ -17,7 +23,7 @@
@if ($collapsible)
x-on:click="$store.sidebar.toggleCollapsedGroup(label)"
@endif
@if (filament()->isSidebarCollapsibleOnDesktop())
@if ($sidebarCollapsible)
x-show="$store.sidebar.isOpen"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
@@ -56,17 +62,118 @@
</div>
@endif
@if ($hasDropdown)
<x-filament::dropdown
:placement="(__('filament-panels::layout.direction') === 'rtl') ? 'left-start' : 'right-start'"
teleport
x-show="! $store.sidebar.isOpen"
>
<x-slot name="trigger">
<button
x-data="{ tooltip: false }"
x-effect="
tooltip = $store.sidebar.isOpen
? false
: {
content: @js($label),
placement: document.dir === 'rtl' ? 'left' : 'right',
theme: $store.theme,
}
"
x-tooltip.html="tooltip"
class="relative flex flex-1 items-center justify-center gap-x-3 rounded-lg px-2 py-2 outline-none transition duration-75 hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<x-filament::icon
:icon="$icon"
@class([
'h-6 w-6',
'text-gray-400 dark:text-gray-500' => ! $active,
'text-primary-600 dark:text-primary-400' => $active,
])
/>
</button>
</x-slot>
@php
$lists = [];
foreach ($items as $item) {
if ($childItems = $item->getChildItems()) {
$lists[] = [
$item,
...$childItems,
];
$lists[] = [];
continue;
}
if (empty($lists)) {
$lists[] = [$item];
continue;
}
$lists[count($lists) - 1][] = $item;
}
if (empty($lists[count($lists) - 1])) {
array_pop($lists);
}
@endphp
@if (filled($label))
<x-filament::dropdown.header>
{{ $label }}
</x-filament::dropdown.header>
@endif
@foreach ($lists as $list)
<x-filament::dropdown.list>
@foreach ($list as $item)
@php
$itemIsActive = $item->isActive();
@endphp
<x-filament::dropdown.list.item
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:color="$itemIsActive ? 'primary' : 'gray'"
:href="$item->getUrl()"
:icon="$itemIsActive ? ($item->getActiveIcon() ?? $item->getIcon()) : $item->getIcon()"
tag="a"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
@endforeach
</x-filament::dropdown>
@endif
<ul
x-show="! ($store.sidebar.groupIsCollapsed(label) && ($store.sidebar.isOpen || @js(! filament()->isSidebarCollapsibleOnDesktop())))"
@if (filament()->isSidebarCollapsibleOnDesktop())
@if (filled($label))
@if ($sidebarCollapsible)
x-show="$store.sidebar.isOpen ? ! $store.sidebar.groupIsCollapsed(label) : ! @js($hasDropdown)"
@else
x-show="! $store.sidebar.groupIsCollapsed(label)"
@endif
x-collapse.duration.200ms
@endif
@if ($sidebarCollapsible)
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
x-collapse.duration.200ms
class="fi-sidebar-group-items flex flex-col gap-y-1"
>
@foreach ($items as $item)
@php
$itemIcon = $item->getIcon();
@endphp
<x-filament-panels::sidebar.item
:active="$item->isActive()"
:active-child-items="$item->isChildItemsActive()"
@@ -77,7 +184,7 @@
:child-items="$item->getChildItems()"
:first="$loop->first"
:grouped="filled($label)"
:icon="$item->getIcon()"
:icon="$icon ? (($hasDropdown || blank($itemIcon)) ? null : throw new \Exception('Navigation group [' . $label . '] has an icon but one or more of its items also have icons. Either the group or its items can have icons, but not both. This is to ensure a proper user experience.')) : $itemIcon"
:last="$loop->last"
:should-open-url-in-new-tab="$item->shouldOpenUrlInNewTab()"
:sidebar-collapsible="$sidebarCollapsible"
@@ -117,6 +117,7 @@
<ul class="fi-sidebar-nav-groups -mx-2 flex flex-col gap-y-7">
@foreach ($navigation as $group)
<x-filament-panels::sidebar.group
:active="$group->isActive()"
:collapsible="$group->isCollapsible()"
:icon="$group->getIcon()"
:items="$group->getItems()"
@@ -51,10 +51,10 @@
'bg-gray-100 dark:bg-white/5' => $active,
])
>
@if (filled($icon) && ((! $subGrouped) || filament()->isSidebarCollapsibleOnDesktop()))
@if (filled($icon) && ((! $subGrouped) || $sidebarCollapsible))
<x-filament::icon
:icon="($active && $activeIcon) ? $activeIcon : $icon"
:x-show="($subGrouped && filament()->isSidebarCollapsibleOnDesktop()) ? '! $store.sidebar.isOpen' : false"
:x-show="($subGrouped && $sidebarCollapsible) ? '! $store.sidebar.isOpen' : false"
@class([
'fi-sidebar-item-icon h-6 w-6',
'text-gray-400 dark:text-gray-500' => ! $active,
@@ -65,7 +65,7 @@
@if ((blank($icon) && $grouped) || $subGrouped)
<div
@if (filled($icon) && $subGrouped && filament()->isSidebarCollapsibleOnDesktop())
@if (filled($icon) && $subGrouped && $sidebarCollapsible)
x-show="$store.sidebar.isOpen"
@endif
class="fi-sidebar-item-grouped-border relative flex h-6 w-6 items-center justify-center"
@@ -79,27 +79,56 @@
</x-filament-panels::topbar.item>
</x-slot>
<x-filament::dropdown.list>
@foreach ($group->getItems() as $item)
@php
$icon = $item->getIcon();
$isActive = $item->isActive();
@endphp
@php
$lists = [];
<x-filament::dropdown.list.item
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:color="$isActive ? 'primary' : 'gray'"
:href="$item->getUrl()"
:icon="$isActive ? ($item->getActiveIcon() ?? $icon) : $icon"
tag="a"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
foreach ($group->getItems() as $item) {
if ($childItems = $item->getChildItems()) {
$lists[] = [
$item,
...$childItems,
];
$lists[] = [];
continue;
}
if (empty($lists)) {
$lists[] = [$item];
continue;
}
$lists[count($lists) - 1][] = $item;
}
if (empty($lists[count($lists) - 1])) {
array_pop($lists);
}
@endphp
@foreach ($lists as $list)
<x-filament::dropdown.list>
@foreach ($list as $item)
@php
$itemIsActive = $item->isActive();
@endphp
<x-filament::dropdown.list.item
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:color="$itemIsActive ? 'primary' : 'gray'"
:href="$item->getUrl()"
:icon="$itemIsActive ? ($item->getActiveIcon() ?? $item->getIcon()) : $item->getIcon()"
tag="a"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
@endforeach
</x-filament::dropdown>
@else
@foreach ($group->getItems() as $item)
@@ -0,0 +1,26 @@
<?php
namespace Filament\Commands;
use Filament\Facades\Filament;
use Illuminate\Console\Command;
class CacheComponentsCommand extends Command
{
protected $description = 'Cache all components';
protected $signature = 'filament:cache-components';
public function handle(): int
{
$this->info('Caching registered components...');
foreach (Filament::getPanels() as $panel) {
$panel->cacheComponents();
}
$this->info('All done!');
return static::SUCCESS;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Filament\Commands;
use Filament\Facades\Filament;
use Illuminate\Console\Command;
class ClearCachedComponentsCommand extends Command
{
protected $description = 'Cache all components';
protected $signature = 'filament:clear-cached-components';
public function handle(): int
{
$this->info('Clearing cached components...');
foreach (Filament::getPanels() as $panel) {
$panel->clearCachedComponents();
}
$this->info('All done!');
return static::SUCCESS;
}
}
+3 -3
View File
@@ -13,6 +13,7 @@ use Filament\Navigation\MenuItem;
use Filament\Navigation\NavigationGroup;
use Filament\Navigation\NavigationItem;
use Filament\Panel;
use Filament\PanelRegistry;
use Filament\Support\Assets\Theme;
use Filament\Support\Enums\MaxWidth;
use Illuminate\Contracts\Auth\Authenticatable;
@@ -114,7 +115,6 @@ use Illuminate\Support\Facades\Facade;
* @method static bool isServing()
* @method static bool isSidebarCollapsibleOnDesktop()
* @method static bool isSidebarFullyCollapsibleOnDesktop()
* @method static void mountNavigation()
* @method static void serving(Closure $callback)
* @method static void setCurrentPanel(Panel | null $panel = null)
* @method static void setServingStatus(bool $condition = true)
@@ -132,8 +132,8 @@ class Filament extends Facade
public static function registerPanel(Panel | Closure $panel): void
{
static::getFacadeApplication()->resolving(
static::getFacadeAccessor(),
fn (FilamentManager $filamentManager) => $filamentManager->registerPanel(value($panel)),
PanelRegistry::class,
fn (PanelRegistry $registry) => $registry->register(value($panel)),
);
}
}
+12 -25
View File
@@ -34,11 +34,6 @@ use Illuminate\Support\Facades\Event;
class FilamentManager
{
/**
* @var array<string, Panel>
*/
protected array $panels = [];
protected ?Panel $currentPanel = null;
protected bool $isServing = false;
@@ -47,6 +42,13 @@ class FilamentManager
protected ?Model $tenant = null;
public function __construct()
{
// Resolve the panel registry to set the current panel
// as the default, which uses a `resolving()` callback.
app()->resolved(PanelRegistry::class) || app(PanelRegistry::class);
}
public function auth(): Guard
{
return $this->getCurrentPanel()->auth();
@@ -103,7 +105,7 @@ class FilamentManager
public function getCurrentPanel(): ?Panel
{
return $this->currentPanel ?? null;
return $this->currentPanel;
}
public function getDarkModeBrandLogo(): string | Htmlable | null
@@ -126,11 +128,7 @@ class FilamentManager
*/
public function getDefaultPanel(): Panel
{
return Arr::first(
$this->panels,
fn (Panel $panel): bool => $panel->isDefault(),
fn () => throw NoDefaultPanelSetException::make(),
);
return app(PanelRegistry::class)->getDefault();
}
/**
@@ -276,7 +274,7 @@ class FilamentManager
public function getPanel(?string $id = null): Panel
{
return $this->panels[$id] ?? $this->getDefaultPanel();
return app(PanelRegistry::class)->get($id);
}
/**
@@ -284,7 +282,7 @@ class FilamentManager
*/
public function getPanels(): array
{
return $this->panels;
return app(PanelRegistry::class)->all();
}
public function getPlugin(string $id): Plugin
@@ -655,20 +653,9 @@ class FilamentManager
return $this->getCurrentPanel()->isSidebarFullyCollapsibleOnDesktop();
}
public function mountNavigation(): void
{
$this->getCurrentPanel()->mountNavigation();
}
public function registerPanel(Panel $panel): void
{
$this->panels[$panel->getId()] = $panel;
$panel->register();
if ($panel->isDefault()) {
$this->setCurrentPanel($panel);
}
app(PanelRegistry::class)->register($panel);
}
/**
@@ -18,6 +18,7 @@ use Filament\Http\Responses\Auth\LoginResponse;
use Filament\Http\Responses\Auth\LogoutResponse;
use Filament\Http\Responses\Auth\PasswordResetResponse;
use Filament\Http\Responses\Auth\RegistrationResponse;
use Filament\Navigation\NavigationManager;
use Filament\Support\Assets\Js;
use Filament\Support\Assets\Theme;
use Filament\Support\Facades\FilamentAsset;
@@ -47,6 +48,14 @@ class FilamentServiceProvider extends PackageServiceProvider
return new FilamentManager();
});
$this->app->singleton(PanelRegistry::class, function (): PanelRegistry {
return new PanelRegistry();
});
$this->app->scoped(NavigationManager::class, function (): NavigationManager {
return new NavigationManager();
});
$this->app->bind(EmailVerificationResponseContract::class, EmailVerificationResponse::class);
$this->app->bind(LoginResponseContract::class, LoginResponse::class);
$this->app->bind(LogoutResponseContract::class, LogoutResponse::class);
@@ -96,6 +105,8 @@ class FilamentServiceProvider extends PackageServiceProvider
protected function getCommands(): array
{
$commands = [
Commands\CacheComponentsCommand::class,
Commands\ClearCachedComponentsCommand::class,
Commands\MakeClusterCommand::class,
Commands\MakePageCommand::class,
Commands\MakePanelCommand::class,
@@ -3,7 +3,6 @@
namespace Filament\Navigation;
use Closure;
use Exception;
use Filament\Support\Components\Component;
use Filament\Support\Concerns\HasExtraSidebarAttributes;
use Filament\Support\Concerns\HasExtraTopbarAttributes;
@@ -82,13 +81,7 @@ class NavigationGroup extends Component
public function getIcon(): ?string
{
$icon = $this->evaluate($this->icon);
if ($this->hasItemIcons() && filled($icon)) {
throw new Exception("Navigation group [{$this->getLabel()}] has an icon but one or more of its items also have icons. Either the group or its items can have icons, but not both. This is to ensure a proper user experience.");
}
return $icon;
return $this->evaluate($this->icon);
}
/**
@@ -126,30 +119,4 @@ class NavigationGroup extends Component
return false;
}
public function hasItemIcons(): bool
{
$hasIconCount = 0;
$hasNoIconCount = 0;
foreach ($this->getItems() as $item) {
if (! $item instanceof NavigationItem) {
continue;
}
if (blank($item->getIcon())) {
$hasNoIconCount++;
continue;
}
$hasIconCount++;
}
if (($hasIconCount > 0) && ($hasNoIconCount > 0) && filled($label = $this->getLabel())) {
throw new Exception("Navigation group [{$label}] has items with and without icons. All items must have icons or none of them can have icons. This is to ensure a proper user experience.");
}
return $hasIconCount > 0;
}
}
@@ -0,0 +1,185 @@
<?php
namespace Filament\Navigation;
use Filament\Facades\Filament;
use Filament\Panel;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class NavigationManager
{
protected Panel $panel;
protected bool $isNavigationMounted = false;
/**
* @var array<string | int, NavigationGroup | string>
*/
protected array $navigationGroups = [];
/**
* @var array<NavigationItem>
*/
protected array $navigationItems = [];
public function __construct()
{
$this->panel = Filament::getCurrentPanel();
$this->navigationGroups = array_map(
fn (NavigationGroup | string $group): NavigationGroup | string => $group instanceof NavigationGroup ? (clone $group) : $group,
$this->panel->getNavigationGroups(),
);
$this->navigationItems = array_map(
fn (NavigationItem $item): NavigationItem => clone $item,
$this->panel->getNavigationItems(),
);
}
/**
* @return array<NavigationGroup>
*/
public function get(): array
{
if ($this->panel->hasNavigationBuilder()) {
return $this->panel->buildNavigation();
}
if (! $this->isNavigationMounted) {
$this->mountNavigation();
}
$groups = collect($this->getNavigationGroups());
return collect($this->getNavigationItems())
->filter(fn (NavigationItem $item): bool => $item->isVisible())
->sortBy(fn (NavigationItem $item): int => $item->getSort())
->groupBy(fn (NavigationItem $item): ?string => $item->getGroup())
->map(function (Collection $items, ?string $groupIndex) use ($groups): NavigationGroup {
$parentItems = $items->groupBy(fn (NavigationItem $item): ?string => $item->getParentItem());
$items = $parentItems->get('')
->keyBy(fn (NavigationItem $item): string => $item->getLabel());
$parentItems->except([''])->each(function (Collection $parentItemItems, string $parentItemLabel) use ($items) {
if (! $items->has($parentItemLabel)) {
return;
}
$items->get($parentItemLabel)->childItems($parentItemItems);
});
if (blank($groupIndex)) {
return NavigationGroup::make()->items($items);
}
$registeredGroup = $groups
->first(function (NavigationGroup | string $registeredGroup, string | int $registeredGroupIndex) use ($groupIndex) {
if ($registeredGroupIndex === $groupIndex) {
return true;
}
if ($registeredGroup === $groupIndex) {
return true;
}
if (! $registeredGroup instanceof NavigationGroup) {
return false;
}
return $registeredGroup->getLabel() === $groupIndex;
});
if ($registeredGroup instanceof NavigationGroup) {
return $registeredGroup->items($items);
}
return NavigationGroup::make($registeredGroup ?? $groupIndex)
->items($items);
})
->sortBy(function (NavigationGroup $group, ?string $groupIndex): int {
if (blank($group->getLabel())) {
return -1;
}
$registeredGroups = $this->getNavigationGroups();
$groupsToSearch = $registeredGroups;
if (Arr::first($registeredGroups) instanceof NavigationGroup) {
$groupsToSearch = [
...array_keys($registeredGroups),
...array_map(fn (NavigationGroup $registeredGroup): string => $registeredGroup->getLabel(), array_values($registeredGroups)),
];
}
$sort = array_search(
$groupIndex,
$groupsToSearch,
);
if ($sort === false) {
return count($registeredGroups);
}
return $sort;
})
->all();
}
public function mountNavigation(): void
{
foreach ($this->panel->getPages() as $page) {
$page::registerNavigationItems();
}
foreach ($this->panel->getResources() as $resource) {
$resource::registerNavigationItems();
}
$this->isNavigationMounted = true;
}
/**
* @param array<string | int, NavigationGroup | string> $groups
*/
public function navigationGroups(array $groups): static
{
$this->navigationGroups = [
...$this->navigationGroups,
...$groups,
];
return $this;
}
/**
* @param array<NavigationItem> $items
*/
public function navigationItems(array $items): static
{
$this->navigationItems = [
...$this->navigationItems,
...$items,
];
return $this;
}
/**
* @return array<string | int, NavigationGroup | string>
*/
public function getNavigationGroups(): array
{
return $this->navigationGroups;
}
/**
* @return array<NavigationItem>
*/
public function getNavigationItems(): array
{
return $this->navigationItems;
}
}
@@ -115,6 +115,13 @@ class EditProfile extends Page
static::routes($panel);
}
public static function getRouteName(?string $panel = null): string
{
$panel = $panel ? Filament::getPanel($panel) : Filament::getCurrentPanel();
return $panel->generateRouteName('auth.' . static::getRelativeRouteName());
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
@@ -53,6 +53,13 @@ abstract class EditTenantProfile extends Page
return 'profile';
}
public static function getRouteName(?string $panel = null): string
{
$panel = $panel ? Filament::getPanel($panel) : Filament::getCurrentPanel();
return $panel->generateRouteName('tenant.' . static::getRelativeRouteName());
}
public static function isTenantSubscriptionRequired(Panel $panel): bool
{
return false;
+1 -4
View File
@@ -46,10 +46,7 @@ class Panel extends Component
public static function make(): static
{
$static = app(static::class);
$static->configure();
return $static;
return app(static::class);
}
public function default(bool $condition = true): static
@@ -2,11 +2,13 @@
namespace Filament\Panel\Concerns;
use Closure;
trait HasBreadcrumbs
{
protected bool $hasBreadcrumbs = true;
protected bool | Closure $hasBreadcrumbs = true;
public function breadcrumbs(bool $condition = true): static
public function breadcrumbs(bool | Closure $condition = true): static
{
$this->hasBreadcrumbs = $condition;
@@ -15,6 +17,6 @@ trait HasBreadcrumbs
public function hasBreadcrumbs(): bool
{
return $this->hasBreadcrumbs;
return (bool) $this->evaluate($this->hasBreadcrumbs);
}
}
@@ -97,11 +97,17 @@ trait HasComponents
protected bool | Closure $hasReadOnlyRelationManagersOnResourceViewPagesByDefault = true;
protected ?bool $hasCachedComponents = null;
/**
* @param array<class-string> $pages
*/
public function pages(array $pages): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->pages = [
...$this->pages,
...$pages,
@@ -120,6 +126,10 @@ trait HasComponents
*/
public function resources(array $resources): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->resources = [
...$this->resources,
...$resources,
@@ -154,6 +164,10 @@ trait HasComponents
*/
public function widgets(array $widgets): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->widgets = [
...$this->widgets,
...$widgets,
@@ -181,6 +195,10 @@ trait HasComponents
public function discoverPages(string $in, string $for): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->pageDirectories[] = $in;
$this->pageNamespaces[] = $for;
@@ -218,6 +236,10 @@ trait HasComponents
public function discoverClusters(string $in, string $for): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->clusterDirectories[] = $in;
$this->clusterNamespaces[] = $for;
@@ -261,6 +283,10 @@ trait HasComponents
public function discoverResources(string $in, string $for): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->resourceDirectories[] = $in;
$this->resourceNamespaces[] = $for;
@@ -298,6 +324,10 @@ trait HasComponents
public function discoverWidgets(string $in, string $for): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->widgetDirectories[] = $in;
$this->widgetNamespaces[] = $for;
@@ -329,11 +359,15 @@ trait HasComponents
public function discoverLivewireComponents(string $in, string $for): static
{
$component = [];
if ($this->hasCachedComponents()) {
return $this;
}
$components = [];
$this->discoverComponents(
Component::class,
$component,
$components,
directory: $in,
namespace: $for,
);
@@ -434,6 +468,10 @@ trait HasComponents
*/
public function livewireComponents(array $components): static
{
if ($this->hasCachedComponents()) {
return $this;
}
foreach ($components as $component) {
$this->queueLivewireComponentForRegistration($component);
}
@@ -443,60 +481,60 @@ trait HasComponents
protected function registerLivewireComponents(): void
{
$this->queueLivewireComponentForRegistration(DatabaseNotifications::class);
$this->queueLivewireComponentForRegistration(EditProfile::class);
$this->queueLivewireComponentForRegistration(GlobalSearch::class);
$this->queueLivewireComponentForRegistration(Notifications::class);
if (! $this->hasCachedComponents()) {
$this->queueLivewireComponentForRegistration(DatabaseNotifications::class);
$this->queueLivewireComponentForRegistration(EditProfile::class);
$this->queueLivewireComponentForRegistration(GlobalSearch::class);
$this->queueLivewireComponentForRegistration(Notifications::class);
if ($this->hasEmailVerification() && is_subclass_of($emailVerificationRouteAction = $this->getEmailVerificationPromptRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($emailVerificationRouteAction);
}
if ($this->hasLogin() && is_subclass_of($loginRouteAction = $this->getLoginRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($loginRouteAction);
}
if ($this->hasPasswordReset()) {
if (is_subclass_of($requestPasswordResetRouteAction = $this->getRequestPasswordResetRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($requestPasswordResetRouteAction);
if ($this->hasEmailVerification() && is_subclass_of($emailVerificationRouteAction = $this->getEmailVerificationPromptRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($emailVerificationRouteAction);
}
if (is_subclass_of($resetPasswordRouteAction = $this->getResetPasswordRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($resetPasswordRouteAction);
}
}
if ($this->hasRegistration() && is_subclass_of($registrationRouteAction = $this->getRegistrationRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($registrationRouteAction);
}
foreach ($this->getResources() as $resource) {
foreach ($resource::getPages() as $pageRegistration) {
$this->queueLivewireComponentForRegistration($pageRegistration->getPage());
if ($this->hasLogin() && is_subclass_of($loginRouteAction = $this->getLoginRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($loginRouteAction);
}
foreach ($resource::getRelations() as $relation) {
if ($relation instanceof RelationGroup) {
foreach ($relation->getManagers() as $groupedRelation) {
$this->queueLivewireComponentForRegistration($this->normalizeRelationManagerClass($groupedRelation));
}
continue;
if ($this->hasPasswordReset()) {
if (is_subclass_of($requestPasswordResetRouteAction = $this->getRequestPasswordResetRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($requestPasswordResetRouteAction);
}
$this->queueLivewireComponentForRegistration($this->normalizeRelationManagerClass($relation));
if (is_subclass_of($resetPasswordRouteAction = $this->getResetPasswordRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($resetPasswordRouteAction);
}
}
foreach ($resource::getWidgets() as $widget) {
$this->queueLivewireComponentForRegistration($this->normalizeWidgetClass($widget));
if ($this->hasRegistration() && is_subclass_of($registrationRouteAction = $this->getRegistrationRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($registrationRouteAction);
}
foreach ($this->getResources() as $resource) {
foreach ($resource::getPages() as $pageRegistration) {
$this->queueLivewireComponentForRegistration($pageRegistration->getPage());
}
foreach ($resource::getRelations() as $relation) {
if ($relation instanceof RelationGroup) {
foreach ($relation->getManagers() as $groupedRelation) {
$this->queueLivewireComponentForRegistration($this->normalizeRelationManagerClass($groupedRelation));
}
continue;
}
$this->queueLivewireComponentForRegistration($this->normalizeRelationManagerClass($relation));
}
foreach ($resource::getWidgets() as $widget) {
$this->queueLivewireComponentForRegistration($this->normalizeWidgetClass($widget));
}
}
}
foreach ($this->livewireComponents as $componentName => $componentClass) {
Livewire::component($componentName, $componentClass);
}
$this->livewireComponents = [];
}
/**
@@ -561,4 +599,78 @@ trait HasComponents
return $this->clusteredComponents[$cluster] ?? [];
}
public function hasCachedComponents(): bool
{
return $this->hasCachedComponents ??= ((! app()->runningInConsole()) && app(Filesystem::class)->exists($this->getComponentCachePath()));
}
public function cacheComponents(): void
{
$this->hasCachedComponents = false;
$cachePath = $this->getComponentCachePath();
$filesystem = app(Filesystem::class);
$filesystem->ensureDirectoryExists((string) str($cachePath)->beforeLast(DIRECTORY_SEPARATOR));
$filesystem->put(
$cachePath,
'<?php return ' . var_export([
'livewireComponents' => $this->livewireComponents,
'clusters' => $this->clusters,
'clusteredComponents' => $this->clusteredComponents,
'clusterDirectories' => $this->clusterDirectories,
'clusterNamespaces' => $this->clusterNamespaces,
'pages' => $this->pages,
'pageDirectories' => $this->pageDirectories,
'pageNamespaces' => $this->pageNamespaces,
'resources' => $this->resources,
'resourceDirectories' => $this->resourceDirectories,
'resourceNamespaces' => $this->resourceNamespaces,
'widgets' => $this->widgets,
'widgetDirectories' => $this->widgetDirectories,
'widgetNamespaces' => $this->widgetNamespaces,
], true) . ';',
);
$this->hasCachedComponents = true;
}
public function restoreCachedComponents(): void
{
if (! $this->hasCachedComponents()) {
return;
}
$cache = require $this->getComponentCachePath();
$this->livewireComponents = $cache['livewireComponents'] ?? [];
$this->clusters = $cache['clusters'] ?? [];
$this->clusteredComponents = $cache['clusteredComponents'] ?? [];
$this->clusterDirectories = $cache['clusterDirectories'] ?? [];
$this->clusterNamespaces = $cache['clusterNamespaces'] ?? [];
$this->pages = $cache['pages'] ?? [];
$this->pageDirectories = $cache['pageDirectories'] ?? [];
$this->pageNamespaces = $cache['pageNamespaces'] ?? [];
$this->resources = $cache['resources'] ?? [];
$this->resourceDirectories = $cache['resourceDirectories'] ?? [];
$this->resourceNamespaces = $cache['resourceNamespaces'] ?? [];
$this->widgets = $cache['widgets'] ?? [];
$this->widgetDirectories = $cache['widgetDirectories'] ?? [];
$this->widgetNamespaces = $cache['widgetNamespaces'] ?? [];
}
public function clearCachedComponents(): void
{
app(Filesystem::class)->delete($this->getComponentCachePath());
$this->hasCachedComponents = false;
}
public function getComponentCachePath(): string
{
return (config('filament.cache_path') ?? base_path('bootstrap/cache/filament')) . DIRECTORY_SEPARATOR . "panels/{$this->getId()}.php";
}
}
@@ -2,19 +2,31 @@
namespace Filament\Panel\Concerns;
use Exception;
trait HasId
{
protected string $id;
public function id(string $id): static
{
if (isset($this->id)) {
throw new Exception("The panel has already been registered with the ID [{$this->id}].");
}
$this->id = $id;
$this->configure();
$this->restoreCachedComponents();
return $this;
}
public function getId(): string
{
if (! isset($this->id)) {
throw new Exception('A panel has been registered without an `id()`.');
}
return $this->id;
}
}
@@ -5,13 +5,10 @@ namespace Filament\Panel\Concerns;
use Closure;
use Filament\Navigation\NavigationGroup;
use Filament\Navigation\NavigationItem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Filament\Navigation\NavigationManager;
trait HasNavigation
{
protected bool $isNavigationMounted = false;
/**
* @var array<string | int, NavigationGroup | string>
*/
@@ -24,6 +21,8 @@ trait HasNavigation
protected Closure | bool $navigationBuilder = true;
protected ?NavigationManager $navigationManager = null;
public function navigation(Closure | bool $builder = true): static
{
$this->navigationBuilder = $builder;
@@ -42,24 +41,17 @@ trait HasNavigation
return $builder->getNavigation();
}
public function mountNavigation(): void
{
foreach ($this->getPages() as $page) {
$page::registerNavigationItems();
}
foreach ($this->getResources() as $resource) {
$resource::registerNavigationItems();
}
$this->isNavigationMounted = true;
}
/**
* @param array<string | int, NavigationGroup | string> $groups
*/
public function navigationGroups(array $groups): static
{
if (isset($this->navigationManager)) {
$this->navigationManager->navigationGroups($groups);
return $this;
}
$this->navigationGroups = [
...$this->navigationGroups,
...$groups,
@@ -73,6 +65,12 @@ trait HasNavigation
*/
public function navigationItems(array $items): static
{
if (isset($this->navigationManager)) {
$this->navigationManager->navigationItems($items);
return $this;
}
$this->navigationItems = [
...$this->navigationItems,
...$items,
@@ -86,95 +84,23 @@ trait HasNavigation
return $this->navigationBuilder !== false;
}
public function hasNavigationBuilder(): bool
{
return $this->navigationBuilder instanceof Closure;
}
/**
* @return array<NavigationGroup>
*/
public function getNavigation(): array
{
if ($this->navigationBuilder instanceof Closure) {
return $this->buildNavigation();
$this->navigationManager = app(NavigationManager::class);
try {
return app(NavigationManager::class)->get();
} finally {
$this->navigationManager = null;
}
if (! $this->isNavigationMounted) {
$this->mountNavigation();
}
$groups = collect($this->getNavigationGroups());
return collect($this->getNavigationItems())
->filter(fn (NavigationItem $item): bool => $item->isVisible())
->sortBy(fn (NavigationItem $item): int => $item->getSort())
->groupBy(fn (NavigationItem $item): ?string => $item->getGroup())
->map(function (Collection $items, ?string $groupIndex) use ($groups): NavigationGroup {
$parentItems = $items->groupBy(fn (NavigationItem $item): ?string => $item->getParentItem());
$items = $parentItems->get('')
->keyBy(fn (NavigationItem $item): string => $item->getLabel());
$parentItems->except([''])->each(function (Collection $parentItemItems, string $parentItemLabel) use ($items) {
if (! $items->has($parentItemLabel)) {
return;
}
$items->get($parentItemLabel)->childItems($parentItemItems);
});
if (blank($groupIndex)) {
return NavigationGroup::make()->items($items);
}
$registeredGroup = $groups
->first(function (NavigationGroup | string $registeredGroup, string | int $registeredGroupIndex) use ($groupIndex) {
if ($registeredGroupIndex === $groupIndex) {
return true;
}
if ($registeredGroup === $groupIndex) {
return true;
}
if (! $registeredGroup instanceof NavigationGroup) {
return false;
}
return $registeredGroup->getLabel() === $groupIndex;
});
if ($registeredGroup instanceof NavigationGroup) {
return $registeredGroup->items($items);
}
return NavigationGroup::make($registeredGroup ?? $groupIndex)
->items($items);
})
->sortBy(function (NavigationGroup $group, ?string $groupIndex): int {
if (blank($group->getLabel())) {
return -1;
}
$registeredGroups = $this->getNavigationGroups();
$groupsToSearch = $registeredGroups;
if (Arr::first($registeredGroups) instanceof NavigationGroup) {
$groupsToSearch = [
...array_keys($registeredGroups),
...array_map(fn (NavigationGroup $registeredGroup): string => $registeredGroup->getLabel(), array_values($registeredGroups)),
];
}
$sort = array_search(
$groupIndex,
$groupsToSearch,
);
if ($sort === false) {
return count($registeredGroups);
}
return $sort;
})
->all();
}
/**
@@ -4,6 +4,7 @@ namespace Filament\Panel\Concerns;
use Closure;
use Filament\Facades\Filament;
use Filament\Navigation\NavigationManager;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Laravel\SerializableClosure\Serializers\Native;
@@ -155,19 +156,11 @@ trait HasRoutes
if ($tenant) {
$originalTenant = Filament::getTenant();
Filament::setTenant($tenant, isQuiet: true);
$isNavigationMountedOriginally = $this->isNavigationMounted;
$originalNavigationItems = $this->navigationItems;
$originalNavigationGroups = $this->navigationGroups;
$this->isNavigationMounted = false;
$this->navigationItems = [];
$this->navigationGroups = [];
$navigation = $this->getNavigation();
}
$navigation = $this->getNavigation();
$this->navigationManager = new NavigationManager();
$navigation = $this->navigationManager->get();
try {
$firstGroup = Arr::first($navigation);
@@ -186,11 +179,9 @@ trait HasRoutes
} finally {
if ($tenant) {
Filament::setTenant($originalTenant, isQuiet: true);
$this->isNavigationMounted = $isNavigationMountedOriginally;
$this->navigationItems = $originalNavigationItems;
$this->navigationGroups = $originalNavigationGroups;
}
$this->navigationManager = null;
}
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace Filament;
use Filament\Exceptions\NoDefaultPanelSetException;
use Illuminate\Support\Arr;
class PanelRegistry
{
/**
* @var array<string, Panel>
*/
public array $panels = [];
public function register(Panel $panel): void
{
$this->panels[$panel->getId()] = $panel;
$panel->register();
if (! $panel->isDefault()) {
return;
}
if (app()->resolved('filament')) {
app('filament')->setCurrentPanel($panel);
}
app()->resolving(
'filament',
fn (FilamentManager $manager) => $manager->setCurrentPanel($panel),
);
}
/**
* @throws NoDefaultPanelSetException
*/
public function getDefault(): Panel
{
return Arr::first(
$this->panels,
fn (Panel $panel): bool => $panel->isDefault(),
fn () => throw NoDefaultPanelSetException::make(),
);
}
public function get(?string $id = null): Panel
{
return $this->panels[$id] ?? $this->getDefault();
}
/**
* @return array<string, Panel>
*/
public function all(): array
{
return $this->panels;
}
}
+12 -7
View File
@@ -613,13 +613,8 @@ abstract class Resource
public static function routes(Panel $panel): void
{
$slug = static::getSlug();
$routeBaseName = (string) str($slug)
->replace('/', '.')
->append('.');
Route::name($routeBaseName)
->prefix($slug)
Route::name(static::getRelativeRouteName() . '.')
->prefix(static::getRoutePrefix())
->middleware(static::getRouteMiddleware($panel))
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
->group(function () use ($panel) {
@@ -629,6 +624,16 @@ abstract class Resource
});
}
public static function getRelativeRouteName(): string
{
return (string) str(static::getSlug())->replace('/', '.');
}
public static function getRoutePrefix(): string
{
return static::getSlug();
}
/**
* @return string | array<string>
*/

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