mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
More Profile Feature WIP. Next up - tests
This commit is contained in:
+1
-2
@@ -17,7 +17,6 @@ return [
|
||||
'features' => [
|
||||
Features::registration(),
|
||||
// Features::profile(),
|
||||
// Features::permissions(),
|
||||
Features::resources(),
|
||||
],
|
||||
|
||||
@@ -138,7 +137,7 @@ return [
|
||||
*/
|
||||
|
||||
'cache_disk' => env('FILAMENT_CACHE_DISK', 'local'),
|
||||
'cache_path' => '.cache',
|
||||
'cache_path' => 'filament-tmp',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"/js/filament.js": "/js/filament.js?id=9995e728ce8648b81f5d",
|
||||
"/css/filament.css": "/css/filament.css?id=154a0f553a2b1539829a",
|
||||
"/css/filament.css": "/css/filament.css?id=59ae2ccb0284510a04e7",
|
||||
"/js/filament.js.map": "/js/filament.js.map?id=1d3615b89069bfd79261",
|
||||
"/css/filament.css.map": "/css/filament.css.map?id=227394566b95aa646612"
|
||||
"/css/filament.css.map": "/css/filament.css.map?id=e875f899bca9d385fc36"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.btn {
|
||||
@apply text-sm font-medium py-2 px-4 bg-gray-100 text-gray-600 rounded transition duration-200 shadow-sm inline-block relative focus:ring focus:ring-indigo-200 focus:ring-opacity-50 hover:bg-gray-200;
|
||||
@apply cursor-pointer text-sm font-semibold py-2 px-4 border border-gray-300 bg-gray-100 text-gray-800 rounded transition duration-200 shadow-sm inline-block relative focus:ring focus:ring-indigo-200 focus:ring-opacity-50 hover:bg-gray-200;
|
||||
|
||||
&-primary {
|
||||
@apply bg-blue-700 text-white hover:bg-blue-800;
|
||||
@apply border-transparent bg-blue-700 text-white hover:bg-blue-800;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@import './utilities/typography.css';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.help,
|
||||
.hint {
|
||||
@apply text-xs font-normal leading-tight text-gray-500;
|
||||
|
||||
a {
|
||||
@apply text-blue-600 hover:text-blue-800;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
@apply font-mono;
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
<span {{ $attributes->merge(['class' => 'text-xs font-normal leading-tight text-gray-500']) }}>
|
||||
<span {{ $attributes->merge(['class' => 'help']) }}>
|
||||
{{ $slot }}
|
||||
</span>
|
||||
@@ -1,3 +1,3 @@
|
||||
<span {{ $attributes->merge(['class' => 'font-normal font-mono text-xs leading-tight text-gray-500']) }}>
|
||||
<span {{ $attributes->merge(['class' => 'hint']) }}>
|
||||
{{ $slot }}
|
||||
</span>
|
||||
@@ -0,0 +1,42 @@
|
||||
@props([
|
||||
'field',
|
||||
'avatar',
|
||||
'user',
|
||||
'size' => 64,
|
||||
'class' => '',
|
||||
'delete' => null,
|
||||
'id',
|
||||
])
|
||||
|
||||
<div x-cloak
|
||||
x-data="{ isUploading: false, progress: 0 }"
|
||||
x-on:livewire-upload-start="isUploading = true"
|
||||
x-on:livewire-upload-finish="isUploading = false"
|
||||
x-on:livewire-upload-error="isUploading = false"
|
||||
x-on:livewire-upload-progress="progress = $event.detail.progress"
|
||||
class="flex items-center space-x-4">
|
||||
<div class="flex-shrink-0 relative">
|
||||
<label for="{{ $id }}" class="cursor-pointer rounded-full shadow flex overflow-hidden {{ $class }}">
|
||||
@if ($avatar)
|
||||
<img src="{{ $avatar->temporaryUrl() }}" alt="{{ $user->name }}" class="h-full object-cover" width="{{ $size }}" height="{{ $size }}" loading="lazy">
|
||||
@else
|
||||
<x-filament-avatar :size="$size" :user="$user" />
|
||||
@endif
|
||||
</label>
|
||||
@if ($user->avatar && $delete)
|
||||
<button type="button"
|
||||
wire:click="{{ $delete }}"
|
||||
class="absolute top-0 right-0 w-4 h-4 rounded-full bg-gray-800 text-white flex items-center justify-center hover:bg-red-700">
|
||||
<span class="sr-only">{{ __('Delete Avatar') }}</span>
|
||||
<x-heroicon-o-x class="w-3 h-3" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex-grow relative">
|
||||
<label class="btn" for="{{ $id }}">{{ __('Change') }}</label>
|
||||
<input type="file" wire:model="{{ $field }}" class="sr-only" id="{{ $id }}" {{ $attributes }}>
|
||||
<div x-show="isUploading" :aria-hidden="!isUploading" class="absolute bottom-0 -mb-3 w-48 max-w-full">
|
||||
<x-filament::progress progress="progress" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
@props([
|
||||
'max' => 100,
|
||||
'progress',
|
||||
])
|
||||
<div {{ $attributes }}>
|
||||
<progress max="{{ $max }}" x-bind:value="{{ $progress }}" class="sr-only"></progress>
|
||||
<div class="rounded-full border border-gray-200 overflow-hidden" aria-hidden="true">
|
||||
<div class="h-1 bg-gray-500" :style="`width: ${ {{ $progress }} }%`"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,16 +7,11 @@
|
||||
<x-filament::input type="email" field="user.email" wire:model.lazy="user.email" id="email" required autocomplete="email" tabindex="2" />
|
||||
</x-filament::input-group-stacked>
|
||||
|
||||
<x-filament::input-group-stacked field="avatar" for="photo" :label="__('Photo')">
|
||||
<div>
|
||||
<input type="file" wire:model="avatar" id="photo" tabindex="3" />
|
||||
</div>
|
||||
<x-filament::input-group-stacked field="avatar" for="photo" :label="__('Avatar')">
|
||||
<x-filament::input-avatar field="avatar" :avatar="$avatar" :user="$user" delete="deleteAvatar" id="photo" tabindex="3" />
|
||||
<x-slot name="hint">
|
||||
{{ __('Optional') }}
|
||||
</x-slot>
|
||||
<x-slot name="help">
|
||||
{{ __('Your user avatar photo.') }}
|
||||
</x-slot>
|
||||
</x-filament::input-group-stacked>
|
||||
|
||||
<x-filament::fieldset :legend="__('Update Password')" cols="2">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<x-filament::input type="email" field="email" wire:model.defer="email" id="email" required autocomplete="email" autofocus tabindex="1" />
|
||||
@if (Route::has('filament.register'))
|
||||
<x-slot name="hint">
|
||||
<a href="{{ route('filament.register') }}" class="text-current hover:text-blue-600" tabindex="6">
|
||||
<a href="{{ route('filament.register') }}" tabindex="6">
|
||||
{{ __('filament::auth.register') }}
|
||||
</a>
|
||||
</x-slot>
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<x-filament::input-group-stacked field="password" for="password" :label="__('Password')" required>
|
||||
<x-slot name="hint">
|
||||
<a href="{{ route('filament.password.forgot') }}" class="text-current hover:text-blue-600" tabindex="5">
|
||||
<a href="{{ route('filament.password.forgot') }}" tabindex="5">
|
||||
{{ __('Forgot Your Password?') }}
|
||||
</a>
|
||||
</x-slot>
|
||||
|
||||
@@ -3,18 +3,8 @@
|
||||
<x-filament::tab id="account">
|
||||
{{ __('Account') }}
|
||||
</x-filament::tab>
|
||||
@if (Filament\Features::hasPermissions())
|
||||
<x-filament::tab id="permissions">
|
||||
{{ __('Permissions') }}
|
||||
</x-filament::tab>
|
||||
@endif
|
||||
</x-slot>
|
||||
<x-filament::tab-panel id="account">
|
||||
<livewire:filament-account :user="Auth::user()" />
|
||||
</x-filament::tab-panel>
|
||||
@if (Filament\Features::hasPermissions())
|
||||
<x-filament::tab-panel id="permissions">
|
||||
Permissions...
|
||||
</x-filament::tab-panel>
|
||||
@endif
|
||||
</x-filament::tabs>
|
||||
@@ -45,16 +45,6 @@ class Features
|
||||
return static::enabled(static::resources());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the application has user permissions.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasPermissions()
|
||||
{
|
||||
return static::enabled(static::permissions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the user registration feature.
|
||||
*
|
||||
@@ -75,16 +65,6 @@ class Features
|
||||
return 'profile';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the user permissions feature.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function permissions()
|
||||
{
|
||||
return 'permissions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the resources feature.
|
||||
*
|
||||
|
||||
@@ -5,7 +5,11 @@ namespace Filament\Http\Livewire;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\{
|
||||
Hash,
|
||||
Storage,
|
||||
};
|
||||
|
||||
class Account extends Component
|
||||
{
|
||||
@@ -22,7 +26,6 @@ class Account extends Component
|
||||
'required',
|
||||
'email',
|
||||
],
|
||||
'avatar' => 'nullable|image|max:20',
|
||||
'password' => 'nullable',
|
||||
];
|
||||
|
||||
@@ -39,12 +42,39 @@ class Account extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatedAvatar($value)
|
||||
{
|
||||
$extension = pathinfo($value->getFilename(), PATHINFO_EXTENSION);
|
||||
if (!in_array($extension, ['png', 'jpeg', 'bmp', 'gif'])) {
|
||||
$this->reset('avatar');
|
||||
}
|
||||
|
||||
$this->validate([
|
||||
'avatar' => 'mimes:png,jpeg,bmp,gif|max:512', // .5MB Max
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteAvatar()
|
||||
{
|
||||
$avatar = $this->user->avatar;
|
||||
|
||||
if ($avatar) {
|
||||
Storage::disk(config('filament.storage_disk'))->delete($avatar);
|
||||
$this->avatar = null;
|
||||
|
||||
$this->user->avatar = null;
|
||||
$this->user->save();
|
||||
|
||||
$this->dispatchBrowserEvent('notify', __('Avatar removed for :name', ['name' => $this->user->name]));
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
if ($this->avatar) {
|
||||
dd($this->avatar);
|
||||
$this->user->avatar = $this->avatar->store('avatars', config('filament.storage_disk'));
|
||||
}
|
||||
|
||||
if ($this->password) {
|
||||
|
||||
@@ -24,6 +24,10 @@ class Login extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (Auth::check()) {
|
||||
return redirect()->to(Filament::home());
|
||||
}
|
||||
|
||||
$this->message = session('message');
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,14 @@ class RouteServiceProvider extends ServiceProvider
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
protected function prefix()
|
||||
{
|
||||
return config('filament.prefix.route');
|
||||
}
|
||||
|
||||
public function map(): void
|
||||
{
|
||||
$this->mapWebRoutes();
|
||||
$this->mapAliasRoutes();
|
||||
}
|
||||
|
||||
protected function prefix()
|
||||
{
|
||||
return config('filament.prefix.route');
|
||||
}
|
||||
|
||||
protected function mapWebRoutes(): void
|
||||
@@ -35,13 +34,4 @@ class RouteServiceProvider extends ServiceProvider
|
||||
->group(__DIR__.'/../../routes/web.php');
|
||||
}
|
||||
}
|
||||
|
||||
protected function mapAliasRoutes(): void
|
||||
{
|
||||
if (!Route::has('login')) {
|
||||
Route::get('/login', function () {
|
||||
return redirect()->route('filament.login');
|
||||
})->name('login');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,14 @@ class LoginTest extends TestCase
|
||||
$this->assertAuthenticated();
|
||||
}
|
||||
|
||||
public function test_redirect_if_logged_in()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
Livewire::actingAs($user)
|
||||
->test(Login::class)
|
||||
->assertRedirect(Filament::home());
|
||||
}
|
||||
|
||||
public function test_email_is_required()
|
||||
{
|
||||
Livewire::test(Login::class)
|
||||
|
||||
Reference in New Issue
Block a user