diff --git a/.editorconfig b/.editorconfig index a15e76db9..d215edcd7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,3 +19,6 @@ indent_size = 2 [*.config.js] indent_size = 2 + +[generated.d.ts] +indent_size = 2 diff --git a/demo/app/Sharp/Authors/AuthorList.php b/demo/app/Sharp/Authors/AuthorList.php index 16ed2b25b..f928f8ca4 100644 --- a/demo/app/Sharp/Authors/AuthorList.php +++ b/demo/app/Sharp/Authors/AuthorList.php @@ -80,17 +80,7 @@ function (Builder $builder) { } }, ) - - // Handle sorting - ->when( - $this->queryParams->sortedBy() === 'email', - function (Builder $builder) { - $builder->orderBy('email', $this->queryParams->sortedDir()); - }, - function (Builder $builder) { - $builder->orderBy('name', $this->queryParams->sortedDir() ?: 'asc'); - }, - ); + ->orderBy($this->queryParams->sortedBy(), $this->queryParams->sortedDir()); return $this ->setCustomTransformer('avatar', (new SharpUploadModelThumbnailUrlTransformer(100))->renderAsImageTag()) @@ -101,6 +91,15 @@ function (Builder $builder) { default => 'Unknown', }; }) + ->setCustomTransformer('email', function ($value, User $user) { + return $user->hasVerifiedEmail() + ? $value + : sprintf( + '
%s
%s pending invitation...
', + $value, + svg('far-envelope')->toHtml() + ); + }) ->transform($users->get()); } } diff --git a/demo/app/Sharp/Authors/Commands/InviteUserCommand.php b/demo/app/Sharp/Authors/Commands/InviteUserCommand.php index f47a33aa4..c1049e363 100644 --- a/demo/app/Sharp/Authors/Commands/InviteUserCommand.php +++ b/demo/app/Sharp/Authors/Commands/InviteUserCommand.php @@ -2,9 +2,11 @@ namespace App\Sharp\Authors\Commands; +use App\Models\User; use Code16\Sharp\EntityList\Commands\EntityCommand; use Code16\Sharp\Form\Fields\SharpFormTextField; use Code16\Sharp\Utils\Fields\FieldsContainer; +use Illuminate\Support\Str; class InviteUserCommand extends EntityCommand { @@ -23,20 +25,33 @@ public function buildCommandConfig(): void public function buildFormFields(FieldsContainer $formFields): void { - $formFields->addField( - SharpFormTextField::make('email') - ->setLabel('Email'), - ); + $formFields + ->addField( + SharpFormTextField::make('email') + ->setLabel('Email'), + ) + ->addField( + SharpFormTextField::make('name') + ->setLabel('Name'), + ); } public function execute(array $data = []): array { $this->validate($data, [ - 'email' => ['required', 'email'], + 'email' => ['required', 'email', 'max:100', 'unique:users,email'], + 'name' => ['required', 'string', 'max:100'], + ]); + + User::create([ + 'email' => $data['email'], + 'name' => $data['name'], + 'password' => bcrypt(Str::random()), + 'role' => 'editor', ]); // Here we send an invitation, or something - return $this->info('Invitation sent!'); + return $this->info('Invitation sent!', reload: true); } } diff --git a/demo/app/Sharp/Authors/Commands/VisitFacebookProfileCommand.php b/demo/app/Sharp/Authors/Commands/VisitFacebookProfileCommand.php index 2652ca017..8245784b8 100644 --- a/demo/app/Sharp/Authors/Commands/VisitFacebookProfileCommand.php +++ b/demo/app/Sharp/Authors/Commands/VisitFacebookProfileCommand.php @@ -2,22 +2,30 @@ namespace App\Sharp\Authors\Commands; +use App\Models\User; use Code16\Sharp\EntityList\Commands\InstanceCommand; class VisitFacebookProfileCommand extends InstanceCommand { public function label(): string { - return "Visit author's facebook profile"; + return 'Visit author’s facebook profile'; } public function buildCommandConfig(): void { - $this->configureDescription('You will leave sharp'); + $this->configureDescription('You will leave Sharp'); } public function execute(mixed $instanceId, array $data = []): array { return $this->link('https://facebook.com'); } + + public function authorizeFor(mixed $instanceId): bool + { + return sharp()->context() + ->findListInstance($instanceId, fn ($id) => User::find($id)) + ->hasVerifiedEmail(); + } } diff --git a/docs/guide/commands.md b/docs/guide/commands.md index b6ec3968d..2812d326c 100644 --- a/docs/guide/commands.md +++ b/docs/guide/commands.md @@ -171,7 +171,7 @@ Here is the full list of available methods: Finally, let's review the return possibilities: after a Command has been executed, the code must return something to tell to the front what to do next. There are height of them: -- `return $this->info('some text')`: displays the entered text in a modal. +- `return $this->info('some text', reload: true)`: displays the entered text in a modal. The second argument, optional (default is `false`), is a boolean to also mark Sharp to reload the page. - `return $this->reload()`: reload the current page (with context). - `return $this->refresh(1)`*: refresh only the instance with an id on `1`. We can pass an id array also to refresh more than one instance. - `return $this->view('view.name', ['some'=>'params'])`: display a view right in Sharp; useful for page previews. diff --git a/resources/js/commands/CommandManager.ts b/resources/js/commands/CommandManager.ts index b9b8e784f..c5ea2f847 100644 --- a/resources/js/commands/CommandManager.ts +++ b/resources/js/commands/CommandManager.ts @@ -41,11 +41,15 @@ export class CommandManager { get defaultCommandResponseHandlers(): CommandResponseHandlers { return { - info: async ({ message }, { formModal }) => { + info: async ({ message, reload }, { formModal }) => { await showAlert(message, { title: __('sharp::modals.command.info.title'), }); - formModal.shouldReopen && formModal.reloadAndReopen(); + if(formModal.shouldReopen) { + formModal.reloadAndReopen(); + } else if(reload) { + await this.handleCommandResponse({ action: 'reload' }); + } }, link: ({ link }, { formModal }) => { if(formModal.shouldReopen) { diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index fbca4aac4..3c737415b 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -55,7 +55,7 @@ export type CommandFormData = { }; export type CommandResponseData = | { action: "link"; link: string } - | { action: "info"; message: string } + | { action: "info"; message: string; reload: boolean } | { action: "refresh"; items?: Array<{ [key: string]: any }> } | { action: "reload" } | { action: "step"; step: string } @@ -601,13 +601,13 @@ export type FormUploadFieldValueData = { } | null; nativeFile?: File; }; -export type GlobalSearchData = { - config: { placeholder: string }; -}; export type GlobalFiltersData = { config: { filters: ConfigFiltersData }; filterValues: FilterValuesData; }; +export type GlobalSearchData = { + config: { placeholder: string }; +}; export type GraphWidgetData = { value?: { key: string; @@ -830,8 +830,8 @@ export type ShowListFieldData = { key: string; type: "list"; emptyVisible: boolean; - label: string | null; itemFields: { [key: string]: ShowFieldData }; + label: string | null; }; export type ShowPictureFieldData = { value?: string; diff --git a/resources/js/types/routes.d.ts b/resources/js/types/routes.d.ts index ff2e39943..113422edc 100644 --- a/resources/js/types/routes.d.ts +++ b/resources/js/types/routes.d.ts @@ -106,6 +106,7 @@ declare module 'ziggy-js' { "name": "filterKey" } ], + "code16.sharp.update-assets": [], "code16.sharp.api.dashboard.command.form": [ { "name": "dashboardKey" diff --git a/src/Data/Commands/CommandResponseData.php b/src/Data/Commands/CommandResponseData.php index 91a741be5..2c2ce798e 100644 --- a/src/Data/Commands/CommandResponseData.php +++ b/src/Data/Commands/CommandResponseData.php @@ -9,7 +9,7 @@ // download & streamDownload actions returns the file directly in the response #[LiteralTypeScriptType( '{ action: "'.CommandAction::Link->value.'", link: string } | '. - '{ action: "'.CommandAction::Info->value.'", message: string } | '. + '{ action: "'.CommandAction::Info->value.'", message: string, reload: boolean } | '. '{ action: "'.CommandAction::Refresh->value.'", items?: Array<{ [key: string]: any }> } | '. '{ action: "'.CommandAction::Reload->value.'" } | '. '{ action: "'.CommandAction::Step->value.'", step: string } | '. diff --git a/src/EntityList/Commands/Command.php b/src/EntityList/Commands/Command.php index 627d0427f..f28bf6ed5 100644 --- a/src/EntityList/Commands/Command.php +++ b/src/EntityList/Commands/Command.php @@ -34,11 +34,12 @@ abstract class Command private ?string $confirmationButtonLabel = null; private ?string $description = null; - protected function info(string $message): array + protected function info(string $message, bool $reload = false): array { return [ 'action' => CommandAction::Info->value, 'message' => $message, + 'reload' => $reload, ]; } diff --git a/src/EntityList/Commands/EntityState.php b/src/EntityList/Commands/EntityState.php index 496ffa4ed..31f2a0eb3 100644 --- a/src/EntityList/Commands/EntityState.php +++ b/src/EntityList/Commands/EntityState.php @@ -31,7 +31,7 @@ protected function view(string $bladeView, array $params = []): array throw new SharpInvalidConfigException('View return type is not supported for a state.'); } - protected function info(string $message): array + protected function info(string $message, bool $reload = false): array { throw new SharpInvalidConfigException('Info return type is not supported for a state.'); } diff --git a/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php index e3aa8d3db..dcbd3c715 100644 --- a/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php @@ -42,6 +42,7 @@ public function execute(array $data = []): array ->assertJson([ 'action' => 'info', 'message' => 'ok', + 'reload' => false, ]); }); @@ -74,6 +75,37 @@ public function execute(array $data = []): array ]); }); +it('allows to call an info + reload entity command', function () { + fakeListFor('person', new class() extends PersonList + { + protected function getEntityCommands(): ?array + { + return [ + 'cmd' => new class() extends EntityCommand + { + public function label(): ?string + { + return 'entity'; + } + + public function execute(array $data = []): array + { + return $this->info('ok', reload: true); + } + }, + ]; + } + }); + + $this->postJson(route('code16.sharp.api.list.command.entity', ['person', 'cmd'])) + ->assertOk() + ->assertJson([ + 'action' => 'info', + 'message' => 'ok', + 'reload' => true, + ]); +}); + it('allows to call a view entity command', function () { fakeListFor('person', new class() extends PersonList { diff --git a/tests/Http/Api/Commands/ApiEntityListInstanceCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListInstanceCommandControllerTest.php index 58380f96c..9a12dc88f 100644 --- a/tests/Http/Api/Commands/ApiEntityListInstanceCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListInstanceCommandControllerTest.php @@ -41,6 +41,7 @@ public function execute($instanceId, array $data = []): array ->assertJson([ 'action' => 'info', 'message' => 'ok', + 'reload' => false, ]); }); @@ -73,6 +74,37 @@ public function execute($instanceId, array $data = []): array ]); }); +it('allows to call an info + reload instance command', function () { + fakeListFor('person', new class() extends PersonList + { + protected function getInstanceCommands(): ?array + { + return [ + 'instance_info' => new class() extends InstanceCommand + { + public function label(): ?string + { + return 'my command'; + } + + public function execute($instanceId, array $data = []): array + { + return $this->info('ok', reload: true); + } + }, + ]; + } + }); + + $this->postJson(route('code16.sharp.api.list.command.instance', ['person', 'instance_info', 1])) + ->assertOk() + ->assertJson([ + 'action' => 'info', + 'message' => 'ok', + 'reload' => true, + ]); +}); + it('allows to call a view instance command', function () { fakeListFor('person', new class() extends PersonList {