From c3a56e5f7e28c9fe78b6b20820535895520eef55 Mon Sep 17 00:00:00 2001 From: Randall Wilk Date: Mon, 22 Jan 2024 12:57:43 -0600 Subject: [PATCH 1/3] Cleanup copy to clipboard action --- src/Actions/CopyToClipboardAction.php | 53 ++++++++++++ src/Concerns/CanCopyToClipboard.php | 99 +++-------------------- src/Password.php | 38 ++++----- tests/Concerns/CanCopyToClipboardTest.php | 21 ----- 4 files changed, 85 insertions(+), 126 deletions(-) create mode 100644 src/Actions/CopyToClipboardAction.php diff --git a/src/Actions/CopyToClipboardAction.php b/src/Actions/CopyToClipboardAction.php new file mode 100644 index 0000000..dd03402 --- /dev/null +++ b/src/Actions/CopyToClipboardAction.php @@ -0,0 +1,53 @@ +label(__('filament-password-input::password.actions.copy.tooltip')); + + $this->icon(FilamentIcon::resolve('filament-password-input::copy') ?? 'heroicon-m-clipboard'); + + $this->color('gray'); + + $this->alpineClickHandler(function (Component $component) { + $copyDuration = Js::from( + rescue( + callback: fn () => $component->getCopyMessageDuration(), + rescue: fn () => 2000, + report: false, + ) + ); + + $copyMessage = Js::from( + rescue( + callback: fn () => $component->getCopyMessage(), + rescue: fn () => __('filament::components/copyable.messages.copied'), + report: false, + ) + ); + + return <<getStatePath()}'); + window.navigator.clipboard.writeText(text); + \$tooltip({$copyMessage}, { theme: \$store.theme, timeout: {$copyDuration} }); + JS; + }); + } + + public static function getDefaultName(): ?string + { + return 'copyToClipboard'; + } +} diff --git a/src/Concerns/CanCopyToClipboard.php b/src/Concerns/CanCopyToClipboard.php index 56f8c3b..caeeec7 100644 --- a/src/Concerns/CanCopyToClipboard.php +++ b/src/Concerns/CanCopyToClipboard.php @@ -5,33 +5,30 @@ namespace Rawilk\FilamentPasswordInput\Concerns; use Closure; -use Filament\Forms\Components\Actions\Action; -use Illuminate\Support\HtmlString; -use Illuminate\Support\Js; +use Rawilk\FilamentPasswordInput\Actions\CopyToClipboardAction; /** - * This will behave very similar to filament's CanBeCopied trait. - * - * Big difference is we determine the state from the input's current - * value instead of defining it with php. + * @mixin \Filament\Forms\Components\TextInput */ trait CanCopyToClipboard { - protected bool|Closure $isCopyable = false; - protected string|Closure|null $copyMessage = null; protected int|Closure|null $copyMessageDuration = null; - protected string|Closure|null $copyIcon = null; - - protected string|Closure|array|null $copyIconColor = null; + public function copyable( + bool|Closure $condition = true, + string|array|Closure|null $color = null, + ): static { + $action = CopyToClipboardAction::make()->visible($condition); - protected string|Closure|null $copyTooltip = null; + if ($color) { + $action->color($color); + } - public function copyable(bool|Closure $condition = true): static - { - $this->isCopyable = $condition; + $this->suffixActions([ + $action, + ]); return $this; } @@ -50,32 +47,6 @@ public function copyMessageDuration(int|Closure|null $duration): static return $this; } - public function copyIcon(string|Closure|null $icon): static - { - $this->copyIcon = $icon; - - return $this; - } - - public function copyIconColor(string|Closure|array|null $color): static - { - $this->copyIconColor = $color; - - return $this; - } - - public function copyTooltip(string|Closure|null $tooltip): static - { - $this->copyTooltip = $tooltip; - - return $this; - } - - public function isCopyable(): bool - { - return (bool) $this->evaluate($this->isCopyable); - } - public function getCopyMessage(): string { return $this->evaluate($this->copyMessage) ?? __('filament::components/copyable.messages.copied'); @@ -85,48 +56,4 @@ public function getCopyMessageDuration(): int { return $this->evaluate($this->copyMessageDuration) ?? 2000; } - - public function getCopyIcon(): string - { - return $this->evaluate($this->copyIcon) ?? 'heroicon-m-clipboard'; - } - - public function getCopyIconColor(): string|array|null - { - return $this->evaluate($this->copyIconColor); - } - - public function getCopyTooltip(): string - { - return $this->evaluate($this->copyTooltip) ?? __('filament-password-input::password.actions.copy.tooltip'); - } - - public function getCopyToClipboardAction(): Action - { - $copyDuration = Js::from($this->getCopyMessageDuration()); - $copyMessage = Js::from($this->getCopyMessage()); - $tooltip = $this->getCopyTooltip(); - - $action = Action::make('copyToClipboard') - ->livewireClickHandlerEnabled(false) - ->icon($this->getCopyIcon()) - ->iconButton() - ->tooltip($tooltip) - ->label($tooltip) - ->extraAttributes([ - 'x-on:click' => new HtmlString(<<getStatePath()}'); - window.navigator.clipboard.writeText(text); - \$tooltip({$copyMessage}, { theme: \$store.theme, timeout: {$copyDuration} }); - JS), - // IMO it's weird when the title and tooltip show at the same time... - 'title' => '', - ]); - - if ($color = $this->getCopyIconColor()) { - $action->color($color); - } - - return $action; - } } diff --git a/src/Password.php b/src/Password.php index 469e2d2..0855621 100755 --- a/src/Password.php +++ b/src/Password.php @@ -53,23 +53,23 @@ public function shouldHidePasswordManagerIcons(): bool return (bool) $this->evaluate($this->hidePasswordManagerIcons); } - public function getSuffixActions(): array - { - if ($this->cachedSuffixActions) { - return $this->cachedSuffixActions; - } - - $isDisabled = $this->isDisabled(); - $isReadonly = $this->isReadOnly(); - - if (! $isDisabled && $this->isCopyable()) { - $this->suffixActions([$this->getCopyToClipboardAction()], $this->isSuffixInline); - } - - if (! ($isDisabled || $isReadonly) && $this->canRegeneratePassword()) { - $this->suffixActions([$this->getRegeneratePasswordAction()], $this->isSuffixInline); - } - - return parent::getSuffixActions(); - } + // public function getSuffixActions(): array + // { + // if ($this->cachedSuffixActions) { + // return $this->cachedSuffixActions; + // } + // + // $isDisabled = $this->isDisabled(); + // $isReadonly = $this->isReadOnly(); + // + // if (! $isDisabled && $this->isCopyable()) { + // $this->suffixActions([$this->getCopyToClipboardAction()], $this->isSuffixInline); + // } + // + // if (! ($isDisabled || $isReadonly) && $this->canRegeneratePassword()) { + // $this->suffixActions([$this->getRegeneratePasswordAction()], $this->isSuffixInline); + // } + // + // return parent::getSuffixActions(); + // } } diff --git a/tests/Concerns/CanCopyToClipboardTest.php b/tests/Concerns/CanCopyToClipboardTest.php index fcc3e42..f05b97b 100644 --- a/tests/Concerns/CanCopyToClipboardTest.php +++ b/tests/Concerns/CanCopyToClipboardTest.php @@ -34,27 +34,6 @@ expect($input->getCopyMessageDuration())->toBe(3000); }); -test('a custom icon can be used for the copy button', function () { - $input = Password::make('password') - ->copyIcon('my-icon'); - - expect($input->getCopyIcon())->toBe('my-icon'); -}); - -test('a custom color can be used for the copy button', function () { - $input = Password::make('password') - ->copyIconColor('success'); - - expect($input->getCopyIconColor())->toBe('success'); -}); - -test('a custom tooltip message can be used for the initial state of the button', function () { - $input = Password::make('password') - ->copyTooltip('my tooltip'); - - expect($input->getCopyTooltip())->toBe('my tooltip'); -}); - class CanCopyWithButton extends Livewire { public function form(Form $form): Form From b31136812369870c640b5acac484261d41c2ead1 Mon Sep 17 00:00:00 2001 From: Randall Wilk Date: Mon, 22 Jan 2024 13:35:21 -0600 Subject: [PATCH 2/3] Cleanup regenerate action --- src/Actions/RegeneratePasswordAction.php | 73 +++++++++ src/Concerns/CanRegeneratePassword.php | 148 ++++--------------- tests/Concerns/CanRegeneratePasswordTest.php | 59 -------- 3 files changed, 101 insertions(+), 179 deletions(-) create mode 100644 src/Actions/RegeneratePasswordAction.php diff --git a/src/Actions/RegeneratePasswordAction.php b/src/Actions/RegeneratePasswordAction.php new file mode 100644 index 0000000..2095621 --- /dev/null +++ b/src/Actions/RegeneratePasswordAction.php @@ -0,0 +1,73 @@ +label(__('filament-password-input::password.actions.regenerate.tooltip')); + + $this->icon(FilamentIcon::resolve('filament-password-input::regenerate') ?? 'heroicon-o-key'); + + $this->color('gray'); + + $this->successNotificationTitle(__('filament-password-input::password.actions.regenerate.success_message')); + + $this->action(function (Set $set, Component $component) { + $secret = $this->process(function (Component $component) { + $maxLength = rescue( + callback: fn () => $component->getNewPasswordLength() ?? static::DEFAULT_MAX_LENGTH, + rescue: fn () => static::DEFAULT_MAX_LENGTH, + report: false, + ); + + return Str::password(max(3, $maxLength)); + }); + + // Not sure if I'm doing something wrong here, but using ->getStatePath() + // doesn't work here, but using ->getName() sets the correct path + // to set the value. + $set($component->getName(), $secret); + + if ($this->shouldNotifyOnSuccess()) { + $this->success(); + } + }); + } + + public static function getDefaultName(): ?string + { + return 'regeneratePassword'; + } + + public function notifyOnSuccess(bool|Closure $condition = true): static + { + $this->notifyOnSuccess = $condition; + + return $this; + } + + public function shouldNotifyOnSuccess(): bool + { + return $this->evaluate($this->notifyOnSuccess) ?? true; + } +} diff --git a/src/Concerns/CanRegeneratePassword.php b/src/Concerns/CanRegeneratePassword.php index 755ed05..f1821f7 100644 --- a/src/Concerns/CanRegeneratePassword.php +++ b/src/Concerns/CanRegeneratePassword.php @@ -5,143 +5,51 @@ namespace Rawilk\FilamentPasswordInput\Concerns; use Closure; -use Filament\Forms\Components\Actions\Action; -use Filament\Forms\Set; -use Filament\Notifications\Notification; -use Illuminate\Support\Str; +use Rawilk\FilamentPasswordInput\Actions\RegeneratePasswordAction; +/** + * @mixin \Filament\Forms\Components\Component + */ trait CanRegeneratePassword { - protected bool|Closure $regeneratePassword = false; + protected int|Closure|null $newPasswordLength = null; - protected ?Closure $generatePasswordUsing = null; + public function regeneratePassword( + bool|Closure $condition = true, + string|array|Closure|null $color = null, + ?Closure $using = null, + bool|Closure|null $notify = null, + ): static { + $action = RegeneratePasswordAction::make()->visible($condition); - protected string|Closure|null $regeneratePasswordIcon = null; - - protected string|Closure|array|null $regeneratePasswordIconColor = null; - - protected string|Closure|null $regeneratePasswordTooltip = null; - - protected bool $notifyOnPasswordRegenerate = true; - - protected string|Closure|null $passwordRegeneratedMessage = null; - - public function regeneratePassword(bool|Closure $condition = true): static - { - $this->regeneratePassword = $condition; - - return $this; - } - - public function generatePasswordUsing(Closure $closure): static - { - $this->generatePasswordUsing = $closure; - - return $this; - } - - public function regeneratePasswordIcon(string|Closure|null $icon): static - { - $this->regeneratePasswordIcon = $icon; - - return $this; - } - - public function regeneratePasswordIconColor(string|Closure|array|null $color): static - { - $this->regeneratePasswordIconColor = $color; - - return $this; - } + if ($color) { + $action->color($color); + } - public function regeneratePasswordTooltip(string|Closure|null $tooltip): static - { - $this->regeneratePasswordTooltip = $tooltip; + if ($using) { + $action->using($using); + } - return $this; - } + if (filled($notify)) { + $action->notifyOnSuccess($notify); + } - public function notifyOnPasswordRegenerate(bool $condition): static - { - $this->notifyOnPasswordRegenerate = $condition; + $this->suffixActions([ + $action, + ]); return $this; } - public function passwordRegeneratedMessage(string|Closure|null $message): static + public function newPasswordLength(int|Closure|null $length = null): static { - $this->passwordRegeneratedMessage = $message; + $this->newPasswordLength = $length; return $this; } - public function canRegeneratePassword(): bool - { - return (bool) $this->evaluate($this->regeneratePassword); - } - - public function getRegeneratePasswordIcon(): string - { - return $this->evaluate($this->regeneratePasswordIcon) ?? 'heroicon-o-key'; - } - - public function getRegeneratePasswordIconColor(): string|array|null + public function getNewPasswordLength(): ?int { - return $this->evaluate($this->regeneratePasswordIconColor); - } - - public function getRegeneratePasswordTooltip(): string - { - return $this->evaluate($this->regeneratePasswordTooltip) ?? __('filament-password-input::password.actions.regenerate.tooltip'); - } - - public function getPasswordRegeneratedMessage(): string - { - return $this->evaluate($this->passwordRegeneratedMessage) ?? __('filament-password-input::password.actions.regenerate.success_message'); - } - - public function generateNewSecret(mixed $state): string - { - $callback = $this->generatePasswordUsing; - if (is_callable($callback)) { - return $this->evaluate($callback, [ - 'state' => $state, - ]); - } - - $maxLength = $this->getMaxLength() ?? 32; - - return Str::password(max(3, $maxLength)); - } - - public function getRegeneratePasswordAction(): Action - { - $tooltip = $this->getRegeneratePasswordTooltip(); - - $action = Action::make('regeneratePassword') - ->icon($this->getRegeneratePasswordIcon()) - ->iconButton() - ->tooltip($tooltip) - ->label($tooltip) - ->extraAttributes([ - // IMO it's weird when the title and tooltip show at the same time... - 'title' => '', - ]) - ->action(function (Set $set, $state) { - $set($this->getName(), $this->generateNewSecret($state)); - - if ($this->notifyOnPasswordRegenerate) { - Notification::make() - ->title($this->getPasswordRegeneratedMessage()) - ->success() - ->send(); - } - }); - - if ($color = $this->getRegeneratePasswordIconColor()) { - $action->color($color); - } - - return $action; + return $this->evaluate($this->newPasswordLength) ?? $this->getMaxLength(); } } diff --git a/tests/Concerns/CanRegeneratePasswordTest.php b/tests/Concerns/CanRegeneratePasswordTest.php index b93453c..036e864 100644 --- a/tests/Concerns/CanRegeneratePasswordTest.php +++ b/tests/Concerns/CanRegeneratePasswordTest.php @@ -20,65 +20,6 @@ }); }); -test('a custom icon can be used for the button', function () { - $input = Password::make('password') - ->regeneratePasswordIcon('my-icon'); - - expect($input->getRegeneratePasswordIcon())->toBe('my-icon') - ->and($input->getRegeneratePasswordAction()->getIcon())->toBe('my-icon'); -}); - -test('a custom color can be specified for the button', function () { - $input = Password::make('password') - ->regeneratePasswordIconColor('success'); - - expect($input->getRegeneratePasswordIconColor())->toBe('success') - ->and($input->getRegeneratePasswordAction()->getColor())->toBe('success'); -}); - -test('a custom tooltip can be provided', function () { - $input = Password::make('password') - ->regeneratePasswordTooltip('my tooltip'); - - $action = $input->getRegeneratePasswordAction(); - - expect($input->getRegeneratePasswordTooltip())->toBe('my tooltip') - ->and($action->getTooltip())->toBe('my tooltip') - ->and($action->getLabel())->toBe('my tooltip'); -}); - -test('a custom message can be used for the notification when a new password is generated', function () { - $input = Password::make('password') - ->passwordRegeneratedMessage('my message'); - - expect($input->getPasswordRegeneratedMessage())->toBe('my message'); -}); - -it('accepts a custom callback to generate a new password with', function () { - $input = Password::make('password') - ->generatePasswordUsing(fn () => 'my new password'); - - expect($input->generateNewSecret(null))->toBe('my new password'); -}); - -it("respects the input's maxlength when generating a new password with the default generator", function () { - $input = Password::make('password') - ->maxlength(3); - - expect($input->generateNewSecret(null))->toHaveLength(3); -}); - -test('a minimum of 3 characters is required when using the default password generator helper on the string class', function (int $minLength) { - $input = Password::make('password') - ->maxLength($minLength); - - expect($input->generateNewSecret(null))->toHaveLength(3); -})->with([ - 0, - 1, - 2, -]); - class CanRegenerateWithButton extends Livewire { public function form(Form $form): Form From 901497ebb211fdbaba4c6a228cdc71f1319edd39 Mon Sep 17 00:00:00 2001 From: Randall Wilk Date: Mon, 22 Jan 2024 13:35:42 -0600 Subject: [PATCH 3/3] Remove dead code --- src/Password.php | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/Password.php b/src/Password.php index 0855621..bc3f2c0 100755 --- a/src/Password.php +++ b/src/Password.php @@ -52,24 +52,4 @@ public function shouldHidePasswordManagerIcons(): bool { return (bool) $this->evaluate($this->hidePasswordManagerIcons); } - - // public function getSuffixActions(): array - // { - // if ($this->cachedSuffixActions) { - // return $this->cachedSuffixActions; - // } - // - // $isDisabled = $this->isDisabled(); - // $isReadonly = $this->isReadOnly(); - // - // if (! $isDisabled && $this->isCopyable()) { - // $this->suffixActions([$this->getCopyToClipboardAction()], $this->isSuffixInline); - // } - // - // if (! ($isDisabled || $isReadonly) && $this->canRegeneratePassword()) { - // $this->suffixActions([$this->getRegeneratePasswordAction()], $this->isSuffixInline); - // } - // - // return parent::getSuffixActions(); - // } }