-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds Reversible Forms to Prompts (#118)
* Work in progress * Adds form support to Prompts * Work in progress * Work in progress * Feedback changes * Feedback changes * Feedback changes * Work in progress * Work in progress
- Loading branch information
1 parent
e6f70bc
commit 1ae61d1
Showing
18 changed files
with
700 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
<?php | ||
|
||
use function Laravel\Prompts\confirm; | ||
use function Laravel\Prompts\form; | ||
use function Laravel\Prompts\info; | ||
use function Laravel\Prompts\note; | ||
use function Laravel\Prompts\spin; | ||
|
||
require __DIR__.'/../vendor/autoload.php'; | ||
|
||
$responses = form() | ||
->intro('Welcome to Laravel') | ||
->suggest( | ||
label: 'What is your name?', | ||
placeholder: 'E.g. Taylor Otwell', | ||
options: [ | ||
'Dries Vints', | ||
'Guus Leeuw', | ||
'James Brooks', | ||
'Jess Archer', | ||
'Joe Dixon', | ||
'Mior Muhammad Zaki Mior Khairuddin', | ||
'Nuno Maduro', | ||
'Taylor Otwell', | ||
'Tim MacDonald', | ||
], | ||
validate: fn ($value) => match (true) { | ||
! $value => 'Please enter your name.', | ||
default => null, | ||
}, | ||
) | ||
->text( | ||
label: 'Where should we create your project?', | ||
placeholder: 'E.g. ./laravel', | ||
validate: fn ($value) => match (true) { | ||
! $value => 'Please enter a path', | ||
$value[0] !== '.' => 'Please enter a relative path', | ||
default => null, | ||
}, | ||
name: 'path' | ||
) | ||
->textarea('Describe your project') | ||
->pause() | ||
->submit(); | ||
|
||
$moreResponses = form() | ||
->password( | ||
label: 'Provide a password', | ||
validate: fn ($value) => match (true) { | ||
! $value => 'Please enter a password.', | ||
strlen($value) < 5 => 'Password should have at least 5 characters.', | ||
default => null, | ||
}, | ||
) | ||
->select( | ||
label: 'Pick a project type', | ||
default: 'ts', | ||
options: [ | ||
'ts' => 'TypeScript', | ||
'js' => 'JavaScript', | ||
], | ||
) | ||
->multiselect( | ||
label: 'Select additional tools.', | ||
default: ['pint', 'eslint'], | ||
options: [ | ||
'pint' => 'Pint', | ||
'eslint' => 'ESLint', | ||
'prettier' => 'Prettier', | ||
], | ||
validate: function ($values) { | ||
if (count($values) === 0) { | ||
return 'Please select at least one tool.'; | ||
} | ||
} | ||
) | ||
->add(function () { | ||
$install = confirm( | ||
label: 'Install dependencies?', | ||
); | ||
|
||
if ($install) { | ||
spin(fn () => sleep(3), 'Installing dependencies...'); | ||
} | ||
|
||
return $install; | ||
}, name: 'install') | ||
->confirm('Finish installation?') | ||
->add(fn ($responses) => note(<<<EOT | ||
Installation complete! | ||
To get started, run: | ||
cd {$responses['path']} | ||
php artisan serve | ||
EOT | ||
)) | ||
->submit(); | ||
|
||
var_dump($responses, $moreResponses); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
|
||
namespace Laravel\Prompts\Exceptions; | ||
|
||
use RuntimeException; | ||
|
||
class FormRevertedException extends RuntimeException | ||
{ | ||
// | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
<?php | ||
|
||
namespace Laravel\Prompts; | ||
|
||
use Closure; | ||
use Illuminate\Support\Collection; | ||
use Laravel\Prompts\Exceptions\FormRevertedException; | ||
|
||
class FormBuilder | ||
{ | ||
/** | ||
* Each step that should be executed. | ||
* | ||
* @var array<int, \Laravel\Prompts\FormStep> | ||
*/ | ||
protected array $steps = []; | ||
|
||
/** | ||
* The responses provided by each step. | ||
* | ||
* @var array<mixed> | ||
*/ | ||
protected array $responses = []; | ||
|
||
/** | ||
* Add a new step. | ||
*/ | ||
public function add(Closure $step, ?string $name = null, bool $ignoreWhenReverting = false): self | ||
{ | ||
$this->steps[] = new FormStep($step, true, $name, $ignoreWhenReverting); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Run all of the given steps. | ||
* | ||
* @return array<mixed> | ||
*/ | ||
public function submit(): array | ||
{ | ||
$index = 0; | ||
$wasReverted = false; | ||
|
||
while ($index < count($this->steps)) { | ||
$step = $this->steps[$index]; | ||
|
||
if ($wasReverted && $index > 0 && $step->shouldIgnoreWhenReverting($this->responses)) { | ||
$index--; | ||
|
||
continue; | ||
} | ||
|
||
$wasReverted = false; | ||
|
||
$index > 0 | ||
? Prompt::revertUsing(function () use (&$wasReverted) { | ||
$wasReverted = true; | ||
}) : Prompt::preventReverting(); | ||
|
||
try { | ||
$this->responses[$step->name ?? $index] = $step->run( | ||
$this->responses, | ||
$this->responses[$step->name ?? $index] ?? null, | ||
); | ||
} catch (FormRevertedException) { | ||
$wasReverted = true; | ||
} | ||
|
||
$wasReverted ? $index-- : $index++; | ||
} | ||
|
||
Prompt::preventReverting(); | ||
|
||
return $this->responses; | ||
} | ||
|
||
/** | ||
* Prompt the user for text input. | ||
*/ | ||
public function text(string $label, string $placeholder = '', string $default = '', bool|string $required = false, mixed $validate = null, string $hint = '', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(text(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Prompt the user for multiline text input. | ||
*/ | ||
public function textarea(string $label, string $placeholder = '', string $default = '', bool|string $required = false, ?Closure $validate = null, string $hint = '', int $rows = 5, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(textarea(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Prompt the user for input, hiding the value. | ||
*/ | ||
public function password(string $label, string $placeholder = '', bool|string $required = false, mixed $validate = null, string $hint = '', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(password(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Prompt the user to select an option. | ||
* | ||
* @param array<int|string, string>|Collection<int|string, string> $options | ||
* @param true|string $required | ||
*/ | ||
public function select(string $label, array|Collection $options, int|string|null $default = null, int $scroll = 5, mixed $validate = null, string $hint = '', bool|string $required = true, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(select(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Prompt the user to select multiple options. | ||
* | ||
* @param array<int|string, string>|Collection<int|string, string> $options | ||
* @param array<int|string>|Collection<int, int|string> $default | ||
*/ | ||
public function multiselect(string $label, array|Collection $options, array|Collection $default = [], int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = 'Use the space bar to select options.', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(multiselect(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Prompt the user to confirm an action. | ||
*/ | ||
public function confirm(string $label, bool $default = true, string $yes = 'Yes', string $no = 'No', bool|string $required = false, mixed $validate = null, string $hint = '', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(confirm(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Prompt the user to continue or cancel after pausing. | ||
*/ | ||
public function pause(string $message = 'Press enter to continue...', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(pause(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Prompt the user for text input with auto-completion. | ||
* | ||
* @param array<string>|Collection<int, string>|Closure(string): array<string> $options | ||
*/ | ||
public function suggest(string $label, array|Collection|Closure $options, string $placeholder = '', string $default = '', int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = '', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(suggest(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Allow the user to search for an option. | ||
* | ||
* @param Closure(string): array<int|string, string> $options | ||
* @param true|string $required | ||
*/ | ||
public function search(string $label, Closure $options, string $placeholder = '', int $scroll = 5, mixed $validate = null, string $hint = '', bool|string $required = true, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(search(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Allow the user to search for multiple option. | ||
* | ||
* @param Closure(string): array<int|string, string> $options | ||
*/ | ||
public function multisearch(string $label, Closure $options, string $placeholder = '', int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = 'Use the space bar to select options.', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(multisearch(...), get_defined_vars()); | ||
} | ||
|
||
/** | ||
* Render a spinner while the given callback is executing. | ||
* | ||
* @param \Closure(): mixed $callback | ||
*/ | ||
public function spin(Closure $callback, string $message = '', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(spin(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display a note. | ||
*/ | ||
public function note(string $message, ?string $type = null, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(note(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display an error. | ||
*/ | ||
public function error(string $message, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(error(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display a warning. | ||
*/ | ||
public function warning(string $message, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(warning(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display an alert. | ||
*/ | ||
public function alert(string $message, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(alert(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display an informational message. | ||
*/ | ||
public function info(string $message, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(info(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display an introduction. | ||
*/ | ||
public function intro(string $message, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(intro(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display a closing message. | ||
*/ | ||
public function outro(string $message, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(outro(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display a table. | ||
* | ||
* @param array<int, string|array<int, string>>|Collection<int, string|array<int, string>> $headers | ||
* @param array<int, array<int, string>>|Collection<int, array<int, string>> $rows | ||
*/ | ||
public function table(array|Collection $headers = [], array|Collection|null $rows = null, ?string $name = null): self | ||
{ | ||
return $this->runPrompt(table(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Display a progress bar. | ||
* | ||
* @template TSteps of iterable<mixed>|int | ||
* @template TReturn | ||
* | ||
* @param TSteps $steps | ||
* @param ?Closure((TSteps is int ? int : value-of<TSteps>), Progress<TSteps>): TReturn $callback | ||
*/ | ||
public function progress(string $label, iterable|int $steps, ?Closure $callback = null, string $hint = '', ?string $name = null): self | ||
{ | ||
return $this->runPrompt(progress(...), get_defined_vars(), true); | ||
} | ||
|
||
/** | ||
* Execute the given prompt passing the given arguments. | ||
* | ||
* @param array<mixed> $arguments | ||
*/ | ||
protected function runPrompt(callable $prompt, array $arguments, bool $ignoreWhenReverting = false): self | ||
{ | ||
return $this->add(function (array $responses, mixed $previousResponse) use ($prompt, $arguments) { | ||
unset($arguments['name']); | ||
|
||
if (array_key_exists('default', $arguments) && $previousResponse !== null) { | ||
$arguments['default'] = $previousResponse; | ||
} | ||
|
||
return $prompt(...$arguments); | ||
}, name: $arguments['name'], ignoreWhenReverting: $ignoreWhenReverting); | ||
} | ||
} |
Oops, something went wrong.