diff --git a/demo/app/Sharp/Authors/Commands/VisitFacebookProfileCommand.php b/demo/app/Sharp/Authors/Commands/VisitFacebookProfileCommand.php index 64b5667b4..8a5226fe7 100644 --- a/demo/app/Sharp/Authors/Commands/VisitFacebookProfileCommand.php +++ b/demo/app/Sharp/Authors/Commands/VisitFacebookProfileCommand.php @@ -20,7 +20,7 @@ public function buildCommandConfig(): void public function execute(mixed $instanceId, array $data = []): array { - return $this->link('https://facebook.com'); + return $this->link('https://facebook.com', openInNewTab: true); } public function authorizeFor(mixed $instanceId): bool diff --git a/docs/guide/commands.md b/docs/guide/commands.md index 2812d326c..fad8db12f 100644 --- a/docs/guide/commands.md +++ b/docs/guide/commands.md @@ -169,18 +169,18 @@ Here is the full list of available methods: ### Command return types -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: +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 eight of them: -- `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->info(string $message, bool $reload = false)`: displays the entered text in a modal. The second argument allows reloading the page first. - `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. -- `return $this->html('...')`: display an HTML content. -- `return $this->link('/path/to/redirect')`: redirect to the given path. -- `return $this->download('path', 'diskName')`: the browser will download the specified file. -- `return $this->streamDownload('path', 'name')`: the browser will stream the specified file. - -\* `refresh()` is only useful in an Entity List case (in a Dashboard or a Show Page, it will be treated as a `reload()`). In order to make it work properly, you have to slightly adapt the `getListData()` of your Entity List implementation, making use of `$this->queryParams->specificIds()`: +- `return $this->refresh(mixed $ids)`*: refresh only instance(s) with an id in `$ids`, which can be either a single id or an array. +- `return $this->view(string $bladeView, array $params = [])`: display a view right in Sharp; useful for page previews. +- `return $this->html(string $htmlContent)`: display an HTML content. +- `return $this->link(string $link, bool $openInNewTab = false)`: redirect to the given path. The second argument, optional (default is `false`), is a boolean to open the link in a new tab. +- `return $this->download(string $filePath, ?string $fileName = null, ?string $diskName = null)`: the browser will download the specified file. +- `return $this->streamDownload(string $fileContent, string $fileName)`: the browser will stream the specified file. + +\* `refresh()` is only useful in an Entity List case (in a Dashboard or a Show Page, it will be treated as a `reload()`). To make it work properly, you have to slightly adapt the `getListData()` of your Entity List implementation, making use of `$this->queryParams->specificIds()`: ```php class OrderList extends SharpEntityList diff --git a/resources/js/commands/CommandManager.ts b/resources/js/commands/CommandManager.ts index c5ea2f847..454737025 100644 --- a/resources/js/commands/CommandManager.ts +++ b/resources/js/commands/CommandManager.ts @@ -41,25 +41,27 @@ export class CommandManager { get defaultCommandResponseHandlers(): CommandResponseHandlers { return { - info: async ({ message, reload }, { formModal }) => { - await showAlert(message, { + info: async (data, { formModal }) => { + await showAlert(data.message, { title: __('sharp::modals.command.info.title'), }); if(formModal.shouldReopen) { formModal.reloadAndReopen(); - } else if(reload) { + } else if(data.reload) { await this.handleCommandResponse({ action: 'reload' }); } }, - link: ({ link }, { formModal }) => { + link: (data, { formModal }) => { if(formModal.shouldReopen) { formModal.reloadAndReopen(); return; } - if(isSharpLink(link)) { - router.visit(link); + if(data.openInNewTab) { + window.open(data.link, '_blank'); + } else if(isSharpLink(data.link)) { + router.visit(data.link); } else { - location.href = link; + location.href = data.link; } }, reload: (data, { formModal }) => { diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index c161ea09c..faa14533a 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -65,7 +65,7 @@ export type CommandFormData = { pageAlert: PageAlertData | null; }; export type CommandResponseData = - | { action: "link"; link: string } + | { action: "link"; link: string; openInNewTab: boolean } | { action: "info"; message: string; reload: boolean } | { action: "refresh"; items?: Array<{ [key: string]: any }> } | { action: "reload" } @@ -140,9 +140,6 @@ export type EmbedFormData = { fields: { [key: string]: FormFieldData }; layout: FormLayoutData | null; }; -export type EmbeddedFieldAuthorizationsData = { - view: boolean; -}; export type EntityListAuthorizationsData = { create: boolean; reorder: boolean; @@ -811,7 +808,7 @@ export type ShowDashboardFieldData = { dashboardKey: string; hiddenCommands: Array; endpointUrl: string; - authorizations: EmbeddedFieldAuthorizationsData; + authorizations: ShowFieldAuthorizationsData; label: string | null; hiddenFilters: { [key: string]: any } | null; }; @@ -838,10 +835,13 @@ export type ShowEntityListFieldData = { showSearchField: boolean; showCount: boolean; endpointUrl: string; - authorizations: EmbeddedFieldAuthorizationsData; + authorizations: ShowFieldAuthorizationsData; label: string | null; hiddenFilters: { [key: string]: any } | null; }; +export type ShowFieldAuthorizationsData = { + view: boolean; +}; export type ShowFieldData = | ShowDashboardFieldData | ShowEntityListFieldData diff --git a/src/Data/Commands/CommandResponseData.php b/src/Data/Commands/CommandResponseData.php index 2c2ce798e..f5ec202d0 100644 --- a/src/Data/Commands/CommandResponseData.php +++ b/src/Data/Commands/CommandResponseData.php @@ -8,7 +8,7 @@ // download & streamDownload actions returns the file directly in the response #[LiteralTypeScriptType( - '{ action: "'.CommandAction::Link->value.'", link: string } | '. + '{ action: "'.CommandAction::Link->value.'", link: string, openInNewTab: boolean } | '. '{ action: "'.CommandAction::Info->value.'", message: string, reload: boolean } | '. '{ action: "'.CommandAction::Refresh->value.'", items?: Array<{ [key: string]: any }> } | '. '{ action: "'.CommandAction::Reload->value.'" } | '. diff --git a/src/EntityList/Commands/Command.php b/src/EntityList/Commands/Command.php index bc29bce41..04da25df3 100644 --- a/src/EntityList/Commands/Command.php +++ b/src/EntityList/Commands/Command.php @@ -44,11 +44,12 @@ protected function info(string $message, bool $reload = false): array ]; } - protected function link(string $link): array + protected function link(string $link, bool $openInNewTab = false): array { return [ 'action' => CommandAction::Link->value, 'link' => $link, + 'openInNewTab' => $openInNewTab, ]; } diff --git a/src/EntityList/Commands/EntityState.php b/src/EntityList/Commands/EntityState.php index 31f2a0eb3..546c5309a 100644 --- a/src/EntityList/Commands/EntityState.php +++ b/src/EntityList/Commands/EntityState.php @@ -46,7 +46,7 @@ protected function streamDownload(string $fileContent, string $fileName): array throw new SharpInvalidConfigException('StreamDownload return type is not supported for a state.'); } - protected function link(string $link): array + protected function link(string $link, bool $openInNewTab = false): array { throw new SharpInvalidConfigException('Link 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 c5869b343..409f05fe7 100644 --- a/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php @@ -211,6 +211,68 @@ public function getListData(): array|\Illuminate\Contracts\Support\Arrayable ]); }); +it('allows to call an link 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->link('https://sharp.code16.fr'); + } + }, + ]; + } + }); + + $this->postJson(route('code16.sharp.api.list.command.entity', ['person', 'cmd'])) + ->assertOk() + ->assertJson([ + 'action' => 'link', + 'link' => 'https://sharp.code16.fr', + 'openInNewTab' => false, + ]); +}); + +it('allows to call an link + openInNewTab 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->link('https://sharp.code16.fr', openInNewTab: true); + } + }, + ]; + } + }); + + $this->postJson(route('code16.sharp.api.list.command.entity', ['person', 'cmd'])) + ->assertOk() + ->assertJson([ + 'action' => 'link', + 'link' => 'https://sharp.code16.fr', + 'openInNewTab' => true, + ]); +}); + it('allows to call a form entity command and it handles 422', function () { fakeListFor('person', new class() extends PersonList {