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
{