From b5c459aa4c25fdd1a93ff25c9634f61a67be6c29 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Mon, 20 Jan 2025 16:34:00 +0800 Subject: [PATCH 01/12] feature: support PresetsHelper --- .gitignore | 4 + composer.json | 10 +- phpunit.xml | 18 ++ src/Controller/AmisSourceController.php | 6 + .../CreateUpdateFormTrait.php | 6 + .../AmisSourceController/DetailTrait.php | 6 + src/Helper/ArrayHelper.php | 3 + src/Helper/ConfigHelper.php | 3 + src/Helper/PresetsHelper.php | 218 ++++++++++++++++++ src/Helper/PresetsHelperInterface.php | 80 +++++++ src/Repository/AbsRepository.php | 16 ++ src/Repository/EloquentRepository.php | 4 + src/Repository/HasPresetInterface.php | 14 ++ src/Repository/HasPresetTrait.php | 28 +++ tests/Pest.php | 45 ++++ tests/Unit/PresetsHelperTest.php | 114 +++++++++ tests/bootstrap.php | 14 ++ tests/config/app.php | 1 + tests/config/container.php | 3 + 19 files changed, 591 insertions(+), 2 deletions(-) create mode 100644 phpunit.xml create mode 100644 src/Helper/PresetsHelper.php create mode 100644 src/Helper/PresetsHelperInterface.php create mode 100644 src/Repository/HasPresetInterface.php create mode 100644 src/Repository/HasPresetTrait.php create mode 100644 tests/Pest.php create mode 100644 tests/Unit/PresetsHelperTest.php create mode 100644 tests/bootstrap.php create mode 100644 tests/config/app.php create mode 100644 tests/config/container.php diff --git a/.gitignore b/.gitignore index 615e5e5..3d2c4e7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ vendor .vscode .phpunit* composer.lock + +# webman 1.6 以上版本 BASE_PATH 限定在根目录,为了 tests 排除 +/config +/tests/helpers.php \ No newline at end of file diff --git a/composer.json b/composer.json index 777419d..8835b3e 100644 --- a/composer.json +++ b/composer.json @@ -16,10 +16,16 @@ "ext-json": "*" }, "require-dev": { - "workerman/webman-framework": "^1.3", + "workerman/webman-framework": "^1.5", "illuminate/database": "^8.83", "illuminate/pagination": "^8.83", "illuminate/validation": "^8.83", - "webman-tech/polyfill": "^1.0" + "webman-tech/polyfill": "^1.0", + "pestphp/pest": "^1.23" + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..1b5690f --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests + + + + + ./app + ./src + + + diff --git a/src/Controller/AmisSourceController.php b/src/Controller/AmisSourceController.php index f084e7b..ebaaa70 100644 --- a/src/Controller/AmisSourceController.php +++ b/src/Controller/AmisSourceController.php @@ -6,6 +6,7 @@ use Webman\Http\Response; use WebmanTech\AmisAdmin\Amis; use WebmanTech\AmisAdmin\Amis\Component; +use WebmanTech\AmisAdmin\Repository\HasPresetInterface; use WebmanTech\AmisAdmin\Repository\RepositoryInterface; abstract class AmisSourceController @@ -132,6 +133,11 @@ protected function crudConfig(): array */ protected function grid(): array { + $repository = $this->repository(); + if ($repository instanceof HasPresetInterface) { + return $repository->getPresetsHelper()->pickGrid(); + } + return [ Amis\GridColumn::make()->name($this->repository()->getPrimaryKey()), ]; diff --git a/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php b/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php index 8aa0f33..2718a9f 100644 --- a/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php +++ b/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php @@ -3,6 +3,7 @@ namespace WebmanTech\AmisAdmin\Controller\Traits\AmisSourceController; use WebmanTech\AmisAdmin\Amis; +use WebmanTech\AmisAdmin\Repository\HasPresetInterface; trait CreateUpdateFormTrait { @@ -13,6 +14,11 @@ trait CreateUpdateFormTrait */ protected function form(string $scene): array { + $repository = $this->repository(); + if ($repository instanceof HasPresetInterface) { + return $repository->getPresetsHelper()->pickForm($scene); + } + return [ //Amis\FormField::make()->name('name'), ]; diff --git a/src/Controller/Traits/AmisSourceController/DetailTrait.php b/src/Controller/Traits/AmisSourceController/DetailTrait.php index 3d111d2..3aab75a 100644 --- a/src/Controller/Traits/AmisSourceController/DetailTrait.php +++ b/src/Controller/Traits/AmisSourceController/DetailTrait.php @@ -6,6 +6,7 @@ use Webman\Http\Response; use WebmanTech\AmisAdmin\Amis; use WebmanTech\AmisAdmin\Exceptions\ActionDisableException; +use WebmanTech\AmisAdmin\Repository\HasPresetInterface; trait DetailTrait { @@ -66,6 +67,11 @@ protected function addDetailAction(Amis\GridColumnActions $actions, string $rout */ protected function detail(): array { + $repository = $this->repository(); + if ($repository instanceof HasPresetInterface) { + return $repository->getPresetsHelper()->pickDetail(); + } + return [ Amis\DetailAttribute::make()->name($this->repository()->getPrimaryKey()), ]; diff --git a/src/Helper/ArrayHelper.php b/src/Helper/ArrayHelper.php index 7f36b96..edb9d5d 100644 --- a/src/Helper/ArrayHelper.php +++ b/src/Helper/ArrayHelper.php @@ -2,6 +2,9 @@ namespace WebmanTech\AmisAdmin\Helper; +/** + * @internal + */ class ArrayHelper { /** diff --git a/src/Helper/ConfigHelper.php b/src/Helper/ConfigHelper.php index 266137b..c392fc6 100644 --- a/src/Helper/ConfigHelper.php +++ b/src/Helper/ConfigHelper.php @@ -2,6 +2,9 @@ namespace WebmanTech\AmisAdmin\Helper; +/** + * @internal + */ class ConfigHelper { public const AMIS_MODULE = '__amis_module'; diff --git a/src/Helper/PresetsHelper.php b/src/Helper/PresetsHelper.php new file mode 100644 index 0000000..b710adc --- /dev/null +++ b/src/Helper/PresetsHelper.php @@ -0,0 +1,218 @@ +presets = $presets; + } + + /** + * @inheritDoc + */ + public function withPresets(array $presets) + { + $this->presets = array_merge($this->presets, $presets); + return $this; + } + + /** + * @inheritDoc + */ + public function withDefaultEnable(?array $keys = null) + { + $this->defaultEnable = $keys; + + return $this; + } + + /** + * @inheritDoc + */ + public function pickLabel(?array $keys = null): array + { + return $this->pickColumn('label', $keys, null, true); + } + + /** + * @inheritDoc + */ + public function pickFilter(?array $keys = null): array + { + return $this->pickColumn('filter', $keys, function ($v) { + if ($v === '=') { + return fn($query, $value, $attribute) => $query->where($attribute, $value); + } + if ($v === 'like') { + return fn($query, $value, $attribute) => $query->where($attribute, 'like', '%' . $value . '%'); + } + if ($v === 'datetime-range') { + return fn($query, $value, $attribute) => $query + ->whereBetween($attribute, array_map( + fn($timestamp) => date('Y-m-d H:i:s', (int)$timestamp), + explode(',', $value) + )); + } + return $v; + }, true); + } + + /** + * @inheritDoc + */ + public function pickGrid(?array $keys = null): array + { + return $this->pickColumn('grid', $keys, function ($v, string $column) { + if ($v === null) { + return null; + } + return $v($column); + }); + } + + /** + * @inheritDoc + */ + public function pickForm(string $scene, ?array $keys = null): array + { + $items = $this->pickColumn('form', $keys, function ($v, string $column) use ($scene) { + if ($v === null) { + return null; + } + return $v($column, $scene); + }); + // 允许同时展示两个 FormItem + $data = []; + foreach ($items as $item) { + if (is_array($item)) { + $data = array_merge($data, $item); + } else { + $data[] = $item; + } + } + return $data; + } + + /** + * @inheritDoc + */ + public function pickRules(string $scene, ?array $keys = null): array + { + return $this->pickColumn('rule', $keys, function ($v, string $column) use ($scene) { + if ($v instanceof \Closure) { + return $v($column, $scene); + } + return $v; + }, true); + } + + /** + * @inheritDoc + */ + public function pickRuleMessages(string $scene, ?array $keys = null): array + { + $items = $this->pickColumn('ruleMessages', $keys, function ($v, string $column) use ($scene) { + if ($v instanceof \Closure) { + return $v($column, $scene); + } + return $v; + }); + $data = []; + foreach ($items as $key => $value) { + $data[$key] = $value; + } + return $data; + } + + /** + * @inheritDoc + */ + public function pickRuleCustomAttributes(string $scene, ?array $keys = null): array + { + return $this->pickColumn('ruleCustomAttribute', $keys, function ($v, string $column) use ($scene) { + if ($v instanceof \Closure) { + return $v($column, $scene); + } + return $v; + }, true); + } + + /** + * @inheritDoc + */ + public function pickDetail(?array $keys = null): array + { + return $this->pickColumn('detail', $keys, function ($v, string $column) { + if ($v === true) { + return $column; + } + if ($v instanceof \Closure) { + return $v($column); + } + return $v; + }); + } + + protected ?Collection $formattedPresets = null; + + protected function pickColumn(string $type, ?array $keys = null, ?callable $fnForValue = null, bool $keepKey = false): array + { + if ($this->formattedPresets === null) { + $this->formattedPresets = collect($this->presets) + ->map(function (array $item) { + return array_merge($this->getDefaultColumnConfig(), $item); + }); + } + if ($keys === null) { + if ($this->defaultEnable === null) { + $this->defaultEnable = $this->formattedPresets->keys()->toArray(); + } + if ($this->defaultEnable) { + $keys = $this->defaultEnable; + } + } + + $data = $this->formattedPresets + ->only($keys) + ->map(function (array $item, string $key) use ($type, $fnForValue) { + $v = $item[$type]; + if ($fnForValue !== null) { + $v = $fnForValue($v, $key); + } + return $v; + }) + ->filter(fn($v) => $v !== null) + ->only($keys) + ->toArray(); + + // 按照给定的 key 排序 + if ($keys !== null) { + $data = array_merge(array_flip(array_intersect($keys, array_keys($data))), $data); + } + + return $keepKey ? $data : array_values($data); + } + + protected function getDefaultColumnConfig(): array + { + return [ + 'label' => null, + 'filter' => '=', + 'grid' => fn(string $column) => GridColumn::make()->name($column)->searchable(), + 'form' => fn(string $column) => FormField::make()->name($column), + 'rule' => 'nullable', + 'ruleMessages' => null, + 'ruleCustomAttribute' => null, + 'detail' => true, + ]; + } +} diff --git a/src/Helper/PresetsHelperInterface.php b/src/Helper/PresetsHelperInterface.php new file mode 100644 index 0000000..31fde07 --- /dev/null +++ b/src/Helper/PresetsHelperInterface.php @@ -0,0 +1,80 @@ +getPresetsHelper()->pickLabel(); + } + return []; } @@ -173,6 +177,10 @@ protected function validator(): ValidatorInterface */ protected function rules(string $scene): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->pickRules($scene); + } + return []; } @@ -182,6 +190,10 @@ protected function rules(string $scene): array */ protected function ruleMessages(string $scene): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->pickRuleMessages($scene); + } + return []; } @@ -191,6 +203,10 @@ protected function ruleMessages(string $scene): array */ protected function ruleCustomAttributes(string $scene): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->pickRuleCustomAttributes($scene); + } + return $this->attributeLabels(); } } \ No newline at end of file diff --git a/src/Repository/EloquentRepository.php b/src/Repository/EloquentRepository.php index 0cdfcca..392fad3 100644 --- a/src/Repository/EloquentRepository.php +++ b/src/Repository/EloquentRepository.php @@ -120,6 +120,10 @@ protected function buildSearch(EloquentBuilder $query, array $search): EloquentB */ protected function searchableAttributes(): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->pickFilter(); + } + // 表下的所有字段可搜索 $columns = $this->model()->getConnection()->getSchemaBuilder()->getColumnListing($this->model()->getTable()); $result = []; diff --git a/src/Repository/HasPresetInterface.php b/src/Repository/HasPresetInterface.php new file mode 100644 index 0000000..ef613a7 --- /dev/null +++ b/src/Repository/HasPresetInterface.php @@ -0,0 +1,14 @@ +presetsHelper === null) { + $this->presetsHelper = $this->createPresetsHelper(); + } + return $this->presetsHelper; + } + + /** + * 创建 PresetsHelper + * @return PresetsHelperInterface + */ + abstract protected function createPresetsHelper(): PresetsHelperInterface; +} diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..5949c61 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,45 @@ +in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/tests/Unit/PresetsHelperTest.php b/tests/Unit/PresetsHelperTest.php new file mode 100644 index 0000000..09677d8 --- /dev/null +++ b/tests/Unit/PresetsHelperTest.php @@ -0,0 +1,114 @@ + $item->toArray(), $items); +} + +test('simple', function () { + $presetsHelper = new PresetsHelper([ + 'id' => [ + 'label' => 'ID', + 'filter' => '=', + 'grid' => fn(string $column) => GridColumn::make()->name($column)->searchable(), + 'form' => null, + 'detail' => true, + ], + 'code' => [ + 'label' => '编码', + 'filter' => '=', + 'grid' => fn(string $column) => GridColumn::make()->name($column)->searchable(), + 'form' => fn(string $column, string $scene) => FormField::make()->name($column)->required($scene === AmisSourceController::SCENE_CREATE), + 'detail' => true, + 'rule' => 'required|string|max:64', + ], + ]); + + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => '编码']) + ->and(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ + GridColumn::make()->name('id')->searchable(), + GridColumn::make()->name('code')->searchable() + ])) + ->and(array_keys($presetsHelper->pickFilter()))->toBe(['id', 'code']) // 无法比较匿名函数 + ->and(components_to_array($presetsHelper->pickForm(AmisSourceController::SCENE_CREATE)))->toBe(components_to_array([ + FormField::make()->name('code')->required(true) + ])) + ->and($presetsHelper->pickDetail())->toBe([ + 'id', + 'code', + ]) + ->and($presetsHelper->pickRules(AmisSourceController::SCENE_CREATE))->toBe([ + 'id' => 'nullable', + 'code' => 'required|string|max:64', + ]); +}); + +test('check withPresets', function () { + $presetsHelper = new PresetsHelper(); + expect(array_keys($presetsHelper->pickLabel()))->toBe([]); + + $presetsHelper->withPresets([ + 'id' => [ + 'label' => 'ID', + ], + ]); + expect($presetsHelper->pickLabel())->toBe([]); // 后更改的无效 + + $presetsHelper = (new PresetsHelper()) + ->withPresets([ + 'id' => [ + 'label' => 'ID', + ], + ]); + expect(array_keys($presetsHelper->pickLabel()))->toBe(['id']); +}); + +test('check withDefaultEnable', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'label' => 'ID', + ], + 'code' => [ + 'label' => '编码', + ] + ]))->withDefaultEnable(['id']); + expect(array_keys($presetsHelper->pickLabel()))->toBe(['id']); +}); + +test('支持指定 key', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'label' => 'ID', + ], + 'code' => [ + 'label' => '编码', + ] + ])); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => '编码']) + ->and($presetsHelper->pickLabel(['id']))->toBe(['id' => 'ID']); +}); + +test('pickForm 支持多个 field', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'form' => fn(string $column, string $scene) => FormField::make()->name($column) + ], + 'code' => [ + 'form' => fn(string $column, string $scene) => [ + FormField::make()->name($column.'1'), + FormField::make()->name($column.'2'), + ], + ] + ])); + expect(components_to_array($presetsHelper->pickForm(AmisSourceController::SCENE_CREATE)))->toBe(components_to_array([ + FormField::make()->name('id'), + FormField::make()->name('code1'), + FormField::make()->name('code2'), + ])); +}); \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..a97885b --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,14 @@ + Date: Mon, 20 Jan 2025 21:39:40 +0800 Subject: [PATCH 02/12] feature: change default presets --- src/Helper/PresetsHelper.php | 47 +++++++++++++++++++++++------- tests/Unit/PresetsHelperTest.php | 49 +++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/Helper/PresetsHelper.php b/src/Helper/PresetsHelper.php index b710adc..8404876 100644 --- a/src/Helper/PresetsHelper.php +++ b/src/Helper/PresetsHelper.php @@ -31,7 +31,6 @@ public function withPresets(array $presets) public function withDefaultEnable(?array $keys = null) { $this->defaultEnable = $keys; - return $this; } @@ -49,7 +48,7 @@ public function pickLabel(?array $keys = null): array public function pickFilter(?array $keys = null): array { return $this->pickColumn('filter', $keys, function ($v) { - if ($v === '=') { + if ($v === '=' || $v === true) { return fn($query, $value, $attribute) => $query->where($attribute, $value); } if ($v === 'like') { @@ -75,6 +74,13 @@ public function pickGrid(?array $keys = null): array if ($v === null) { return null; } + if ($v === true) { + $value = GridColumn::make()->name($column); + if ($this->isColumnSearchable($column)) { + return $value->searchable(); + } + return $value; + } return $v($column); }); } @@ -88,9 +94,16 @@ public function pickForm(string $scene, ?array $keys = null): array if ($v === null) { return null; } + if ($v === true) { + $value = FormField::make()->name($column); + if ($this->isColumnRequired($column, $scene)) { + return $value->required(); + } + return $value; + } return $v($column, $scene); }); - // 允许同时展示两个 FormItem + // 允许同时展示多个 FormItem $data = []; foreach ($items as $item) { if (is_array($item)) { @@ -109,7 +122,7 @@ public function pickRules(string $scene, ?array $keys = null): array { return $this->pickColumn('rule', $keys, function ($v, string $column) use ($scene) { if ($v instanceof \Closure) { - return $v($column, $scene); + return $v($scene, $column); } return $v; }, true); @@ -122,7 +135,7 @@ public function pickRuleMessages(string $scene, ?array $keys = null): array { $items = $this->pickColumn('ruleMessages', $keys, function ($v, string $column) use ($scene) { if ($v instanceof \Closure) { - return $v($column, $scene); + return $v($scene, $column); } return $v; }); @@ -140,7 +153,7 @@ public function pickRuleCustomAttributes(string $scene, ?array $keys = null): ar { return $this->pickColumn('ruleCustomAttribute', $keys, function ($v, string $column) use ($scene) { if ($v instanceof \Closure) { - return $v($column, $scene); + return $v($scene, $column); } return $v; }, true); @@ -206,13 +219,27 @@ protected function getDefaultColumnConfig(): array { return [ 'label' => null, - 'filter' => '=', - 'grid' => fn(string $column) => GridColumn::make()->name($column)->searchable(), - 'form' => fn(string $column) => FormField::make()->name($column), - 'rule' => 'nullable', + 'filter' => true, + 'grid' => true, + 'form' => true, + 'rule' => null, 'ruleMessages' => null, 'ruleCustomAttribute' => null, 'detail' => true, ]; } + + protected function isColumnSearchable(string $column): bool + { + return isset($this->pickFilter([$column])[$column]); + } + + protected function isColumnRequired(string $column, string $scene): bool + { + $rules = $this->pickRules($scene, [$column])[$column] ?? ''; + if (is_string($rules)) { + $rules = array_filter(explode('|', $rules)); + } + return in_array('required', $rules, true); + } } diff --git a/tests/Unit/PresetsHelperTest.php b/tests/Unit/PresetsHelperTest.php index 09677d8..64717ab 100644 --- a/tests/Unit/PresetsHelperTest.php +++ b/tests/Unit/PresetsHelperTest.php @@ -44,11 +44,58 @@ function components_to_array(array $items): array 'code', ]) ->and($presetsHelper->pickRules(AmisSourceController::SCENE_CREATE))->toBe([ - 'id' => 'nullable', 'code' => 'required|string|max:64', ]); }); +test('default', function () { + $presetsHelper = new PresetsHelper([ + 'id' => [], + 'code' => [ + 'rule' => 'required|string|max:64', + ], + 'code2' => [ + 'filter' => null, + 'rule' => fn(string $scene) => array_values(array_filter([ + $scene === AmisSourceController::SCENE_CREATE ? 'required' : null, + 'string' + ])), + ] + ]); + + expect($presetsHelper->pickLabel())->toBe([]) + ->and(array_keys($presetsHelper->pickFilter()))->toBe(['id', 'code']) // 无法比较匿名函数 + ->and(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ + GridColumn::make()->name('id')->searchable(), + GridColumn::make()->name('code')->searchable(), + GridColumn::make()->name('code2'), + ])) + ->and($presetsHelper->pickDetail())->toBe([ + 'id', + 'code', + 'code2', + ]) + ->and($presetsHelper->pickRules(AmisSourceController::SCENE_CREATE))->toBe([ + 'code' => 'required|string|max:64', + 'code2' => ['required', 'string'], + ]) + ->and($presetsHelper->pickRules(AmisSourceController::SCENE_UPDATE))->toBe([ + 'code' => 'required|string|max:64', + 'code2' => ['string'], + ]) + ->and(components_to_array($presetsHelper->pickForm(AmisSourceController::SCENE_CREATE)))->toBe(components_to_array([ + FormField::make()->name('id'), + FormField::make()->name('code')->required(), + FormField::make()->name('code2')->required(), + ])) + ->and(components_to_array($presetsHelper->pickForm(AmisSourceController::SCENE_UPDATE)))->toBe(components_to_array([ + FormField::make()->name('id'), + FormField::make()->name('code')->required(), + FormField::make()->name('code2'), + ])) + ; +}); + test('check withPresets', function () { $presetsHelper = new PresetsHelper(); expect(array_keys($presetsHelper->pickLabel()))->toBe([]); From 5a9470913bd2ae09a78e7c0d1f148938d6d65eb7 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Mon, 20 Jan 2025 21:46:18 +0800 Subject: [PATCH 03/12] feature: support dynamic lang and locale --- src/Amis.php | 10 ++++++++++ src/config/plugin/webman-tech/amis-admin/amis.php | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Amis.php b/src/Amis.php index f021370..72b5159 100644 --- a/src/Amis.php +++ b/src/Amis.php @@ -131,6 +131,16 @@ private function getAssets(): array return $item; }, $assets['js']); + $assets['lang'] = $assets['lang'] ?? 'zh'; + if (is_callable($assets['lang'])) { + $assets['lang'] = call_user_func($assets['lang']); + } + + $assets['locale'] = $assets['locale'] ?? 'zh-CN'; + if (is_callable($assets['locale'])) { + $assets['locale'] = call_user_func($assets['locale']); + } + return $assets; } } diff --git a/src/config/plugin/webman-tech/amis-admin/amis.php b/src/config/plugin/webman-tech/amis-admin/amis.php index 67e2f5b..2dd4df8 100644 --- a/src/config/plugin/webman-tech/amis-admin/amis.php +++ b/src/config/plugin/webman-tech/amis-admin/amis.php @@ -19,7 +19,7 @@ /** * html 上的 lang 属性 */ - 'lang' => config('translation.locale', 'zh'), + 'lang' => fn() => locale(), /** * 静态资源,建议下载下来放到 public 目录下然后替换链接 * @link https://aisuda.bce.baidu.com/amis/zh-CN/docs/start/getting-started#sdk @@ -49,7 +49,7 @@ * 语言 * @link https://aisuda.bce.baidu.com/amis/zh-CN/docs/extend/i18n */ - 'locale' => str_replace('_', '-', config('translation.locale', 'zh-CN')), + 'locale' => fn() => str_replace('_', '-', locale()), /** * debug * @link https://aisuda.bce.baidu.com/amis/zh-CN/docs/extend/debug From 42f8844726f45dfa71feeab0528087b7f86f0e3e Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Mon, 20 Jan 2025 22:00:58 +0800 Subject: [PATCH 04/12] feature: support en lang --- src/Amis/Crud.php | 2 +- src/Amis/GridColumnActions.php | 16 ++++++++-------- src/Controller/RenderController.php | 10 +++++----- src/Install.php | 1 + src/resource/translations/en/amis-admin.php | 16 ++++++++++++++++ 5 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 src/resource/translations/en/amis-admin.php diff --git a/src/Amis/Crud.php b/src/Amis/Crud.php index 26f2c7b..0bd1e85 100644 --- a/src/Amis/Crud.php +++ b/src/Amis/Crud.php @@ -105,7 +105,7 @@ public function withColumns(array $columns) */ public function withCreate(string $api, array $form, string $can = '1==1') { - $label = $this->config['schema_create']['label'] ?? '新增'; + $label = $this->config['schema_create']['label'] ?? trans('新增', [], 'amis-admin'); return $this->withButtonDialog(static::INDEX_CREATE, $label, $form, $this->merge([ 'api' => $api, 'level' => 'primary', diff --git a/src/Amis/GridColumnActions.php b/src/Amis/GridColumnActions.php index a880ec3..83121b9 100644 --- a/src/Amis/GridColumnActions.php +++ b/src/Amis/GridColumnActions.php @@ -24,7 +24,7 @@ public function __construct() $this->config['schema_recovery'] = []; $this->config['schema'] = [ 'type' => 'operation', - 'label' => '操作', + 'label' => trans('操作', [], 'amis-admin'), 'buttons' => [], ]; @@ -40,14 +40,14 @@ public function __construct() */ public function withDetail(array $detailAttributes, string $initApi = null, string $can = '1==1') { - $label = $this->config['schema_detail']['label'] ?? '详情'; + $label = $this->config['schema_detail']['label'] ?? trans('详情', [], 'amis-admin'); return $this->withButtonDialog(static::INDEX_DETAIL, $label, $detailAttributes, $this->merge([ 'initApi' => $initApi, 'visibleOn' => $can, 'dialog' => [ 'closeOnOutside' => true, 'actions' => [ - ['type' => 'button', 'label' => '取消', 'actionType' => 'cancel'], + ['type' => 'button', 'label' => trans('取消', [], 'amis-admin'), 'actionType' => 'cancel'], ], ], ], $this->config['schema_detail'])); @@ -63,7 +63,7 @@ public function withDetail(array $detailAttributes, string $initApi = null, stri */ public function withUpdate(array $formFields, string $api, string $initApi = null, string $can = '1==1') { - $label = $this->config['schema_update']['label'] ?? '修改'; + $label = $this->config['schema_update']['label'] ?? trans('修改', [], 'amis-admin'); return $this->withButtonDialog(static::INDEX_UPDATE, $label, $formFields, $this->merge([ 'initApi' => $initApi, 'api' => $api, @@ -80,10 +80,10 @@ public function withUpdate(array $formFields, string $api, string $initApi = nul */ public function withDelete(string $api, string $can = '1==1') { - $label = $this->config['schema_delete']['label'] ?? '删除'; + $label = $this->config['schema_delete']['label'] ?? trans('删除', [], 'amis-admin'); return $this->withButtonAjax(static::INDEX_DELETE, $label, $api, $this->merge([ 'level' => 'danger', - 'confirmText' => "确定要{$label}?", + 'confirmText' => trans('确定要%operate%?', ['%operate%' => $label], 'amis-admin'), 'visibleOn' => $can, ], $this->config['schema_delete'])); } @@ -96,10 +96,10 @@ public function withDelete(string $api, string $can = '1==1') */ public function withRecovery(string $api, string $can = '1==1') { - $label = $this->config['schema_recovery']['label'] ?? '恢复'; + $label = $this->config['schema_recovery']['label'] ?? trans('恢复', [], 'amis-admin'); return $this->withButtonAjax(static::INDEX_RECOVERY, $label, $api, $this->merge([ 'level' => 'warning', - 'confirmText' => "确定要{$label}?", + 'confirmText' => trans('确定要%operate%?', ['%operate%' => $label], 'amis-admin'), 'visibleOn' => $can, ], $this->config['schema_recovery'])); } diff --git a/src/Controller/RenderController.php b/src/Controller/RenderController.php index 7564f93..3deca13 100644 --- a/src/Controller/RenderController.php +++ b/src/Controller/RenderController.php @@ -19,14 +19,14 @@ public function login() $defaultData = [ // 以下为常用的替换参数 'background' => '#eee', // 可以使用图片, 'url(http://xxxx)' - 'title' => config('app.name', '登录'), - 'submit_text' => '登录', - 'success_msg' => '登录成功', + 'title' => config('app.name', trans('登录', [], 'amis-admin')), + 'submit_text' => trans('登录', [], 'amis-admin'), + 'success_msg' => trans('登录成功', [], 'amis-admin'), 'form_width' => 400, 'login_api' => '/admin/auth/login', 'form' => [ - Amis\FormField::make()->name('username')->label('用户名')->required(), - Amis\FormField::make()->name('password')->label('密码')->typeInputPassword()->required(), + Amis\FormField::make()->name('username')->label(trans('用户名', [], 'amis-admin'))->required(), + Amis\FormField::make()->name('password')->label(trans('密码', [], 'amis-admin'))->typeInputPassword()->required(), ], 'success_redirect' => '/admin', // 用于调整整个表单 diff --git a/src/Install.php b/src/Install.php index a4c6439..420e6e9 100644 --- a/src/Install.php +++ b/src/Install.php @@ -10,6 +10,7 @@ class Install */ protected static $pathRelation = array ( 'config/plugin/webman-tech/amis-admin' => 'config/plugin/webman-tech/amis-admin', + 'resource/translations/en/amis-admin.php' => 'resource/translations/en/amis-admin.php', ); /** diff --git a/src/resource/translations/en/amis-admin.php b/src/resource/translations/en/amis-admin.php new file mode 100644 index 0000000..5c7e0a7 --- /dev/null +++ b/src/resource/translations/en/amis-admin.php @@ -0,0 +1,16 @@ + 'Operate', + '详情' => 'Detail', + '取消' => 'Cancel', + '新增' => 'Create', + '修改' => 'Update', + '删除' => 'Delete', + '恢复' => 'Recovery', + '确定要%operate%?' => 'Confirm to %operate%?', + '登录' => 'Login', + '登录成功' => 'Login success', + '用户名' => 'Username', + '密码' => 'Password', +]; \ No newline at end of file From 7bef507c8f6adf914475452bea49ba551e575f65 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Mon, 20 Jan 2025 22:08:00 +0800 Subject: [PATCH 05/12] feature: add defaultDialogConfig for controller --- src/Controller/AmisSourceController.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Controller/AmisSourceController.php b/src/Controller/AmisSourceController.php index ebaaa70..3b708e5 100644 --- a/src/Controller/AmisSourceController.php +++ b/src/Controller/AmisSourceController.php @@ -25,6 +25,11 @@ abstract class AmisSourceController * @var bool */ protected bool $onlyShow = false; + /** + * 默认的 dialog 框大小 + * @var string|null + */ + protected ?string $defaultDialogConfig = null; /** * @var RepositoryInterface|null @@ -124,6 +129,14 @@ protected function amisCrud(Request $request): Amis\Crud */ protected function crudConfig(): array { + if ($this->defaultDialogConfig) { + return [ + 'schema_create' => [ + 'dialog' => $this->defaultDialogConfig, + ], + ]; + } + return []; } @@ -194,6 +207,17 @@ protected function gridActions(string $routePrefix): Amis\GridColumnActions */ protected function gridActionsConfig(): array { + if ($this->defaultDialogConfig) { + return [ + 'schema_detail' => [ + 'dialog' => $this->defaultDialogConfig, + ], + 'schema_update' => [ + 'dialog' => $this->defaultDialogConfig, + ], + ]; + } + return []; } From 25d394ac96c6033ee74e5a991c098245d5e88117 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Tue, 21 Jan 2025 19:30:14 +0800 Subject: [PATCH 06/12] feature: PresetsHelper enhance --- src/Amis/Component.php | 2 +- src/Controller/AmisSourceController.php | 6 +- src/Helper/PresetsHelper.php | 197 +++++++++++++++++++----- src/Helper/PresetsHelperInterface.php | 80 ---------- src/Repository/AbsRepository.php | 10 +- src/Repository/HasPresetInterface.php | 6 +- src/Repository/HasPresetTrait.php | 12 +- tests/Unit/PresetsHelperTest.php | 193 +++++++++++++++++++---- 8 files changed, 347 insertions(+), 159 deletions(-) delete mode 100644 src/Helper/PresetsHelperInterface.php diff --git a/src/Amis/Component.php b/src/Amis/Component.php index bab5d9f..32ae6a0 100644 --- a/src/Amis/Component.php +++ b/src/Amis/Component.php @@ -36,7 +36,7 @@ public function __construct() public static function make(array $schema = null) { /** @var static $component */ - $component = Container::get(static::class); + $component = clone Container::get(static::class); if ($schema) { $component->schema($schema); } diff --git a/src/Controller/AmisSourceController.php b/src/Controller/AmisSourceController.php index 3b708e5..3d9f9f1 100644 --- a/src/Controller/AmisSourceController.php +++ b/src/Controller/AmisSourceController.php @@ -26,10 +26,10 @@ abstract class AmisSourceController */ protected bool $onlyShow = false; /** - * 默认的 dialog 框大小 - * @var string|null + * 默认的 dialog 框配置 + * @var array|null */ - protected ?string $defaultDialogConfig = null; + protected ?array $defaultDialogConfig = null; /** * @var RepositoryInterface|null diff --git a/src/Helper/PresetsHelper.php b/src/Helper/PresetsHelper.php index 8404876..b3b9f5e 100644 --- a/src/Helper/PresetsHelper.php +++ b/src/Helper/PresetsHelper.php @@ -3,21 +3,29 @@ namespace WebmanTech\AmisAdmin\Helper; use Illuminate\Support\Collection; +use WebmanTech\AmisAdmin\Amis\DetailAttribute; use WebmanTech\AmisAdmin\Amis\FormField; use WebmanTech\AmisAdmin\Amis\GridColumn; +use WebmanTech\AmisAdmin\Repository\AbsRepository; -class PresetsHelper implements PresetsHelperInterface +class PresetsHelper { protected array $presets; - protected ?array $defaultEnable = null; + protected ?bool $defaultNoEdit = null; + protected array $sceneKeys = []; + /** + * @param array $presets + */ public function __construct(array $presets = []) { $this->presets = $presets; } /** - * @inheritDoc + * 添加预设 + * @param array $presets + * @return $this */ public function withPresets(array $presets) { @@ -26,28 +34,81 @@ public function withPresets(array $presets) } /** - * @inheritDoc + * 默认不启用编辑(只读) + * @return $this + */ + public function withDefaultNoEdit() + { + $this->defaultNoEdit = true; + return $this; + } + + /** + * 设置 CRUD 场景对应的字段 + * @param array $keys + * @return $this + */ + public function withCrudSceneKeys(array $keys) + { + return $this->withSceneKeys([ + AbsRepository::SCENE_LIST => $keys, + AbsRepository::SCENE_CREATE => $keys, + AbsRepository::SCENE_UPDATE => $keys, + AbsRepository::SCENE_DETAIL => $keys, + ]); + } + + /** + * 设置场景对应的字段 + * @param array $data + * @return $this */ - public function withDefaultEnable(?array $keys = null) + public function withSceneKeys(array $data) { - $this->defaultEnable = $keys; + foreach ($data as $scene => $keys) { + $this->sceneKeys[$scene] = $keys; + } return $this; } /** - * @inheritDoc + * 获取字段对应的 label + * @param array|null $keys + * @return array */ public function pickLabel(?array $keys = null): array { - return $this->pickColumn('label', $keys, null, true); + return $this->pickColumn(null, 'label', $keys, null, true); } /** - * @inheritDoc + * 获取字段对应的 labelRemark + * @param array|null $keys + * @return array + */ + public function pickLabelRemark(?array $keys = null): array + { + return $this->pickColumn(null, 'labelRemark', $keys, null, true); + } + + /** + * 获取字段对应的 description + * @param array|null $keys + * @return array + */ + public function pickDescription(?array $keys = null): array + { + return $this->pickColumn(null, 'description', $keys, null, true); + } + + /** + * 获取字段对应的 filter + * @param array|null $keys + * @return array */ public function pickFilter(?array $keys = null): array { - return $this->pickColumn('filter', $keys, function ($v) { + return $this->pickColumn(AbsRepository::SCENE_LIST, 'filter', $keys, function ($v) { if ($v === '=' || $v === true) { return fn($query, $value, $attribute) => $query->where($attribute, $value); } @@ -66,18 +127,26 @@ public function pickFilter(?array $keys = null): array } /** - * @inheritDoc + * 获取字段对应的 grid + * @param array|null $keys + * @return array */ public function pickGrid(?array $keys = null): array { - return $this->pickColumn('grid', $keys, function ($v, string $column) { + return $this->pickColumn(AbsRepository::SCENE_LIST, 'grid', $keys, function ($v, string $column, array $columnConfig) { if ($v === null) { return null; } if ($v === true) { $value = GridColumn::make()->name($column); + if (($selectOptions = $this->getSelectOptionsFromColumnConfig($columnConfig)) !== null) { + $value->typeMapping(['map' => $selectOptions['map']]); + } if ($this->isColumnSearchable($column)) { - return $value->searchable(); + $value->searchable(); + } + if (($ext = $columnConfig['gridExt']) instanceof \Closure) { + $ext($value); } return $value; } @@ -86,18 +155,27 @@ public function pickGrid(?array $keys = null): array } /** - * @inheritDoc + * 获取字段对应的 form + * @param string $scene + * @param array|null $keys + * @return array */ public function pickForm(string $scene, ?array $keys = null): array { - $items = $this->pickColumn('form', $keys, function ($v, string $column) use ($scene) { + $items = $this->pickColumn($scene, 'form', $keys, function ($v, string $column, array $columnConfig) use ($scene) { if ($v === null) { return null; } if ($v === true) { $value = FormField::make()->name($column); + if (($selectOptions = $this->getSelectOptionsFromColumnConfig($columnConfig)) !== null) { + $value->typeSelect(['options' => $selectOptions['options']]); + } if ($this->isColumnRequired($column, $scene)) { - return $value->required(); + $value->required(); + } + if (($ext = $columnConfig['formExt']) instanceof \Closure) { + $ext($value, $scene); } return $value; } @@ -116,11 +194,14 @@ public function pickForm(string $scene, ?array $keys = null): array } /** - * @inheritDoc + * 获取字段对应的 rules + * @param string $scene + * @param array|null $keys + * @return array */ public function pickRules(string $scene, ?array $keys = null): array { - return $this->pickColumn('rule', $keys, function ($v, string $column) use ($scene) { + return $this->pickColumn($scene, 'rule', $keys, function ($v, string $column) use ($scene) { if ($v instanceof \Closure) { return $v($scene, $column); } @@ -129,11 +210,14 @@ public function pickRules(string $scene, ?array $keys = null): array } /** - * @inheritDoc + * 获取字段对应的 ruleMessages + * @param string $scene + * @param array|null $keys + * @return array */ public function pickRuleMessages(string $scene, ?array $keys = null): array { - $items = $this->pickColumn('ruleMessages', $keys, function ($v, string $column) use ($scene) { + $items = $this->pickColumn($scene, 'ruleMessages', $keys, function ($v, string $column) use ($scene) { if ($v instanceof \Closure) { return $v($scene, $column); } @@ -147,11 +231,14 @@ public function pickRuleMessages(string $scene, ?array $keys = null): array } /** - * @inheritDoc + * 获取字段对应的 ruleCustomAttributes + * @param string $scene + * @param array|null $keys + * @return array */ public function pickRuleCustomAttributes(string $scene, ?array $keys = null): array { - return $this->pickColumn('ruleCustomAttribute', $keys, function ($v, string $column) use ($scene) { + return $this->pickColumn($scene, 'ruleCustomAttribute', $keys, function ($v, string $column) use ($scene) { if ($v instanceof \Closure) { return $v($scene, $column); } @@ -160,13 +247,22 @@ public function pickRuleCustomAttributes(string $scene, ?array $keys = null): ar } /** - * @inheritDoc + * 获取字段对应的 detail + * @param array|null $keys + * @return array */ public function pickDetail(?array $keys = null): array { - return $this->pickColumn('detail', $keys, function ($v, string $column) { + return $this->pickColumn(AbsRepository::SCENE_DETAIL, 'detail', $keys, function ($v, string $column, array $columnConfig) { if ($v === true) { - return $column; + $value = DetailAttribute::make()->name($column); + if (($selectOptions = $this->getSelectOptionsFromColumnConfig($columnConfig)) !== null) { + $value->typeMapping(['map' => $selectOptions['map']]); + } + if (($ext = $columnConfig['detailExt']) instanceof \Closure) { + $ext($value); + } + return $value; } if ($v instanceof \Closure) { return $v($column); @@ -177,7 +273,7 @@ public function pickDetail(?array $keys = null): array protected ?Collection $formattedPresets = null; - protected function pickColumn(string $type, ?array $keys = null, ?callable $fnForValue = null, bool $keepKey = false): array + protected function pickColumn(?string $scene, string $type, ?array $keys = null, ?callable $fnForValue = null, bool $keepKey = false): array { if ($this->formattedPresets === null) { $this->formattedPresets = collect($this->presets) @@ -185,13 +281,8 @@ protected function pickColumn(string $type, ?array $keys = null, ?callable $fnFo return array_merge($this->getDefaultColumnConfig(), $item); }); } - if ($keys === null) { - if ($this->defaultEnable === null) { - $this->defaultEnable = $this->formattedPresets->keys()->toArray(); - } - if ($this->defaultEnable) { - $keys = $this->defaultEnable; - } + if ($keys === null && $scene !== null) { + $keys = $this->sceneKeys[$scene] ?? null; } $data = $this->formattedPresets @@ -199,7 +290,7 @@ protected function pickColumn(string $type, ?array $keys = null, ?callable $fnFo ->map(function (array $item, string $key) use ($type, $fnForValue) { $v = $item[$type]; if ($fnForValue !== null) { - $v = $fnForValue($v, $key); + $v = $fnForValue($v, $key, $item); } return $v; }) @@ -219,13 +310,19 @@ protected function getDefaultColumnConfig(): array { return [ 'label' => null, + 'labelRemark' => null, + 'description' => null, 'filter' => true, 'grid' => true, - 'form' => true, - 'rule' => null, + 'gridExt' => null, + 'form' => $this->defaultNoEdit ? null : true, + 'formExt' => null, + 'selectOptions' => null, + 'rule' => $this->defaultNoEdit ? null : 'nullable', // 默认为 nullable,使得不填 rule 时可以正常提交和传递数据 'ruleMessages' => null, 'ruleCustomAttribute' => null, 'detail' => true, + 'detailExt' => null, ]; } @@ -242,4 +339,32 @@ protected function isColumnRequired(string $column, string $scene): bool } return in_array('required', $rules, true); } + + protected function getSelectOptionsFromColumnConfig(array &$columnConfig): ?array + { + if ($columnConfig['selectOptions'] === null) { + return null; + } + if (is_array($columnConfig['selectOptions']) && isset($columnConfig['selectOptions']['map'])) { + return $columnConfig['selectOptions']; + } + if ($columnConfig['selectOptions'] instanceof \Closure) { + $columnConfig['selectOptions'] = $columnConfig['selectOptions'](); + } + + $map = $columnConfig['selectOptions']; + $options = []; + foreach ($map as $value => $label) { + $options[] = [ + 'value' => (string)$value, // 强制为 string,保证行为一致 + 'label' => strip_tags($label), // 去除 html + ]; + } + + return $columnConfig['selectOptions'] = [ + 'map' => $map, + 'options' => $options, + 'values' => array_keys($map), + ]; + } } diff --git a/src/Helper/PresetsHelperInterface.php b/src/Helper/PresetsHelperInterface.php deleted file mode 100644 index 31fde07..0000000 --- a/src/Helper/PresetsHelperInterface.php +++ /dev/null @@ -1,80 +0,0 @@ -getPresetsHelper()->pickLabelRemark(); + } + return []; } @@ -77,6 +81,10 @@ public function getDescription(string $attribute) */ protected function attributeDescriptions(): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->pickDescription(); + } + return []; } @@ -134,7 +142,7 @@ abstract protected function doUpdate(array $data, $id): void; * @return array 验证过后的字段 * @throws ValidationException */ - protected function validate(array $data, string $scene): array + public function validate(array $data, string $scene): array { return $this->validator()->validate( $data, diff --git a/src/Repository/HasPresetInterface.php b/src/Repository/HasPresetInterface.php index ef613a7..c6c13b5 100644 --- a/src/Repository/HasPresetInterface.php +++ b/src/Repository/HasPresetInterface.php @@ -2,13 +2,13 @@ namespace WebmanTech\AmisAdmin\Repository; -use WebmanTech\AmisAdmin\Helper\PresetsHelperInterface; +use WebmanTech\AmisAdmin\Helper\PresetsHelper; interface HasPresetInterface { /** * 获取 PresetsHelper - * @return PresetsHelperInterface + * @return PresetsHelper */ - public function getPresetsHelper(): PresetsHelperInterface; + public function getPresetsHelper(): PresetsHelper; } diff --git a/src/Repository/HasPresetTrait.php b/src/Repository/HasPresetTrait.php index 0a42963..3800b5a 100644 --- a/src/Repository/HasPresetTrait.php +++ b/src/Repository/HasPresetTrait.php @@ -2,17 +2,17 @@ namespace WebmanTech\AmisAdmin\Repository; -use WebmanTech\AmisAdmin\Helper\PresetsHelperInterface; +use WebmanTech\AmisAdmin\Helper\PresetsHelper; trait HasPresetTrait { - protected ?PresetsHelperInterface $presetsHelper = null; + protected ?PresetsHelper $presetsHelper = null; /** * 获取 PresetsHelper - * @return PresetsHelperInterface + * @return PresetsHelper */ - public function getPresetsHelper(): PresetsHelperInterface + public function getPresetsHelper(): PresetsHelper { if ($this->presetsHelper === null) { $this->presetsHelper = $this->createPresetsHelper(); @@ -22,7 +22,7 @@ public function getPresetsHelper(): PresetsHelperInterface /** * 创建 PresetsHelper - * @return PresetsHelperInterface + * @return PresetsHelper */ - abstract protected function createPresetsHelper(): PresetsHelperInterface; + abstract protected function createPresetsHelper(): PresetsHelper; } diff --git a/tests/Unit/PresetsHelperTest.php b/tests/Unit/PresetsHelperTest.php index 64717ab..f25c456 100644 --- a/tests/Unit/PresetsHelperTest.php +++ b/tests/Unit/PresetsHelperTest.php @@ -1,20 +1,28 @@ $item->toArray(), $items); } +function components_to_json(array $items): string +{ + return json_encode(components_to_array($items)); +} + test('simple', function () { $presetsHelper = new PresetsHelper([ 'id' => [ 'label' => 'ID', + 'labelRemark' => 'ID remark', + 'description' => 'ID description', 'filter' => '=', 'grid' => fn(string $column) => GridColumn::make()->name($column)->searchable(), 'form' => null, @@ -24,26 +32,29 @@ function components_to_array(array $items): array 'label' => '编码', 'filter' => '=', 'grid' => fn(string $column) => GridColumn::make()->name($column)->searchable(), - 'form' => fn(string $column, string $scene) => FormField::make()->name($column)->required($scene === AmisSourceController::SCENE_CREATE), + 'form' => fn(string $column, string $scene) => FormField::make()->name($column)->required($scene === AbsRepository::SCENE_CREATE), 'detail' => true, 'rule' => 'required|string|max:64', ], ]); expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => '编码']) + ->and($presetsHelper->pickLabelRemark())->toBe(['id' => 'ID remark']) + ->and($presetsHelper->pickDescription())->toBe(['id' => 'ID description']) ->and(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ GridColumn::make()->name('id')->searchable(), GridColumn::make()->name('code')->searchable() ])) ->and(array_keys($presetsHelper->pickFilter()))->toBe(['id', 'code']) // 无法比较匿名函数 - ->and(components_to_array($presetsHelper->pickForm(AmisSourceController::SCENE_CREATE)))->toBe(components_to_array([ - FormField::make()->name('code')->required(true) + ->and(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ + FormField::make()->name('code')->required() ])) - ->and($presetsHelper->pickDetail())->toBe([ - 'id', - 'code', - ]) - ->and($presetsHelper->pickRules(AmisSourceController::SCENE_CREATE))->toBe([ + ->and(components_to_array($presetsHelper->pickDetail()))->toBe(components_to_array([ + DetailAttribute::make()->name('id'), + DetailAttribute::make()->name('code'), + ])) + ->and($presetsHelper->pickRules(AbsRepository::SCENE_CREATE))->toBe([ + 'id' => 'nullable', 'code' => 'required|string|max:64', ]); }); @@ -57,7 +68,7 @@ function components_to_array(array $items): array 'code2' => [ 'filter' => null, 'rule' => fn(string $scene) => array_values(array_filter([ - $scene === AmisSourceController::SCENE_CREATE ? 'required' : null, + $scene === AbsRepository::SCENE_CREATE ? 'required' : null, 'string' ])), ] @@ -70,30 +81,31 @@ function components_to_array(array $items): array GridColumn::make()->name('code')->searchable(), GridColumn::make()->name('code2'), ])) - ->and($presetsHelper->pickDetail())->toBe([ - 'id', - 'code', - 'code2', - ]) - ->and($presetsHelper->pickRules(AmisSourceController::SCENE_CREATE))->toBe([ + ->and(components_to_array($presetsHelper->pickDetail()))->toBe(components_to_array([ + DetailAttribute::make()->name('id'), + DetailAttribute::make()->name('code'), + DetailAttribute::make()->name('code2'), + ])) + ->and($presetsHelper->pickRules(AbsRepository::SCENE_CREATE))->toBe([ + 'id' => 'nullable', 'code' => 'required|string|max:64', 'code2' => ['required', 'string'], ]) - ->and($presetsHelper->pickRules(AmisSourceController::SCENE_UPDATE))->toBe([ + ->and($presetsHelper->pickRules(AbsRepository::SCENE_UPDATE))->toBe([ + 'id' => 'nullable', 'code' => 'required|string|max:64', 'code2' => ['string'], ]) - ->and(components_to_array($presetsHelper->pickForm(AmisSourceController::SCENE_CREATE)))->toBe(components_to_array([ + ->and(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ FormField::make()->name('id'), FormField::make()->name('code')->required(), FormField::make()->name('code2')->required(), ])) - ->and(components_to_array($presetsHelper->pickForm(AmisSourceController::SCENE_UPDATE)))->toBe(components_to_array([ + ->and(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_UPDATE)))->toBe(components_to_array([ FormField::make()->name('id'), FormField::make()->name('code')->required(), FormField::make()->name('code2'), - ])) - ; + ])); }); test('check withPresets', function () { @@ -116,7 +128,7 @@ function components_to_array(array $items): array expect(array_keys($presetsHelper->pickLabel()))->toBe(['id']); }); -test('check withDefaultEnable', function () { +test('check withDefaultNoEdit', function () { $presetsHelper = (new PresetsHelper([ 'id' => [ 'label' => 'ID', @@ -124,11 +136,53 @@ function components_to_array(array $items): array 'code' => [ 'label' => '编码', ] - ]))->withDefaultEnable(['id']); - expect(array_keys($presetsHelper->pickLabel()))->toBe(['id']); + ]))->withDefaultNoEdit(); + expect(array_keys($presetsHelper->pickLabel()))->toBe(['id', 'code']) + ->and($presetsHelper->pickForm(AbsRepository::SCENE_CREATE))->toBe([]) + ->and($presetsHelper->pickForm(AbsRepository::SCENE_UPDATE))->toBe([]) + ->and(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ + GridColumn::make()->name('id')->searchable(), + GridColumn::make()->name('code')->searchable(), + ])) + ->and(components_to_array($presetsHelper->pickDetail()))->toBe(components_to_array([ + DetailAttribute::make()->name('id'), + DetailAttribute::make()->name('code'), + ])); +}); + +test('check withCrudSceneKeys', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'label' => 'ID', + ], + 'code' => [ + 'label' => '编码', + ] + ]))->withCrudSceneKeys(['id']); + expect(array_keys($presetsHelper->pickLabel()))->toBe(['id', 'code']) + ->and(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ + FormField::make()->name('id'), + ])); }); -test('支持指定 key', function () { +test('check withSceneKeys', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'label' => 'ID', + ], + 'code' => [ + 'label' => '编码', + ] + ]))->withSceneKeys([ + 'abcScene' => ['code'] + ]); + expect(array_keys($presetsHelper->pickLabel()))->toBe(['id', 'code']) + ->and(components_to_array($presetsHelper->pickForm('abcScene')))->toBe(components_to_array([ + FormField::make()->name('code'), + ])); +}); + +test('support pick special keys', function () { $presetsHelper = (new PresetsHelper([ 'id' => [ 'label' => 'ID', @@ -141,21 +195,102 @@ function components_to_array(array $items): array ->and($presetsHelper->pickLabel(['id']))->toBe(['id' => 'ID']); }); -test('pickForm 支持多个 field', function () { +test('pickForm support multi field', function () { $presetsHelper = (new PresetsHelper([ 'id' => [ 'form' => fn(string $column, string $scene) => FormField::make()->name($column) ], 'code' => [ 'form' => fn(string $column, string $scene) => [ - FormField::make()->name($column.'1'), - FormField::make()->name($column.'2'), + FormField::make()->name($column . '1'), + FormField::make()->name($column . '2'), ], ] ])); - expect(components_to_array($presetsHelper->pickForm(AmisSourceController::SCENE_CREATE)))->toBe(components_to_array([ + expect(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ FormField::make()->name('id'), FormField::make()->name('code1'), FormField::make()->name('code2'), ])); +}); + +test('pickForm required auto support', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'form' => true, + 'rule' => 'required', + ], + 'code' => [ + 'form' => true, + ], + ])); + expect(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ + FormField::make()->name('id')->required(), + FormField::make()->name('code'), + ])); +}); + +test('pickGrid searchable auto support', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'grid' => true, + ], + 'code' => [ + 'grid' => true, + ], + ])); + expect(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ + GridColumn::make()->name('id')->searchable(), + GridColumn::make()->name('code')->searchable(), + ])); +}); + +test('selectOptions support', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'label' => 'ID', + ], + 'code' => [ + 'label' => '编码', + 'filter' => null, + ], + ])); + expect(components_to_array($presetsHelper->pickGrid())) + ->toBe(components_to_array([ + GridColumn::make()->name('id')->searchable(), + GridColumn::make()->name('code'), + ])); +}); + +test('gridExt formExt detailExt support', function () { + $presetsHelper = (new PresetsHelper([ + 'id' => [ + 'label' => 'ID', + 'gridExt' => fn(GridColumn $column) => $column->sortable(), + 'formExt' => fn(FormField $field) => $field->typeInputNumber(), + 'detailExt' => fn(DetailAttribute $attribute) => $attribute->typeImage(), + ], + 'code' => [ + 'label' => '编码', + 'gridExt' => fn(GridColumn $column) => $column->sortable(), + 'formExt' => fn(FormField $field) => $field->typeInputNumber(), + 'detailExt' => fn(DetailAttribute $attribute) => $attribute->typeImage(), + ], + ])); + + expect(components_to_json($presetsHelper->pickGrid())) + ->toBe(components_to_json([ + GridColumn::make()->name('id')->searchable()->sortable(), + GridColumn::make()->name('code')->searchable()->sortable(), + ])) + ->and(components_to_json($presetsHelper->pickForm(AbsRepository::SCENE_CREATE))) + ->toBe(components_to_json([ + FormField::make()->name('id')->typeInputNumber(), + FormField::make()->name('code')->typeInputNumber(), + ])) + ->and(components_to_json($presetsHelper->pickDetail())) + ->toBe(components_to_json([ + DetailAttribute::make()->name('id')->typeImage(), + DetailAttribute::make()->name('code')->typeImage(), + ])); }); \ No newline at end of file From 883d8756ccc49d5c522f95cc514d64bf85bca384 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Thu, 23 Jan 2025 13:25:06 +0800 Subject: [PATCH 07/12] feature: mapping solve --- src/Amis/DetailAttribute.php | 19 ++++++++++++ src/Amis/GridColumn.php | 21 +++++++------ src/Amis/Traits/TypeMappingSolver.php | 24 +++++++++++++++ src/Helper/PresetsHelper.php | 4 +-- tests/Unit/ArrayHelperTest.php | 43 +++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 src/Amis/Traits/TypeMappingSolver.php create mode 100644 tests/Unit/ArrayHelperTest.php diff --git a/src/Amis/DetailAttribute.php b/src/Amis/DetailAttribute.php index 3793194..59dc58f 100644 --- a/src/Amis/DetailAttribute.php +++ b/src/Amis/DetailAttribute.php @@ -2,6 +2,8 @@ namespace WebmanTech\AmisAdmin\Amis; +use WebmanTech\AmisAdmin\Amis\Traits\TypeMappingSolver; + /** * 详情的一个字段 * @link https://aisuda.bce.baidu.com/amis/zh-CN/components/form/static @@ -36,6 +38,8 @@ */ class DetailAttribute extends Component { + use TypeMappingSolver; + protected array $schema = [ 'type' => 'static', 'name' => '', @@ -73,4 +77,19 @@ public function __call($name, $arguments) } return $this; } + + public function toArray(): array + { + $this->solveType(); + + return parent::toArray(); + } + + protected function solveType() + { + $type = $this->schema['type']; + if ($type === 'static-mapping') { + $this->solveMappingMap(); + } + } } \ No newline at end of file diff --git a/src/Amis/GridColumn.php b/src/Amis/GridColumn.php index 1c3923a..5658992 100644 --- a/src/Amis/GridColumn.php +++ b/src/Amis/GridColumn.php @@ -2,6 +2,8 @@ namespace WebmanTech\AmisAdmin\Amis; +use WebmanTech\AmisAdmin\Amis\Traits\TypeMappingSolver; + /** * table 的单个 column * @@ -36,6 +38,8 @@ */ class GridColumn extends Component { + use TypeMappingSolver; + protected array $schema = [ 'type' => 'text', 'name' => '', @@ -106,9 +110,7 @@ protected function solveType() { $type = $this->schema['type']; if ($type === 'mapping') { - if (isset($this->schema['map'])) { - $this->schema['map'] = (object)$this->schema['map']; // 0 1 会被转为数组的形式,amis 下需要使用 object,所以强制为 object - } + $this->solveMappingMap(); } } @@ -135,12 +137,13 @@ protected function solveSearchable() if (isset($this->schema['map'])) { $searchable['type'] = 'select'; $searchable['options'] = array_map( - fn($label, $value) => [ - 'label' => strip_tags($label), - 'value' => $value, - ], - array_values((array)$this->schema['map']), - array_keys((array)$this->schema['map']) + function (array $item) { + if (isset($item['label'])) { + $item['label'] = strip_tags($item['label']); // 去除 html 结构,使得 map 带 html 格式时支持 + } + return $item; + }, + $this->schema['map'] ); } } elseif ($type === 'date' || $type === 'datetime') { diff --git a/src/Amis/Traits/TypeMappingSolver.php b/src/Amis/Traits/TypeMappingSolver.php new file mode 100644 index 0000000..de42728 --- /dev/null +++ b/src/Amis/Traits/TypeMappingSolver.php @@ -0,0 +1,24 @@ +schema['map']) && !is_array($this->schema['map'][0] ?? null)) { + // 将 [$value => $label] 强制转为 [{label: xx, value: xxx}] 的形式,可以防止 map 被转为 array 的情况 + $this->schema['map'] = array_map( + fn($label, $value) => [ + 'label' => $label, + 'value' => $value, + ], + array_values((array)$this->schema['map']), + array_keys((array)$this->schema['map']) + ); + } + } +} \ No newline at end of file diff --git a/src/Helper/PresetsHelper.php b/src/Helper/PresetsHelper.php index b3b9f5e..11796db 100644 --- a/src/Helper/PresetsHelper.php +++ b/src/Helper/PresetsHelper.php @@ -10,7 +10,7 @@ class PresetsHelper { - protected array $presets; + protected array $presets = []; protected ?bool $defaultNoEdit = null; protected array $sceneKeys = []; @@ -19,7 +19,7 @@ class PresetsHelper */ public function __construct(array $presets = []) { - $this->presets = $presets; + $this->withPresets($presets); } /** diff --git a/tests/Unit/ArrayHelperTest.php b/tests/Unit/ArrayHelperTest.php new file mode 100644 index 0000000..8379ee9 --- /dev/null +++ b/tests/Unit/ArrayHelperTest.php @@ -0,0 +1,43 @@ + 1], + ['b' => 2], + )) + ->toBe([ + 'a' => 1, + 'b' => 2, + ]) + // 同 key 合并 + ->and(ArrayHelper::merge( + ['a' => 1], + ['a' => 2], + )) + ->toBe([ + 'a' => 2, + ]) + // 多维数组合并 + ->and(ArrayHelper::merge( + ['a' => ['x' => 'y']], + ['a' => ['m' => 'n']], + )) + ->toBe([ + 'a' => [ + 'x' => 'y', + 'm' => 'n', + ], + ]) + // 多维 indexed 数组合并 + ->and(ArrayHelper::merge( + ['a' => ['x', 'y']], + ['a' => ['m', 'n']], + )) + ->toBe([ + 'a' => ['x', 'y', 'm', 'n'], + ]) + ; +}); \ No newline at end of file From 2c88a63845dad055443c99957a567f49d3cb4d55 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Thu, 23 Jan 2025 15:52:20 +0800 Subject: [PATCH 08/12] feature: support global config typeXxx components --- composer.json | 8 +- src/Amis/Component.php | 31 +++ src/Amis/FormField.php | 2 + src/Helper/ConfigHelper.php | 33 ++- tests/Fixtures/ClearableContainer.php | 13 + tests/Unit/Amis/ComponentTest.php | 224 ++++++++++++++++++ tests/Unit/{ => Helper}/ArrayHelperTest.php | 0 tests/Unit/Helper/ConfigHelperTest.php | 40 ++++ tests/Unit/{ => Helper}/PresetsHelperTest.php | 0 tests/config/container.php | 2 +- 10 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 tests/Fixtures/ClearableContainer.php create mode 100644 tests/Unit/Amis/ComponentTest.php rename tests/Unit/{ => Helper}/ArrayHelperTest.php (100%) create mode 100644 tests/Unit/Helper/ConfigHelperTest.php rename tests/Unit/{ => Helper}/PresetsHelperTest.php (100%) diff --git a/composer.json b/composer.json index 8835b3e..ce26b68 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,11 @@ "src/helper.php" ] }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, "require": { "php": ">=7.4", "ext-json": "*" @@ -21,7 +26,8 @@ "illuminate/pagination": "^8.83", "illuminate/validation": "^8.83", "webman-tech/polyfill": "^1.0", - "pestphp/pest": "^1.23" + "pestphp/pest": "^1.23", + "symfony/var-dumper": "^5.4" }, "config": { "allow-plugins": { diff --git a/src/Amis/Component.php b/src/Amis/Component.php index 32ae6a0..e90d2e0 100644 --- a/src/Amis/Component.php +++ b/src/Amis/Component.php @@ -2,6 +2,7 @@ namespace WebmanTech\AmisAdmin\Amis; +use Illuminate\Support\Str; use WebmanTech\AmisAdmin\Helper\ArrayHelper; use WebmanTech\AmisAdmin\Helper\ConfigHelper; use support\Container; @@ -88,6 +89,9 @@ public function toArray(): array protected function deepToArray(array $arr): array { $newArr = []; + if (isset($arr['type'])) { + $arr = $this->mergeGlobalConfigWithType($arr['type'], $arr); + } foreach ($arr as $key => $item) { if (is_array($item)) { $item = $this->deepToArray($item); @@ -119,4 +123,31 @@ protected function merge(...$arrays): array { return ArrayHelper::merge(...$arrays); } + + /** + * 合并全局的 配置 参数 + * @param string $type + * @param array $schema + * @return array + */ + protected function mergeGlobalConfigWithType(string $type, array $schema): array + { + $componentTypeName = 'type' . Str::studly($type); + $componentConfig = ConfigHelper::get('components.' . $componentTypeName, [], true); + if ($componentConfig === [] && Str::contains($type, 'static-')) { + // 支持兼容 typeStaticImage 到 typeImage 的配置 + $componentTypeName = str_replace('typeStatic', 'type', $componentTypeName); + $componentConfig = ConfigHelper::get('components.' . $componentTypeName, [], true); + } + if ($globalSchema = $componentConfig['schema'] ?? []) { + if (isset($globalSchema['type'])) { + $schema['type'] = $globalSchema['type']; // 允许做全局的 type 修改,这样可以做自定义组件 + } + $schema = $this->merge($globalSchema, $schema); + if ($schema['type'] !== $type) { + return $this->mergeGlobalConfigWithType($schema['type'], $schema); + } + } + return $schema; + } } \ No newline at end of file diff --git a/src/Amis/FormField.php b/src/Amis/FormField.php index 23739f0..97dec27 100644 --- a/src/Amis/FormField.php +++ b/src/Amis/FormField.php @@ -21,6 +21,7 @@ * @method $this visibleOn(string $expression) * @method $this required(bool $is = true) * @method $this requiredOn(string $expression) + * @method $this static (bool $is = true) * @method $this validations(array $rules) * @method $this validationErrors(array $messages) * @method $this validateOnChange(bool $is = true) @@ -96,6 +97,7 @@ class FormField extends Component 'hidden' => true, 'visible' => true, 'required' => true, + 'static' => true, 'validateOnChange' => true, ]; diff --git a/src/Helper/ConfigHelper.php b/src/Helper/ConfigHelper.php index c392fc6..46a93ac 100644 --- a/src/Helper/ConfigHelper.php +++ b/src/Helper/ConfigHelper.php @@ -5,19 +5,46 @@ /** * @internal */ -class ConfigHelper +final class ConfigHelper { public const AMIS_MODULE = '__amis_module'; + public static bool $isForTest = false; // 是否为测试 + public static array $testConfig = []; // 测试配置 + private static array $closureCache = []; // 闭包缓存 + /** * 获取配置 * @param string $key * @param null $default + * @param bool $solveClosure * @return mixed */ - public static function get(string $key, $default = null) + public static function get(string $key, $default = null, bool $solveClosure = false) { $module = request()->{self::AMIS_MODULE} ?? 'amis'; - return config("plugin.webman-tech.amis-admin.{$module}.{$key}", $default); + $cacheKey = "{$module}.{$key}"; + if (isset(self::$closureCache[$cacheKey])) { + return self::$closureCache[$cacheKey]; + } + + if (self::$isForTest) { + $value = self::$testConfig[$key] ?? $default; + } else { + $value = config("plugin.webman-tech.amis-admin.{$module}.{$key}", $default); + } + + if ($solveClosure && $value instanceof \Closure) { + $value = $value(); + self::$closureCache[$cacheKey] = $value; + } + + return $value; + } + + public static function reset() + { + self::$testConfig = []; + self::$closureCache = []; } } \ No newline at end of file diff --git a/tests/Fixtures/ClearableContainer.php b/tests/Fixtures/ClearableContainer.php new file mode 100644 index 0000000..0007f2f --- /dev/null +++ b/tests/Fixtures/ClearableContainer.php @@ -0,0 +1,13 @@ +instances = []; + } +} \ No newline at end of file diff --git a/tests/Unit/Amis/ComponentTest.php b/tests/Unit/Amis/ComponentTest.php new file mode 100644 index 0000000..bcb96e2 --- /dev/null +++ b/tests/Unit/Amis/ComponentTest.php @@ -0,0 +1,224 @@ +clear(); +}); + +test('toArray simple', function () { + $component = new Component(); + $component->schema([ + 'type' => 'text', + 'body' => 'hello', + 'body_object' => [ + 'type' => 'text', + ], + 'body_array' => [ + [ + 'type' => 'text', + ] + ], + ]); + expect($component->toArray())->toBe([ + 'type' => 'text', + 'body' => 'hello', + 'body_object' => [ + 'type' => 'text', + ], + 'body_array' => [ + [ + 'type' => 'text', + ] + ], + ]); +}); + +test('toArray with deep', function () { + $component = new Component(); + $component->schema([ + 'type' => 'text', + ]); + + // deep toArray + $component2 = new Component(); + $component2->schema([ + 'type' => 'text', + 'body' => [ + $component, + ] + ]); + expect($component2->toArray())->toBe([ + 'type' => 'text', + 'body' => [ + [ + 'type' => 'text', + ] + ] + ]); + + // deep toArray2 + $component2 = new Component(); + $component2->schema([ + 'type' => 'text', + 'body' => $component, + ]); + expect($component2->toArray())->toBe([ + 'type' => 'text', + 'body' => [ + 'type' => 'text', + ] + ]); +}); + +test('make with schema', function () { + $component = Component::make([ + 'type' => 'text', + ]); + expect($component->toArray())->toBe([ + 'type' => 'text', + ]); +}); + +test('make with no config', function () { + $component = Component::make(); + expect($component->toArray())->toBe([ + 'type' => '', + ]); +}); + +test('make with config', function () { + ConfigHelper::$testConfig = [ + 'components.' . Component::class => [ + 'schema' => [ + 'value' => '123', + ], + ], + ]; + + $component = Component::make(); + expect($component->toArray())->toBe([ + 'type' => '', + 'value' => '123', + ]); +}); + +test('toArray with components config', function () { + ConfigHelper::$testConfig = [ + 'components.typeInputText' => [ + 'schema' => [ + 'clearable' => true, + ], + ], + ]; + + $component = Component::make([ + 'type' => 'input-text', + ]); + expect($component->toArray())->toBe([ + 'clearable' => true, + 'type' => 'input-text', + ]); + + $component = FormField::make()->typeInputText(); + expect($component->toArray())->toBe([ + 'clearable' => true, + 'type' => 'input-text', + 'name' => '', + ]); +}); + +test('toArray with components config, deep', function () { + ConfigHelper::$testConfig = [ + 'components.typeText' => [ + 'schema' => [ + 'level' => 'primary', + ], + ], + ]; + + $component = Component::make([ + 'type' => 'tpl', + 'body' => [ + 'type' => 'text', + ] + ]); + expect($component->toArray())->toBe([ + 'type' => 'tpl', + 'body' => [ + 'level' => 'primary', + 'type' => 'text', + ] + ]); + + $component = Component::make([ + 'type' => 'tpl', + 'body' => [ + [ + 'type' => 'text', + ] + ] + ]); + expect($component->toArray())->toBe([ + 'type' => 'tpl', + 'body' => [ + [ + 'level' => 'primary', + 'type' => 'text', + ], + ] + ]); +}); + +test('toArray with components config, change type', function () { + // 可以用作设计自定义组件 + ConfigHelper::$testConfig = [ + 'components.typeSelectMulti' => fn() => [ + 'schema' => [ + 'type' => 'select', + 'multiple' => true, + ], + ], + 'components.typeSelect' => [ + 'schema' => [ + 'clearable' => true, + ], + ], + ]; + + $component = Component::make([ + 'type' => 'select-multi', + ]); + expect($component->toArray())->toBe([ + 'clearable' => true, + 'type' => 'select', + 'multiple' => true, + ]); +}); + +test('toArray with components config, static components', function () { + ConfigHelper::$testConfig = [ + 'components.typeImage' => fn() => [ + 'schema' => [ + 'enlargeAble' => true, + ], + ], + ]; + + $component = DetailAttribute::make()->typeImage(); + expect($component->get('type'))->toBe('static-image') + ->and($component->toArray())->toBe([ + 'enlargeAble' => true, + 'type' => 'static-image', + 'name' => '', + ]); +}); \ No newline at end of file diff --git a/tests/Unit/ArrayHelperTest.php b/tests/Unit/Helper/ArrayHelperTest.php similarity index 100% rename from tests/Unit/ArrayHelperTest.php rename to tests/Unit/Helper/ArrayHelperTest.php diff --git a/tests/Unit/Helper/ConfigHelperTest.php b/tests/Unit/Helper/ConfigHelperTest.php new file mode 100644 index 0000000..ad7f06e --- /dev/null +++ b/tests/Unit/Helper/ConfigHelperTest.php @@ -0,0 +1,40 @@ + '123', + ]; + + expect(ConfigHelper::get('abc'))->toBe('123') + ->and(ConfigHelper::get('def', '456'))->toBe('456'); +}); + +test('get with solveClosure', function () { + ConfigHelper::$testConfig = [ + 'fn' => fn() => '123', + ]; + + expect(ConfigHelper::get('fn', null, true))->toBe('123'); +}); + +test('get with solveClosure cache', function () { + $count = 0; + ConfigHelper::$testConfig = [ + 'fn' => function () use (&$count) { + $count++; + return '123'; + } + ]; + + expect(ConfigHelper::get('fn', null, true))->toBe('123') + ->and($count)->toBe(1) + ->and(ConfigHelper::get('fn', null, true))->toBe('123') + ->and($count)->toBe(1); +}); diff --git a/tests/Unit/PresetsHelperTest.php b/tests/Unit/Helper/PresetsHelperTest.php similarity index 100% rename from tests/Unit/PresetsHelperTest.php rename to tests/Unit/Helper/PresetsHelperTest.php diff --git a/tests/config/container.php b/tests/config/container.php index b1cbfee..7cbd61b 100644 --- a/tests/config/container.php +++ b/tests/config/container.php @@ -1,3 +1,3 @@ Date: Fri, 24 Jan 2025 18:03:45 +0800 Subject: [PATCH 09/12] feature: PresetsHelper refactor --- src/Amis/Component.php | 2 +- src/Amis/DetailAttribute.php | 10 +- src/Amis/FormField.php | 10 +- src/Amis/GridColumn.php | 10 +- ...appingSolver.php => ComponentCommonFn.php} | 19 +- src/Controller/AmisSourceController.php | 3 +- .../CreateUpdateFormTrait.php | 4 +- .../AmisSourceController/DetailTrait.php | 3 +- src/Helper/DTO/PresetItemDTO.php | 250 ++++++++ src/Helper/PresetsHelper.php | 304 +++------- src/Repository/AbsRepository.php | 12 +- src/Repository/EloquentRepository.php | 2 +- tests/Unit/Helper/PresetsHelperTest.php | 572 +++++++++++------- 13 files changed, 737 insertions(+), 464 deletions(-) rename src/Amis/Traits/{TypeMappingSolver.php => ComponentCommonFn.php} (59%) create mode 100644 src/Helper/DTO/PresetItemDTO.php diff --git a/src/Amis/Component.php b/src/Amis/Component.php index e90d2e0..ac85ee8 100644 --- a/src/Amis/Component.php +++ b/src/Amis/Component.php @@ -89,7 +89,7 @@ public function toArray(): array protected function deepToArray(array $arr): array { $newArr = []; - if (isset($arr['type'])) { + if (isset($arr['type']) && $arr['type']) { $arr = $this->mergeGlobalConfigWithType($arr['type'], $arr); } foreach ($arr as $key => $item) { diff --git a/src/Amis/DetailAttribute.php b/src/Amis/DetailAttribute.php index 59dc58f..8857c0f 100644 --- a/src/Amis/DetailAttribute.php +++ b/src/Amis/DetailAttribute.php @@ -2,7 +2,7 @@ namespace WebmanTech\AmisAdmin\Amis; -use WebmanTech\AmisAdmin\Amis\Traits\TypeMappingSolver; +use WebmanTech\AmisAdmin\Amis\Traits\ComponentCommonFn; /** * 详情的一个字段 @@ -38,7 +38,7 @@ */ class DetailAttribute extends Component { - use TypeMappingSolver; + use ComponentCommonFn; protected array $schema = [ 'type' => 'static', @@ -69,11 +69,7 @@ public function __call($name, $arguments) $this->schema['type'] = 'static-' . lcfirst(substr($name, 4)); $this->schema($arguments[0] ?? []); } else { - $value = $arguments[0] ?? null; - if ($value === null) { - $value = $this->defaultValue[$name] ?? null; - } - $this->schema[$name] = $value; + $this->callToSetSchema($name, $arguments); } return $this; } diff --git a/src/Amis/FormField.php b/src/Amis/FormField.php index 97dec27..97e3954 100644 --- a/src/Amis/FormField.php +++ b/src/Amis/FormField.php @@ -2,6 +2,8 @@ namespace WebmanTech\AmisAdmin\Amis; +use WebmanTech\AmisAdmin\Amis\Traits\ComponentCommonFn; + /** * 表单的一个字段 * @@ -86,6 +88,8 @@ */ class FormField extends Component { + use ComponentCommonFn; + protected array $schema = [ 'type' => 'input-text', 'name' => '', @@ -107,11 +111,7 @@ public function __call($name, $arguments) $this->schema['type'] = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '-$1', lcfirst(substr($name, 4)))); $this->schema($arguments[0] ?? []); } else { - $value = $arguments[0] ?? null; - if ($value === null) { - $value = $this->defaultValue[$name] ?? null; - } - $this->schema[$name] = $value; + $this->callToSetSchema($name, $arguments); } return $this; } diff --git a/src/Amis/GridColumn.php b/src/Amis/GridColumn.php index 5658992..4648a98 100644 --- a/src/Amis/GridColumn.php +++ b/src/Amis/GridColumn.php @@ -2,7 +2,7 @@ namespace WebmanTech\AmisAdmin\Amis; -use WebmanTech\AmisAdmin\Amis\Traits\TypeMappingSolver; +use WebmanTech\AmisAdmin\Amis\Traits\ComponentCommonFn; /** * table 的单个 column @@ -38,7 +38,7 @@ */ class GridColumn extends Component { - use TypeMappingSolver; + use ComponentCommonFn; protected array $schema = [ 'type' => 'text', @@ -97,11 +97,7 @@ public function __call($name, $arguments) $this->schema['type'] = lcfirst(substr($name, 4)); $this->schema($arguments[0] ?? []); } else { - $value = $arguments[0] ?? null; - if ($value === null) { - $value = $this->defaultValue[$name] ?? null; - } - $this->schema[$name] = $value; + $this->callToSetSchema($name, $arguments); } return $this; } diff --git a/src/Amis/Traits/TypeMappingSolver.php b/src/Amis/Traits/ComponentCommonFn.php similarity index 59% rename from src/Amis/Traits/TypeMappingSolver.php rename to src/Amis/Traits/ComponentCommonFn.php index de42728..c2e582a 100644 --- a/src/Amis/Traits/TypeMappingSolver.php +++ b/src/Amis/Traits/ComponentCommonFn.php @@ -5,8 +5,25 @@ /** * @internal */ -trait TypeMappingSolver +trait ComponentCommonFn { + private function callToSetSchema(string $name, array $arguments) + { + $value = $arguments[0] ?? null; + if ($value === null) { + $value = $this->defaultValue[$name] ?? null; + } + if ($value === null) { + unset($this->schema[$name]); + } else { + $this->schema[$name] = $value; + } + } + + /** + * 处理 mapping 类型的 map + * @return void + */ private function solveMappingMap() { if (isset($this->schema['map']) && !is_array($this->schema['map'][0] ?? null)) { diff --git a/src/Controller/AmisSourceController.php b/src/Controller/AmisSourceController.php index 3d9f9f1..dac40fe 100644 --- a/src/Controller/AmisSourceController.php +++ b/src/Controller/AmisSourceController.php @@ -6,6 +6,7 @@ use Webman\Http\Response; use WebmanTech\AmisAdmin\Amis; use WebmanTech\AmisAdmin\Amis\Component; +use WebmanTech\AmisAdmin\Repository\AbsRepository; use WebmanTech\AmisAdmin\Repository\HasPresetInterface; use WebmanTech\AmisAdmin\Repository\RepositoryInterface; @@ -148,7 +149,7 @@ protected function grid(): array { $repository = $this->repository(); if ($repository instanceof HasPresetInterface) { - return $repository->getPresetsHelper()->pickGrid(); + return $repository->getPresetsHelper()->withScene(AbsRepository::SCENE_LIST)->pickGrid(); } return [ diff --git a/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php b/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php index 2718a9f..028e791 100644 --- a/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php +++ b/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php @@ -16,7 +16,7 @@ protected function form(string $scene): array { $repository = $this->repository(); if ($repository instanceof HasPresetInterface) { - return $repository->getPresetsHelper()->pickForm($scene); + return $repository->getPresetsHelper()->withScene($scene)->pickForm(); } return [ @@ -53,4 +53,4 @@ protected function buildFormFields(array $formFields): array unset($item); return $formFields; } -} \ No newline at end of file +} diff --git a/src/Controller/Traits/AmisSourceController/DetailTrait.php b/src/Controller/Traits/AmisSourceController/DetailTrait.php index 3aab75a..07d49b5 100644 --- a/src/Controller/Traits/AmisSourceController/DetailTrait.php +++ b/src/Controller/Traits/AmisSourceController/DetailTrait.php @@ -6,6 +6,7 @@ use Webman\Http\Response; use WebmanTech\AmisAdmin\Amis; use WebmanTech\AmisAdmin\Exceptions\ActionDisableException; +use WebmanTech\AmisAdmin\Repository\AbsRepository; use WebmanTech\AmisAdmin\Repository\HasPresetInterface; trait DetailTrait @@ -69,7 +70,7 @@ protected function detail(): array { $repository = $this->repository(); if ($repository instanceof HasPresetInterface) { - return $repository->getPresetsHelper()->pickDetail(); + return $repository->getPresetsHelper()->withScene(AbsRepository::SCENE_DETAIL)->pickDetail(); } return [ diff --git a/src/Helper/DTO/PresetItemDTO.php b/src/Helper/DTO/PresetItemDTO.php new file mode 100644 index 0000000..a44877e --- /dev/null +++ b/src/Helper/DTO/PresetItemDTO.php @@ -0,0 +1,250 @@ + true, // 默认是否可以编辑 + ]; + + public function __construct(string $key, array $define, array $config = []) + { + $this->key = $key; + $this->config = array_merge($this->config, $config); + $this->define = $define; + } + + /** + * 全部的 define 配置 + * @return array + */ + protected function getDefaultDefine(): array + { + return [ + 'label' => null, + 'labelRemark' => null, + 'description' => null, + 'filter' => true, + 'grid' => true, + 'gridExt' => null, + 'gridExtDynamic' => null, + 'form' => $this->config['defaultCanEdit'] ? true : null, + 'formExt' => null, + 'formExtDynamic' => null, + 'rule' => $this->config['defaultCanEdit'] ? 'nullable' : null, // 默认为 nullable,使得不填 rule 时可以正常提交和传递数据 + 'ruleMessages' => null, + 'ruleCustomAttribute' => null, + 'detail' => true, + 'detailExt' => null, + 'detailExtDynamic' => null, + 'selectOptions' => null, + ]; + } + + /** + * 根据 name 获取 define 配置 + * @param string $name + * @return mixed|null + */ + protected function getDefineValueByName(string $name) + { + return array_key_exists($name, $this->define) + ? $this->define[$name] + : ($this->getDefaultDefine()[$name] ?? null); + } + + protected string $scene = self::SCENE_DEFAULT; + + public function withScene(?string $scene) + { + $this->scene = $scene ?? self::SCENE_DEFAULT; + return $this; + } + + public function __get($name) + { + if (!array_key_exists($name, $this->attributes)) { + $value = value($this->getDefineValueByName($name), $this->key); + if ($value === null) { + $value = self::NULL_VALUE; + } else { + $methodName = 'build' . ucfirst($name); + if (method_exists($this, $methodName)) { + $value = $this->{$methodName}($value); + } + } + $this->attributes[$name] = $value; + } + + $value = $this->attributes[$name]; + $value = $this->callExt($name . 'ExtDynamic', $value, true); + + return $value === self::NULL_VALUE ? null : $value; + } + + protected function buildFilter($value) + { + if ($value === '=' || $value === true) { + return fn($query, $value, $attribute) => $query->where($attribute, $value); + } + if ($value === 'like') { + return fn($query, $value, $attribute) => $query->where($attribute, 'like', '%' . $value . '%'); + } + if ($value === 'datetime-range') { + return fn($query, $value, $attribute) => $query + ->whereBetween($attribute, array_map( + fn($timestamp) => date('Y-m-d H:i:s', (int)$timestamp), + explode(',', $value) + )); + } + // TODO 扩展其他 filter,或者方式(比如 static 直接注入?) + return $value; + } + + protected function buildGrid($value) + { + if ($value === true) { + $value = GridColumn::make() + ->name($this->key) + ->label($this->label); + if (($info = $this->getSelectOptionsInfo()) !== null) { + $value->typeMapping(['map' => $info['map']]); + } + if ($this->filter !== null) { + $value->searchable(); + } + } + if ($value instanceof Component) { + $value = $this->callExt('gridExt', $value); + } + return $value; + } + + protected function buildForm($value) + { + if ($value === true) { + $value = FormField::make() + ->name($this->key) + ->label($this->label) + ->labelRemark($this->labelRemark) + ->description($this->description); + if (($info = $this->getSelectOptionsInfo()) !== null) { + $value->typeSelect(['options' => $info['options']]); + } + if (in_array('required', $this->rule, true)) { + $value->required(); + } + } + if ($value instanceof Component) { + $value = $this->callExt('formExt', $value); + } + return $value; + } + + protected function buildDetail($value) + { + if ($value === true) { + $value = DetailAttribute::make() + ->name($this->key) + ->label($this->label); + if (($info = $this->getSelectOptionsInfo()) !== null) { + $value->typeMapping(['map' => $info['map']]); + } + } + if ($value instanceof Component) { + $value = $this->callExt('detailExt', $value); + } + return $value; + } + + protected function buildRule($value) + { + if (is_string($value)) { + $value = array_values(array_filter(explode('|', $value))); + } + return $value; + } + + /** + * @var false|array|null + */ + protected $selectOptionsInfo = false; + + protected function getSelectOptionsInfo(): ?array + { + if ($this->selectOptionsInfo !== false) { + return $this->selectOptionsInfo; + } + $defineSelectOptions = value($this->getDefineValueByName('selectOptions')); + if ($defineSelectOptions === null) { + return $this->selectOptionsInfo = null; + } + + $data = [ + 'map' => [], + 'options' => [], + 'values' => [], + ]; + if (!is_array($defineSelectOptions)) { + throw new \InvalidArgumentException('selectOptions must be an array or Closure return array'); + } + if (isset($defineSelectOptions[0]['value'])) { + // 二维数组的形式 + $data['options'] = $defineSelectOptions; + $data['map'] = array_column($defineSelectOptions, 'label', 'value'); + } else { + // map 的形式 + $data['map'] = $defineSelectOptions; + foreach ($defineSelectOptions as $value => $label) { + $data['options'][] = [ + 'value' => (string)$value, // 强制为 string,保证行为一致 + 'label' => strip_tags($label), // 去除 html + ]; + } + } + $data['values'] = array_keys($data['map']); + + return $this->selectOptionsInfo = $data; + } + + protected function callExt(string $name, $value, bool $useScene = false) + { + $ext = $this->getDefineValueByName($name) ?? null; + if ($ext) { + if ($useScene) { + $result = $ext($value, $this->scene); + } else { + $result = $ext($value); + } + if ($result !== null) { + return $result; + } + } + return $value; + } +} \ No newline at end of file diff --git a/src/Helper/PresetsHelper.php b/src/Helper/PresetsHelper.php index 11796db..b58640c 100644 --- a/src/Helper/PresetsHelper.php +++ b/src/Helper/PresetsHelper.php @@ -2,24 +2,28 @@ namespace WebmanTech\AmisAdmin\Helper; -use Illuminate\Support\Collection; -use WebmanTech\AmisAdmin\Amis\DetailAttribute; -use WebmanTech\AmisAdmin\Amis\FormField; -use WebmanTech\AmisAdmin\Amis\GridColumn; +use WebmanTech\AmisAdmin\Helper\DTO\PresetItemDTO; use WebmanTech\AmisAdmin\Repository\AbsRepository; class PresetsHelper { + private const SCENE_DEFAULT = 'default'; + + protected bool $defaultNoEdit = false; + /** + * @var array + */ protected array $presets = []; - protected ?bool $defaultNoEdit = null; protected array $sceneKeys = []; /** - * @param array $presets + * 默认不启用编辑(只读) + * @return $this */ - public function __construct(array $presets = []) + public function withDefaultNoEdit() { - $this->withPresets($presets); + $this->defaultNoEdit = true; + return $this; } /** @@ -29,20 +33,42 @@ public function __construct(array $presets = []) */ public function withPresets(array $presets) { - $this->presets = array_merge($this->presets, $presets); + foreach ($presets as $attribute => $define) { + if (!$define instanceof PresetItemDTO) { + $define = new PresetItemDTO($attribute, $define, [ + 'defaultCanEdit' => !$this->defaultNoEdit, + ]); + } + $this->presets[$attribute] = $define; + } return $this; } /** - * 默认不启用编辑(只读) + * 设置场景对应的字段 + * @param array $data * @return $this */ - public function withDefaultNoEdit() + public function withSceneKeys(array $data) { - $this->defaultNoEdit = true; + foreach ($data as $scene => $keys) { + $this->sceneKeys[$scene] = $keys; + } return $this; } + /** + * 设置默认场景对应的字段 + * @param array $keys + * @return $this + */ + public function withDefaultSceneKeys(array $keys) + { + return $this->withSceneKeys([ + self::SCENE_DEFAULT => $keys, + ]); + } + /** * 设置 CRUD 场景对应的字段 * @param array $keys @@ -58,16 +84,11 @@ public function withCrudSceneKeys(array $keys) ]); } - /** - * 设置场景对应的字段 - * @param array $data - * @return $this - */ - public function withSceneKeys(array $data) + protected string $scene = self::SCENE_DEFAULT; + + public function withScene(?string $scene = null) { - foreach ($data as $scene => $keys) { - $this->sceneKeys[$scene] = $keys; - } + $this->scene = $scene ?? self::SCENE_DEFAULT; return $this; } @@ -78,7 +99,7 @@ public function withSceneKeys(array $data) */ public function pickLabel(?array $keys = null): array { - return $this->pickColumn(null, 'label', $keys, null, true); + return $this->pickColumn('label', $keys, true); } /** @@ -88,7 +109,7 @@ public function pickLabel(?array $keys = null): array */ public function pickLabelRemark(?array $keys = null): array { - return $this->pickColumn(null, 'labelRemark', $keys, null, true); + return $this->pickColumn('labelRemark', $keys, true); } /** @@ -98,7 +119,7 @@ public function pickLabelRemark(?array $keys = null): array */ public function pickDescription(?array $keys = null): array { - return $this->pickColumn(null, 'description', $keys, null, true); + return $this->pickColumn('description', $keys, true); } /** @@ -108,22 +129,7 @@ public function pickDescription(?array $keys = null): array */ public function pickFilter(?array $keys = null): array { - return $this->pickColumn(AbsRepository::SCENE_LIST, 'filter', $keys, function ($v) { - if ($v === '=' || $v === true) { - return fn($query, $value, $attribute) => $query->where($attribute, $value); - } - if ($v === 'like') { - return fn($query, $value, $attribute) => $query->where($attribute, 'like', '%' . $value . '%'); - } - if ($v === 'datetime-range') { - return fn($query, $value, $attribute) => $query - ->whereBetween($attribute, array_map( - fn($timestamp) => date('Y-m-d H:i:s', (int)$timestamp), - explode(',', $value) - )); - } - return $v; - }, true); + return $this->pickColumn('filter', $keys, true); } /** @@ -133,54 +139,17 @@ public function pickFilter(?array $keys = null): array */ public function pickGrid(?array $keys = null): array { - return $this->pickColumn(AbsRepository::SCENE_LIST, 'grid', $keys, function ($v, string $column, array $columnConfig) { - if ($v === null) { - return null; - } - if ($v === true) { - $value = GridColumn::make()->name($column); - if (($selectOptions = $this->getSelectOptionsFromColumnConfig($columnConfig)) !== null) { - $value->typeMapping(['map' => $selectOptions['map']]); - } - if ($this->isColumnSearchable($column)) { - $value->searchable(); - } - if (($ext = $columnConfig['gridExt']) instanceof \Closure) { - $ext($value); - } - return $value; - } - return $v($column); - }); + return $this->pickColumn('grid', $keys); } /** * 获取字段对应的 form - * @param string $scene * @param array|null $keys * @return array */ - public function pickForm(string $scene, ?array $keys = null): array + public function pickForm(?array $keys = null): array { - $items = $this->pickColumn($scene, 'form', $keys, function ($v, string $column, array $columnConfig) use ($scene) { - if ($v === null) { - return null; - } - if ($v === true) { - $value = FormField::make()->name($column); - if (($selectOptions = $this->getSelectOptionsFromColumnConfig($columnConfig)) !== null) { - $value->typeSelect(['options' => $selectOptions['options']]); - } - if ($this->isColumnRequired($column, $scene)) { - $value->required(); - } - if (($ext = $columnConfig['formExt']) instanceof \Closure) { - $ext($value, $scene); - } - return $value; - } - return $v($column, $scene); - }); + $items = $this->pickColumn('form', $keys); // 允许同时展示多个 FormItem $data = []; foreach ($items as $item) { @@ -195,55 +164,32 @@ public function pickForm(string $scene, ?array $keys = null): array /** * 获取字段对应的 rules - * @param string $scene * @param array|null $keys * @return array */ - public function pickRules(string $scene, ?array $keys = null): array + public function pickRules(?array $keys = null): array { - return $this->pickColumn($scene, 'rule', $keys, function ($v, string $column) use ($scene) { - if ($v instanceof \Closure) { - return $v($scene, $column); - } - return $v; - }, true); + return $this->pickColumn('rule', $keys, true); } /** * 获取字段对应的 ruleMessages - * @param string $scene * @param array|null $keys * @return array */ - public function pickRuleMessages(string $scene, ?array $keys = null): array + public function pickRuleMessages(?array $keys = null): array { - $items = $this->pickColumn($scene, 'ruleMessages', $keys, function ($v, string $column) use ($scene) { - if ($v instanceof \Closure) { - return $v($scene, $column); - } - return $v; - }); - $data = []; - foreach ($items as $key => $value) { - $data[$key] = $value; - } - return $data; + return $this->pickColumn('ruleMessages', $keys, true); } /** * 获取字段对应的 ruleCustomAttributes - * @param string $scene * @param array|null $keys * @return array */ - public function pickRuleCustomAttributes(string $scene, ?array $keys = null): array + public function pickRuleCustomAttributes(?array $keys = null): array { - return $this->pickColumn($scene, 'ruleCustomAttribute', $keys, function ($v, string $column) use ($scene) { - if ($v instanceof \Closure) { - return $v($scene, $column); - } - return $v; - }, true); + return $this->pickColumn('ruleCustomAttribute', $keys, true); } /** @@ -253,118 +199,62 @@ public function pickRuleCustomAttributes(string $scene, ?array $keys = null): ar */ public function pickDetail(?array $keys = null): array { - return $this->pickColumn(AbsRepository::SCENE_DETAIL, 'detail', $keys, function ($v, string $column, array $columnConfig) { - if ($v === true) { - $value = DetailAttribute::make()->name($column); - if (($selectOptions = $this->getSelectOptionsFromColumnConfig($columnConfig)) !== null) { - $value->typeMapping(['map' => $selectOptions['map']]); - } - if (($ext = $columnConfig['detailExt']) instanceof \Closure) { - $ext($value); - } - return $value; - } - if ($v instanceof \Closure) { - return $v($column); - } - return $v; - }); + return $this->pickColumn('detail', $keys); } - protected ?Collection $formattedPresets = null; - - protected function pickColumn(?string $scene, string $type, ?array $keys = null, ?callable $fnForValue = null, bool $keepKey = false): array + /** + * 获取 presetItems + * @param array|null $keys + * @return array + */ + protected function getPresetItems(?array $keys = null): array { - if ($this->formattedPresets === null) { - $this->formattedPresets = collect($this->presets) - ->map(function (array $item) { - return array_merge($this->getDefaultColumnConfig(), $item); - }); - } - if ($keys === null && $scene !== null) { - $keys = $this->sceneKeys[$scene] ?? null; - } - - $data = $this->formattedPresets - ->only($keys) - ->map(function (array $item, string $key) use ($type, $fnForValue) { - $v = $item[$type]; - if ($fnForValue !== null) { - $v = $fnForValue($v, $key, $item); - } - return $v; - }) - ->filter(fn($v) => $v !== null) - ->only($keys) - ->toArray(); + $keys = $this->getKeys($keys); - // 按照给定的 key 排序 - if ($keys !== null) { - $data = array_merge(array_flip(array_intersect($keys, array_keys($data))), $data); + $data = []; + foreach ($keys as $key) { + if (!isset($this->presets[$key])) { + continue; + } + $data[$key] = $this->presets[$key]; } - return $keepKey ? $data : array_values($data); - } - - protected function getDefaultColumnConfig(): array - { - return [ - 'label' => null, - 'labelRemark' => null, - 'description' => null, - 'filter' => true, - 'grid' => true, - 'gridExt' => null, - 'form' => $this->defaultNoEdit ? null : true, - 'formExt' => null, - 'selectOptions' => null, - 'rule' => $this->defaultNoEdit ? null : 'nullable', // 默认为 nullable,使得不填 rule 时可以正常提交和传递数据 - 'ruleMessages' => null, - 'ruleCustomAttribute' => null, - 'detail' => true, - 'detailExt' => null, - ]; - } - - protected function isColumnSearchable(string $column): bool - { - return isset($this->pickFilter([$column])[$column]); + return $data; } - protected function isColumnRequired(string $column, string $scene): bool + /** + * 获取场景对应的 keys + * @param array|null $keys + * @return array + */ + private function getKeys(?array $keys = null): array { - $rules = $this->pickRules($scene, [$column])[$column] ?? ''; - if (is_string($rules)) { - $rules = array_filter(explode('|', $rules)); + if ($keys === null) { + $keys = $this->sceneKeys[$this->scene] ?? array_keys($this->presets); } - return in_array('required', $rules, true); + return $keys; } - protected function getSelectOptionsFromColumnConfig(array &$columnConfig): ?array + /** + * 提取某个类型的数据 + * @param string $type + * @param array|null $keys + * @param bool $keepKey + * @return array + */ + protected function pickColumn(string $type, ?array $keys = null, bool $keepKey = false): array { - if ($columnConfig['selectOptions'] === null) { - return null; - } - if (is_array($columnConfig['selectOptions']) && isset($columnConfig['selectOptions']['map'])) { - return $columnConfig['selectOptions']; - } - if ($columnConfig['selectOptions'] instanceof \Closure) { - $columnConfig['selectOptions'] = $columnConfig['selectOptions'](); + $items = $this->getPresetItems($keys); + $data = []; + foreach ($items as $key => $item) { + $value = $item->withScene($this->scene)->{$type}; + if ($value !== null) { + $data[$key] = $value; + } } - - $map = $columnConfig['selectOptions']; - $options = []; - foreach ($map as $value => $label) { - $options[] = [ - 'value' => (string)$value, // 强制为 string,保证行为一致 - 'label' => strip_tags($label), // 去除 html - ]; + if (!$keepKey) { + $data = array_values($data); } - - return $columnConfig['selectOptions'] = [ - 'map' => $map, - 'options' => $options, - 'values' => array_keys($map), - ]; + return $data; } } diff --git a/src/Repository/AbsRepository.php b/src/Repository/AbsRepository.php index faa299f..2f00fe2 100644 --- a/src/Repository/AbsRepository.php +++ b/src/Repository/AbsRepository.php @@ -40,7 +40,7 @@ public function getLabel(string $attribute): string protected function attributeLabels(): array { if ($this instanceof HasPresetInterface) { - return $this->getPresetsHelper()->pickLabel(); + return $this->getPresetsHelper()->withScene()->pickLabel(); } return []; @@ -61,7 +61,7 @@ public function getLabelRemark(string $attribute) protected function attributeLabelRemarks(): array { if ($this instanceof HasPresetInterface) { - return $this->getPresetsHelper()->pickLabelRemark(); + return $this->getPresetsHelper()->withScene()->pickLabelRemark(); } return []; @@ -82,7 +82,7 @@ public function getDescription(string $attribute) protected function attributeDescriptions(): array { if ($this instanceof HasPresetInterface) { - return $this->getPresetsHelper()->pickDescription(); + return $this->getPresetsHelper()->withScene()->pickDescription(); } return []; @@ -186,7 +186,7 @@ protected function validator(): ValidatorInterface protected function rules(string $scene): array { if ($this instanceof HasPresetInterface) { - return $this->getPresetsHelper()->pickRules($scene); + return $this->getPresetsHelper()->withScene($scene)->pickRules(); } return []; @@ -199,7 +199,7 @@ protected function rules(string $scene): array protected function ruleMessages(string $scene): array { if ($this instanceof HasPresetInterface) { - return $this->getPresetsHelper()->pickRuleMessages($scene); + return $this->getPresetsHelper()->withScene($scene)->pickRuleMessages(); } return []; @@ -212,7 +212,7 @@ protected function ruleMessages(string $scene): array protected function ruleCustomAttributes(string $scene): array { if ($this instanceof HasPresetInterface) { - return $this->getPresetsHelper()->pickRuleCustomAttributes($scene); + return $this->getPresetsHelper()->withScene($scene)->pickRuleCustomAttributes(); } return $this->attributeLabels(); diff --git a/src/Repository/EloquentRepository.php b/src/Repository/EloquentRepository.php index 392fad3..e036f66 100644 --- a/src/Repository/EloquentRepository.php +++ b/src/Repository/EloquentRepository.php @@ -121,7 +121,7 @@ protected function buildSearch(EloquentBuilder $query, array $search): EloquentB protected function searchableAttributes(): array { if ($this instanceof HasPresetInterface) { - return $this->getPresetsHelper()->pickFilter(); + return $this->getPresetsHelper()->withScene(AbsRepository::SCENE_LIST)->pickFilter(); } // 表下的所有字段可搜索 diff --git a/tests/Unit/Helper/PresetsHelperTest.php b/tests/Unit/Helper/PresetsHelperTest.php index f25c456..03cb90b 100644 --- a/tests/Unit/Helper/PresetsHelperTest.php +++ b/tests/Unit/Helper/PresetsHelperTest.php @@ -12,285 +12,407 @@ function components_to_array(array $items): array return array_map(fn(Component $item) => $item->toArray(), $items); } -function components_to_json(array $items): string -{ - return json_encode(components_to_array($items)); -} +beforeEach(function () { + $this->presetsHelper = new PresetsHelper(); +}); -test('simple', function () { - $presetsHelper = new PresetsHelper([ +test('support withPresets', function () { + $presetsHelper = $this->presetsHelper; + expect(array_keys($presetsHelper->pickLabel()))->toBe([]); + + $presetsHelper->withPresets([ 'id' => [ 'label' => 'ID', - 'labelRemark' => 'ID remark', - 'description' => 'ID description', - 'filter' => '=', - 'grid' => fn(string $column) => GridColumn::make()->name($column)->searchable(), - 'form' => null, - 'detail' => true, - ], - 'code' => [ - 'label' => '编码', - 'filter' => '=', - 'grid' => fn(string $column) => GridColumn::make()->name($column)->searchable(), - 'form' => fn(string $column, string $scene) => FormField::make()->name($column)->required($scene === AbsRepository::SCENE_CREATE), - 'detail' => true, - 'rule' => 'required|string|max:64', ], ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID']); +}); - expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => '编码']) - ->and($presetsHelper->pickLabelRemark())->toBe(['id' => 'ID remark']) - ->and($presetsHelper->pickDescription())->toBe(['id' => 'ID description']) - ->and(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ - GridColumn::make()->name('id')->searchable(), - GridColumn::make()->name('code')->searchable() - ])) - ->and(array_keys($presetsHelper->pickFilter()))->toBe(['id', 'code']) // 无法比较匿名函数 - ->and(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ - FormField::make()->name('code')->required() - ])) - ->and(components_to_array($presetsHelper->pickDetail()))->toBe(components_to_array([ - DetailAttribute::make()->name('id'), - DetailAttribute::make()->name('code'), - ])) - ->and($presetsHelper->pickRules(AbsRepository::SCENE_CREATE))->toBe([ - 'id' => 'nullable', - 'code' => 'required|string|max:64', +test('support withDefaultNoEdit', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'before' => [], + ]) + ->withDefaultNoEdit() // 该配置之前的定义不受影响 + ->withPresets([ + 'after' => [], ]); + expect($presetsHelper->pickForm())->toHaveCount(1) + ->and($presetsHelper->pickForm()[0]->get('name'))->toBe('before') + ->and($presetsHelper->pickGrid())->toHaveCount(2) + ->and($presetsHelper->pickDetail())->toHaveCount(2); }); -test('default', function () { - $presetsHelper = new PresetsHelper([ - 'id' => [], - 'code' => [ - 'rule' => 'required|string|max:64', - ], - 'code2' => [ - 'filter' => null, - 'rule' => fn(string $scene) => array_values(array_filter([ - $scene === AbsRepository::SCENE_CREATE ? 'required' : null, - 'string' - ])), - ] +test('support withDefaultSceneKeys', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => ['label' => 'ID'], + 'code' => ['label' => 'Code'] + ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => 'Code']); + $presetsHelper->withDefaultSceneKeys(['id']); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID']); +}); + +test('support withCrudSceneKeys', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => ['label' => 'ID'], + 'code' => ['label' => 'Code'] + ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => 'Code']); + $presetsHelper->withCrudSceneKeys(['id']); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => 'Code']) + ->and($presetsHelper->withScene(AbsRepository::SCENE_CREATE)->pickLabel())->toBe(['id' => 'ID']) + ->and($presetsHelper->withScene(AbsRepository::SCENE_CREATE)->pickLabel())->toBe(['id' => 'ID']) + ->and($presetsHelper->withScene(AbsRepository::SCENE_UPDATE)->pickLabel())->toBe(['id' => 'ID']) + ->and($presetsHelper->withScene(AbsRepository::SCENE_DETAIL)->pickLabel())->toBe(['id' => 'ID']); +}); + +test('support withSceneKeys', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => ['label' => 'ID'], + 'code' => ['label' => 'Code'] + ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => 'Code']); + $presetsHelper->withSceneKeys([ + 'scene_abc' => ['id'], + 'scene_xyz' => ['code'], ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => 'Code']) + ->and($presetsHelper->withScene('scene_abc')->pickLabel())->toBe(['id' => 'ID']) + ->and($presetsHelper->withScene('scene_xyz')->pickLabel())->toBe(['code' => 'Code']); +}); - expect($presetsHelper->pickLabel())->toBe([]) - ->and(array_keys($presetsHelper->pickFilter()))->toBe(['id', 'code']) // 无法比较匿名函数 - ->and(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ - GridColumn::make()->name('id')->searchable(), - GridColumn::make()->name('code')->searchable(), - GridColumn::make()->name('code2'), - ])) - ->and(components_to_array($presetsHelper->pickDetail()))->toBe(components_to_array([ - DetailAttribute::make()->name('id'), - DetailAttribute::make()->name('code'), - DetailAttribute::make()->name('code2'), - ])) - ->and($presetsHelper->pickRules(AbsRepository::SCENE_CREATE))->toBe([ - 'id' => 'nullable', - 'code' => 'required|string|max:64', - 'code2' => ['required', 'string'], - ]) - ->and($presetsHelper->pickRules(AbsRepository::SCENE_UPDATE))->toBe([ - 'id' => 'nullable', - 'code' => 'required|string|max:64', - 'code2' => ['string'], +test('support withScene', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => ['label' => 'ID'], + 'code' => ['label' => 'Code'] ]) - ->and(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ - FormField::make()->name('id'), - FormField::make()->name('code')->required(), - FormField::make()->name('code2')->required(), - ])) - ->and(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_UPDATE)))->toBe(components_to_array([ - FormField::make()->name('id'), - FormField::make()->name('code')->required(), - FormField::make()->name('code2'), - ])); + ->withSceneKeys([ + 'scene_abc' => ['id'] + ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => 'Code']) + ->and($presetsHelper->withScene('scene_abc')->pickLabel())->toBe(['id' => 'ID']) + ->and($presetsHelper->pickLabel())->toBe(['id' => 'ID']) // withScene 具有副作用,会更改全局当前的 scene + ->and($presetsHelper->withScene()->pickLabel())->toBe(['id' => 'ID', 'code' => 'Code']); // 使用 withScene(null) 重置 scene }); -test('check withPresets', function () { - $presetsHelper = new PresetsHelper(); - expect(array_keys($presetsHelper->pickLabel()))->toBe([]); +test('support label', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => [ + 'label' => 'ID', + ], + ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID']) + ->and($presetsHelper->pickGrid(['id'])[0]->get('label'))->toBe('ID') + ->and($presetsHelper->pickForm(['id'])[0]->get('label'))->toBe('ID') + ->and($presetsHelper->pickDetail(['id'])[0]->get('label'))->toBe('ID'); +}); - $presetsHelper->withPresets([ - 'id' => [ - 'label' => 'ID', - ], - ]); - expect($presetsHelper->pickLabel())->toBe([]); // 后更改的无效 +test('support labelRemark', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => [ + 'labelRemark' => 'ID', + ], + ]); + expect($presetsHelper->pickLabelRemark())->toBe(['id' => 'ID']) + ->and($presetsHelper->pickGrid(['id'])[0]->get('labelRemark'))->toBeNull() + ->and($presetsHelper->pickForm(['id'])[0]->get('labelRemark'))->toBe('ID') + ->and($presetsHelper->pickDetail(['id'])[0]->get('labelRemark'))->toBeNull(); +}); - $presetsHelper = (new PresetsHelper()) +test('support description', function () { + $presetsHelper = $this->presetsHelper ->withPresets([ 'id' => [ - 'label' => 'ID', + 'description' => 'ID', ], ]); - expect(array_keys($presetsHelper->pickLabel()))->toBe(['id']); + expect($presetsHelper->pickDescription())->toBe(['id' => 'ID']) + ->and($presetsHelper->pickGrid(['id'])[0]->get('description'))->toBeNull() + ->and($presetsHelper->pickForm(['id'])[0]->get('description'))->toBe('ID') + ->and($presetsHelper->pickDetail(['id'])[0]->get('description'))->toBeNull(); }); -test('check withDefaultNoEdit', function () { - $presetsHelper = (new PresetsHelper([ - 'id' => [ - 'label' => 'ID', - ], - 'code' => [ - 'label' => '编码', - ] - ]))->withDefaultNoEdit(); - expect(array_keys($presetsHelper->pickLabel()))->toBe(['id', 'code']) - ->and($presetsHelper->pickForm(AbsRepository::SCENE_CREATE))->toBe([]) - ->and($presetsHelper->pickForm(AbsRepository::SCENE_UPDATE))->toBe([]) - ->and(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ - GridColumn::make()->name('id')->searchable(), - GridColumn::make()->name('code')->searchable(), - ])) - ->and(components_to_array($presetsHelper->pickDetail()))->toBe(components_to_array([ - DetailAttribute::make()->name('id'), - DetailAttribute::make()->name('code'), - ])); +test('support filter', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'key0' => [ + ], + 'key1' => [ + 'filter' => true, + ], + 'key2' => [ + 'filter' => '=', + ], + 'key3' => [ + 'filter' => 'like', + ], + 'key4' => [ + 'filter' => null, + ], + ]); + $filters = $presetsHelper->pickFilter(); + expect($filters['key0'])->toBeInstanceOf(Closure::class) + ->and($filters['key1'])->toBeInstanceOf(Closure::class) + ->and($filters['key2'])->toBeInstanceOf(Closure::class) + ->and($filters['key3'])->toBeInstanceOf(Closure::class) + ->and(array_key_exists('key4', $filters))->toBeFalse(); }); -test('check withCrudSceneKeys', function () { - $presetsHelper = (new PresetsHelper([ - 'id' => [ - 'label' => 'ID', - ], - 'code' => [ - 'label' => '编码', - ] - ]))->withCrudSceneKeys(['id']); - expect(array_keys($presetsHelper->pickLabel()))->toBe(['id', 'code']) - ->and(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ - FormField::make()->name('id'), - ])); +test('support grid', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'default' => [ + ], + 'change_grid' => [ + 'grid' => fn(string $key) => GridColumn::make()->name($key), + ], + 'ext_grid' => [ + 'gridExt' => fn(GridColumn $column) => $column->sortable(), + ], + 'no_filter' => [ + 'filter' => null, + ], + 'hidden' => [ + 'grid' => null, + ], + ]); + expect(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ + GridColumn::make()->name('default')->searchable(), + GridColumn::make()->name('change_grid'), + GridColumn::make()->name('ext_grid')->searchable()->sortable(), + GridColumn::make()->name('no_filter'), + ])); }); -test('check withSceneKeys', function () { - $presetsHelper = (new PresetsHelper([ +test('support form', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'default' => [ + ], + 'change_form' => [ + 'form' => fn(string $key) => FormField::make()->name($key), + ], + 'ext_form' => [ + 'formExt' => fn(FormField $field) => $field->hidden(), + ], + 'auto_required' => [ + 'rule' => 'required', + ], + 'hidden' => [ + 'form' => null, + ], + ]); + expect(components_to_array($presetsHelper->pickForm()))->toBe(components_to_array([ + FormField::make()->name('default'), + FormField::make()->name('change_form'), + FormField::make()->name('ext_form')->hidden(), + FormField::make()->name('auto_required')->required(), + ])); +}); + +test('support detail', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'default' => [ + ], + 'change_detail' => [ + 'detail' => fn(string $key) => DetailAttribute::make()->name($key), + ], + 'ext_detail' => [ + 'detailExt' => fn(DetailAttribute $attribute) => $attribute->typeImage(), + ], + 'hidden' => [ + 'detail' => null, + ], + ]); + expect(components_to_array($presetsHelper->pickDetail()))->toBe(components_to_array([ + DetailAttribute::make()->name('default'), + DetailAttribute::make()->name('change_detail'), + DetailAttribute::make()->name('ext_detail')->typeImage(), + ])); +}); + +test('support rule', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'default' => [ + ], + 'change_rule' => [ + 'rule' => 'required', + ], + 'change_rule2' => [ + 'rule' => 'required|string', + ], + 'callback_rule' => [ + 'rule' => fn() => 'required', + ], + 'hidden' => [ + 'rule' => null, + ], + ]); + expect($presetsHelper->pickRules())->toBe([ + 'default' => ['nullable'], + 'change_rule' => ['required'], + 'change_rule2' => ['required', 'string'], + 'callback_rule' => ['required'], + ]); +}); + +test('support ruleMessages', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'default_hidden' => [ + ], + 'change_ruleMessages' => [ + 'ruleMessages' => ['required' => 'abc'], + ], + 'callback_ruleMessages' => [ + 'ruleMessages' => fn() => ['required' => 'abc'], + ], + ]); + expect($presetsHelper->pickRuleMessages())->toBe([ + 'change_ruleMessages' => ['required' => 'abc'], + 'callback_ruleMessages' => ['required' => 'abc'], + ]); +}); + +test('support ruleCustomAttribute', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'default_hidden' => [ + ], + 'change_ruleCustomAttribute' => [ + 'ruleCustomAttribute' => 'abc', + ], + 'callback_ruleCustomAttribute' => [ + 'ruleCustomAttribute' => fn() => 'abc', + ], + ]); + expect($presetsHelper->pickRuleCustomAttributes())->toBe([ + 'change_ruleCustomAttribute' => 'abc', + 'callback_ruleCustomAttribute' => 'abc', + ]); +}); + +test('support selectOptions', function () { + $presetsHelper = $this->presetsHelper->withPresets([ 'id' => [ - 'label' => 'ID', + 'selectOptions' => ['a' => 'A', 'b' => 'B'], ], - 'code' => [ - 'label' => '编码', - ] - ]))->withSceneKeys([ - 'abcScene' => ['code'] ]); - expect(array_keys($presetsHelper->pickLabel()))->toBe(['id', 'code']) - ->and(components_to_array($presetsHelper->pickForm('abcScene')))->toBe(components_to_array([ - FormField::make()->name('code'), + expect(components_to_array($presetsHelper->pickGrid())) + ->toBe(components_to_array([ + GridColumn::make()->name('id')->typeMapping(['map' => [ + ['label' => 'A', 'value' => 'a'], + ['label' => 'B', 'value' => 'b'], + ]])->searchable() + ])) + ->and(components_to_array($presetsHelper->pickForm())) + ->toBe(components_to_array([ + FormField::make()->name('id')->typeSelect(['options' => [ + ['value' => 'a', 'label' => 'A'], + ['value' => 'b', 'label' => 'B'], + ]]), + ])) + ->and(components_to_array($presetsHelper->pickDetail())) + ->toBe(components_to_array([ + DetailAttribute::make()->name('id')->typeMapping(['map' => [ + ['label' => 'A', 'value' => 'a'], + ['label' => 'B', 'value' => 'b'], + ]]), ])); }); test('support pick special keys', function () { - $presetsHelper = (new PresetsHelper([ - 'id' => [ - 'label' => 'ID', - ], - 'code' => [ - 'label' => '编码', - ] - ])); - expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => '编码']) + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => [ + 'label' => 'ID', + ], + 'code' => [ + 'label' => 'Code', + ] + ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID', 'code' => 'Code']) ->and($presetsHelper->pickLabel(['id']))->toBe(['id' => 'ID']); }); -test('pickForm support multi field', function () { - $presetsHelper = (new PresetsHelper([ +test('support pickForm multi field', function () { + $presetsHelper = $this->presetsHelper->withPresets([ 'id' => [ - 'form' => fn(string $column, string $scene) => FormField::make()->name($column) + 'form' => fn(string $column) => FormField::make()->name($column) ], 'code' => [ - 'form' => fn(string $column, string $scene) => [ + 'form' => fn(string $column) => [ FormField::make()->name($column . '1'), FormField::make()->name($column . '2'), ], ] - ])); - expect(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ + ]); + expect(components_to_array($presetsHelper->pickForm()))->toBe(components_to_array([ FormField::make()->name('id'), FormField::make()->name('code1'), FormField::make()->name('code2'), ])); }); -test('pickForm required auto support', function () { - $presetsHelper = (new PresetsHelper([ - 'id' => [ - 'form' => true, - 'rule' => 'required', - ], - 'code' => [ - 'form' => true, - ], - ])); - expect(components_to_array($presetsHelper->pickForm(AbsRepository::SCENE_CREATE)))->toBe(components_to_array([ - FormField::make()->name('id')->required(), - FormField::make()->name('code'), - ])); -}); - -test('pickGrid searchable auto support', function () { - $presetsHelper = (new PresetsHelper([ - 'id' => [ - 'grid' => true, - ], - 'code' => [ - 'grid' => true, +test('support extDynamic', function () { + $presetsHelper = $this->presetsHelper->withPresets([ + 'key_useDynamic' => [ + 'formExtDynamic' => fn(FormField $field, string $scene) => $field->required($scene === 'create'), + 'ruleExtDynamic' => fn(array $rule, string $scene) => array_values(array_filter([ + $scene === 'create' ? 'required' : null, + 'string', + ])), ], - ])); - expect(components_to_array($presetsHelper->pickGrid()))->toBe(components_to_array([ - GridColumn::make()->name('id')->searchable(), - GridColumn::make()->name('code')->searchable(), - ])); -}); + ]); -test('selectOptions support', function () { - $presetsHelper = (new PresetsHelper([ - 'id' => [ - 'label' => 'ID', - ], - 'code' => [ - 'label' => '编码', - 'filter' => null, - ], - ])); - expect(components_to_array($presetsHelper->pickGrid())) - ->toBe(components_to_array([ - GridColumn::make()->name('id')->searchable(), - GridColumn::make()->name('code'), - ])); + expect($presetsHelper->withScene()->pickForm()[0]->get('required'))->toBeFalse() + ->and($presetsHelper->withScene('create')->pickForm()[0]->get('required'))->toBeTrue() + ->and($presetsHelper->withScene()->pickRules()['key_useDynamic'])->toBe(['string']) + ->and($presetsHelper->withScene('create')->pickRules()['key_useDynamic'])->toBe(['required', 'string']); }); -test('gridExt formExt detailExt support', function () { - $presetsHelper = (new PresetsHelper([ - 'id' => [ - 'label' => 'ID', - 'gridExt' => fn(GridColumn $column) => $column->sortable(), - 'formExt' => fn(FormField $field) => $field->typeInputNumber(), - 'detailExt' => fn(DetailAttribute $attribute) => $attribute->typeImage(), +test('ext and extDynamic compare', function () { + $globalValue = new stdClass(); + $globalValue->value = '123'; + $presetsHelper = $this->presetsHelper->withPresets([ + 'key_noDynamic' => [ + 'gridExt' => fn(GridColumn $column) => $column->width($globalValue->value), + 'formExt' => fn(FormField $field) => $field->value($globalValue->value), + 'detailExt' => fn(DetailAttribute $attribute) => $attribute->value($globalValue->value), ], - 'code' => [ - 'label' => '编码', - 'gridExt' => fn(GridColumn $column) => $column->sortable(), - 'formExt' => fn(FormField $field) => $field->typeInputNumber(), - 'detailExt' => fn(DetailAttribute $attribute) => $attribute->typeImage(), + 'key_useDynamic' => [ + 'gridExtDynamic' => fn(GridColumn $column) => $column->width($globalValue->value), + 'formExtDynamic' => fn(FormField $field, string $scene) => $field->value($globalValue->value), + 'detailExtDynamic' => fn(DetailAttribute $attribute, string $scene) => $attribute->value($globalValue->value), ], - ])); + ]); + $grids = $presetsHelper->pickGrid(); + $forms = $presetsHelper->pickForm(); + $details = $presetsHelper->pickDetail(); + expect($grids[0]->get('width'))->toBe('123') + ->and($forms[0]->get('value'))->toBe('123') + ->and($details[0]->get('value'))->toBe('123') + ->and($grids[1]->get('width'))->toBe('123') + ->and($forms[1]->get('value'))->toBe('123') + ->and($details[1]->get('value'))->toBe('123'); - expect(components_to_json($presetsHelper->pickGrid())) - ->toBe(components_to_json([ - GridColumn::make()->name('id')->searchable()->sortable(), - GridColumn::make()->name('code')->searchable()->sortable(), - ])) - ->and(components_to_json($presetsHelper->pickForm(AbsRepository::SCENE_CREATE))) - ->toBe(components_to_json([ - FormField::make()->name('id')->typeInputNumber(), - FormField::make()->name('code')->typeInputNumber(), - ])) - ->and(components_to_json($presetsHelper->pickDetail())) - ->toBe(components_to_json([ - DetailAttribute::make()->name('id')->typeImage(), - DetailAttribute::make()->name('code')->typeImage(), - ])); + $globalValue->value = '456'; // 修改值,仅 ExtDynamic 的才会变 + $grids = $presetsHelper->pickGrid(); + $forms = $presetsHelper->pickForm(); + $details = $presetsHelper->pickDetail(); + expect($grids[0]->get('width'))->toBe('123') + ->and($forms[0]->get('value'))->toBe('123') + ->and($details[0]->get('value'))->toBe('123') + ->and($grids[1]->get('width'))->toBe('456') + ->and($forms[1]->get('value'))->toBe('456') + ->and($details[1]->get('value'))->toBe('456'); }); \ No newline at end of file From 1c899c6f5bd0907612e2a8443543bed2924438a1 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Thu, 6 Feb 2025 16:26:45 +0800 Subject: [PATCH 10/12] feature: add test action --- .github/workflows/tests.yml | 52 +++++++++++++++++++++++++++++++++++++ composer.json | 5 +++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6091663 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,52 @@ +name: phpunit + +on: + push: + branches: + - 'main' + pull_request: +# schedule: +# - cron: '0 0 * * *' + +jobs: + phpunit: + runs-on: ubuntu-latest + strategy: + matrix: + php: [7.4, 8.0, 8.1, 8.2, 8.3, 8.4] + stability: [ + #prefer-lowest, 问题较多 + prefer-stable + ] + + name: PHP ${{ matrix.php }} - ${{ matrix.stability }} + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + #extensions: 'redis' + #ini-values: error_reporting=E_ALL php8.4 下 nullable is deprecated 暂时未完全解决,因此暂时不启用 + tools: composer:v2 + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: php${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + restore-keys: php${{ matrix.php }}-${{ matrix.stability }}-composer- + + - name: Install dependencies + run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + + - name: Run test + run: composer test diff --git a/composer.json b/composer.json index ce26b68..3e62c92 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ } }, "require": { - "php": ">=7.4", + "php": "^7.4||^8.0", "ext-json": "*" }, "require-dev": { @@ -33,5 +33,8 @@ "allow-plugins": { "pestphp/pest-plugin": true } + }, + "scripts": { + "test": "pest" } } From c4740c4f1748ed20c0d27d77ac4b5d61e2622142 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Thu, 6 Feb 2025 16:41:16 +0800 Subject: [PATCH 11/12] docs: update --- docs/common_usage.md | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/common_usage.md b/docs/common_usage.md index 1a2f382..f9b752f 100644 --- a/docs/common_usage.md +++ b/docs/common_usage.md @@ -2,14 +2,19 @@
如何修改 crud 的详情打开的 dialog 的 size + 在对应的 controller 中添加以下配置
-/**
- * @inheritdoc
- */
+// 修改该参数,对于 新增、修改、明细 都使用 lg 的 dialog
+protected ?array $defaultDialogConfig = [
+    'size' => 'lg',
+];
+
+// 单独控制
 protected function gridActionsConfig(): array
 {
     return [
+        // 单独配置 detail
         'schema_detail' => [
             'dialog' => [
                 'size' => 'lg',
@@ -18,4 +23,33 @@ protected function gridActionsConfig(): array
     ];
 }
 
+
+ +
+如何全局配置一个 amis 的组件 + +在 config 的 amis 中 components 中添加以下配置 +
+return [
+    // ... 其他配置
+    /*
+     * 用于全局替换组件的默认参数
+     * @see Component::$config
+     */
+    'components' => [
+        // 例如: 将列表页的字段默认左显示
+        /*\WebmanTech\AmisAdmin\Amis\GridColumn::class => [
+            'schema' => [
+                'align' => 'left',
+            ],
+        ],*/
+        // typeXxx,xxx 未 amis 的组件 type,通过 schema 会全局注入到每个 type 组件
+        'typeImage' => [
+            'schema' => [
+                'enlargeAble' => true,
+            ],
+        ],
+    ],
+];
+
\ No newline at end of file From 439ca575b62912f8827d89900254ced603df4ba5 Mon Sep 17 00:00:00 2001 From: kriss <462679766@qq.com> Date: Thu, 6 Feb 2025 17:03:02 +0800 Subject: [PATCH 12/12] feature: update dep --- composer.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 3e62c92..763e376 100644 --- a/composer.json +++ b/composer.json @@ -21,13 +21,13 @@ "ext-json": "*" }, "require-dev": { - "workerman/webman-framework": "^1.5", - "illuminate/database": "^8.83", - "illuminate/pagination": "^8.83", - "illuminate/validation": "^8.83", - "webman-tech/polyfill": "^1.0", - "pestphp/pest": "^1.23", - "symfony/var-dumper": "^5.4" + "workerman/webman-framework": "^1.5|^2.0", + "illuminate/database": "^8.0|^9.0|^10.0|^11.0", + "illuminate/pagination": "^8.0|^9.0|^10.0|^11.0", + "illuminate/validation": "^8.0|^9.0|^10.0|^11.0", + "webman-tech/polyfill": "^1.0|^2.0", + "pestphp/pest": "^1.23|^2.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "config": { "allow-plugins": {