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/.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..763e376 100644 --- a/composer.json +++ b/composer.json @@ -11,15 +11,30 @@ "src/helper.php" ] }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, "require": { - "php": ">=7.4", + "php": "^7.4||^8.0", "ext-json": "*" }, "require-dev": { - "workerman/webman-framework": "^1.3", - "illuminate/database": "^8.83", - "illuminate/pagination": "^8.83", - "illuminate/validation": "^8.83", - "webman-tech/polyfill": "^1.0" + "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": { + "pestphp/pest-plugin": true + } + }, + "scripts": { + "test": "pest" } } 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 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/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/Amis/Component.php b/src/Amis/Component.php index bab5d9f..ac85ee8 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; @@ -36,7 +37,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); } @@ -88,6 +89,9 @@ public function toArray(): array protected function deepToArray(array $arr): array { $newArr = []; + if (isset($arr['type']) && $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/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/DetailAttribute.php b/src/Amis/DetailAttribute.php index 3793194..8857c0f 100644 --- a/src/Amis/DetailAttribute.php +++ b/src/Amis/DetailAttribute.php @@ -2,6 +2,8 @@ namespace WebmanTech\AmisAdmin\Amis; +use WebmanTech\AmisAdmin\Amis\Traits\ComponentCommonFn; + /** * 详情的一个字段 * @link https://aisuda.bce.baidu.com/amis/zh-CN/components/form/static @@ -36,6 +38,8 @@ */ class DetailAttribute extends Component { + use ComponentCommonFn; + protected array $schema = [ 'type' => 'static', 'name' => '', @@ -65,12 +69,23 @@ 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; } + + 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/FormField.php b/src/Amis/FormField.php index 23739f0..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; + /** * 表单的一个字段 * @@ -21,6 +23,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) @@ -85,6 +88,8 @@ */ class FormField extends Component { + use ComponentCommonFn; + protected array $schema = [ 'type' => 'input-text', 'name' => '', @@ -96,6 +101,7 @@ class FormField extends Component 'hidden' => true, 'visible' => true, 'required' => true, + 'static' => true, 'validateOnChange' => true, ]; @@ -105,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 1c3923a..4648a98 100644 --- a/src/Amis/GridColumn.php +++ b/src/Amis/GridColumn.php @@ -2,6 +2,8 @@ namespace WebmanTech\AmisAdmin\Amis; +use WebmanTech\AmisAdmin\Amis\Traits\ComponentCommonFn; + /** * table 的单个 column * @@ -36,6 +38,8 @@ */ class GridColumn extends Component { + use ComponentCommonFn; + protected array $schema = [ 'type' => 'text', 'name' => '', @@ -93,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; } @@ -106,9 +106,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 +133,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/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/Amis/Traits/ComponentCommonFn.php b/src/Amis/Traits/ComponentCommonFn.php new file mode 100644 index 0000000..c2e582a --- /dev/null +++ b/src/Amis/Traits/ComponentCommonFn.php @@ -0,0 +1,41 @@ +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)) { + // 将 [$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/Controller/AmisSourceController.php b/src/Controller/AmisSourceController.php index f084e7b..dac40fe 100644 --- a/src/Controller/AmisSourceController.php +++ b/src/Controller/AmisSourceController.php @@ -6,6 +6,8 @@ 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; abstract class AmisSourceController @@ -24,6 +26,11 @@ abstract class AmisSourceController * @var bool */ protected bool $onlyShow = false; + /** + * 默认的 dialog 框配置 + * @var array|null + */ + protected ?array $defaultDialogConfig = null; /** * @var RepositoryInterface|null @@ -123,6 +130,14 @@ protected function amisCrud(Request $request): Amis\Crud */ protected function crudConfig(): array { + if ($this->defaultDialogConfig) { + return [ + 'schema_create' => [ + 'dialog' => $this->defaultDialogConfig, + ], + ]; + } + return []; } @@ -132,6 +147,11 @@ protected function crudConfig(): array */ protected function grid(): array { + $repository = $this->repository(); + if ($repository instanceof HasPresetInterface) { + return $repository->getPresetsHelper()->withScene(AbsRepository::SCENE_LIST)->pickGrid(); + } + return [ Amis\GridColumn::make()->name($this->repository()->getPrimaryKey()), ]; @@ -188,6 +208,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 []; } 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/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php b/src/Controller/Traits/AmisSourceController/CreateUpdateFormTrait.php index 8aa0f33..028e791 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()->withScene($scene)->pickForm(); + } + return [ //Amis\FormField::make()->name('name'), ]; @@ -47,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 3d111d2..07d49b5 100644 --- a/src/Controller/Traits/AmisSourceController/DetailTrait.php +++ b/src/Controller/Traits/AmisSourceController/DetailTrait.php @@ -6,6 +6,8 @@ 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 { @@ -66,6 +68,11 @@ protected function addDetailAction(Amis\GridColumnActions $actions, string $rout */ protected function detail(): array { + $repository = $this->repository(); + if ($repository instanceof HasPresetInterface) { + return $repository->getPresetsHelper()->withScene(AbsRepository::SCENE_DETAIL)->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..46a93ac 100644 --- a/src/Helper/ConfigHelper.php +++ b/src/Helper/ConfigHelper.php @@ -2,19 +2,49 @@ namespace WebmanTech\AmisAdmin\Helper; -class ConfigHelper +/** + * @internal + */ +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/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 new file mode 100644 index 0000000..b58640c --- /dev/null +++ b/src/Helper/PresetsHelper.php @@ -0,0 +1,260 @@ + + */ + protected array $presets = []; + protected array $sceneKeys = []; + + /** + * 默认不启用编辑(只读) + * @return $this + */ + public function withDefaultNoEdit() + { + $this->defaultNoEdit = true; + return $this; + } + + /** + * 添加预设 + * @param array $presets + * @return $this + */ + public function withPresets(array $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 withSceneKeys(array $data) + { + 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 + * @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, + ]); + } + + protected string $scene = self::SCENE_DEFAULT; + + public function withScene(?string $scene = null) + { + $this->scene = $scene ?? self::SCENE_DEFAULT; + return $this; + } + + /** + * 获取字段对应的 label + * @param array|null $keys + * @return array + */ + public function pickLabel(?array $keys = null): array + { + return $this->pickColumn('label', $keys, true); + } + + /** + * 获取字段对应的 labelRemark + * @param array|null $keys + * @return array + */ + public function pickLabelRemark(?array $keys = null): array + { + return $this->pickColumn('labelRemark', $keys, true); + } + + /** + * 获取字段对应的 description + * @param array|null $keys + * @return array + */ + public function pickDescription(?array $keys = null): array + { + return $this->pickColumn('description', $keys, true); + } + + /** + * 获取字段对应的 filter + * @param array|null $keys + * @return array + */ + public function pickFilter(?array $keys = null): array + { + return $this->pickColumn('filter', $keys, true); + } + + /** + * 获取字段对应的 grid + * @param array|null $keys + * @return array + */ + public function pickGrid(?array $keys = null): array + { + return $this->pickColumn('grid', $keys); + } + + /** + * 获取字段对应的 form + * @param array|null $keys + * @return array + */ + public function pickForm(?array $keys = null): array + { + $items = $this->pickColumn('form', $keys); + // 允许同时展示多个 FormItem + $data = []; + foreach ($items as $item) { + if (is_array($item)) { + $data = array_merge($data, $item); + } else { + $data[] = $item; + } + } + return $data; + } + + /** + * 获取字段对应的 rules + * @param array|null $keys + * @return array + */ + public function pickRules(?array $keys = null): array + { + return $this->pickColumn('rule', $keys, true); + } + + /** + * 获取字段对应的 ruleMessages + * @param array|null $keys + * @return array + */ + public function pickRuleMessages(?array $keys = null): array + { + return $this->pickColumn('ruleMessages', $keys, true); + } + + /** + * 获取字段对应的 ruleCustomAttributes + * @param array|null $keys + * @return array + */ + public function pickRuleCustomAttributes(?array $keys = null): array + { + return $this->pickColumn('ruleCustomAttribute', $keys, true); + } + + /** + * 获取字段对应的 detail + * @param array|null $keys + * @return array + */ + public function pickDetail(?array $keys = null): array + { + return $this->pickColumn('detail', $keys); + } + + /** + * 获取 presetItems + * @param array|null $keys + * @return array + */ + protected function getPresetItems(?array $keys = null): array + { + $keys = $this->getKeys($keys); + + $data = []; + foreach ($keys as $key) { + if (!isset($this->presets[$key])) { + continue; + } + $data[$key] = $this->presets[$key]; + } + + return $data; + } + + /** + * 获取场景对应的 keys + * @param array|null $keys + * @return array + */ + private function getKeys(?array $keys = null): array + { + if ($keys === null) { + $keys = $this->sceneKeys[$this->scene] ?? array_keys($this->presets); + } + return $keys; + } + + /** + * 提取某个类型的数据 + * @param string $type + * @param array|null $keys + * @param bool $keepKey + * @return array + */ + protected function pickColumn(string $type, ?array $keys = null, bool $keepKey = false): array + { + $items = $this->getPresetItems($keys); + $data = []; + foreach ($items as $key => $item) { + $value = $item->withScene($this->scene)->{$type}; + if ($value !== null) { + $data[$key] = $value; + } + } + if (!$keepKey) { + $data = array_values($data); + } + return $data; + } +} 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/Repository/AbsRepository.php b/src/Repository/AbsRepository.php index 3fd3cc5..2f00fe2 100644 --- a/src/Repository/AbsRepository.php +++ b/src/Repository/AbsRepository.php @@ -39,6 +39,10 @@ public function getLabel(string $attribute): string */ protected function attributeLabels(): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->withScene()->pickLabel(); + } + return []; } @@ -56,6 +60,10 @@ public function getLabelRemark(string $attribute) */ protected function attributeLabelRemarks(): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->withScene()->pickLabelRemark(); + } + return []; } @@ -73,6 +81,10 @@ public function getDescription(string $attribute) */ protected function attributeDescriptions(): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->withScene()->pickDescription(); + } + return []; } @@ -130,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, @@ -173,6 +185,10 @@ protected function validator(): ValidatorInterface */ protected function rules(string $scene): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->withScene($scene)->pickRules(); + } + return []; } @@ -182,6 +198,10 @@ protected function rules(string $scene): array */ protected function ruleMessages(string $scene): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->withScene($scene)->pickRuleMessages(); + } + return []; } @@ -191,6 +211,10 @@ protected function ruleMessages(string $scene): array */ protected function ruleCustomAttributes(string $scene): array { + if ($this instanceof HasPresetInterface) { + return $this->getPresetsHelper()->withScene($scene)->pickRuleCustomAttributes(); + } + return $this->attributeLabels(); } } \ No newline at end of file diff --git a/src/Repository/EloquentRepository.php b/src/Repository/EloquentRepository.php index 0cdfcca..e036f66 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()->withScene(AbsRepository::SCENE_LIST)->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..c6c13b5 --- /dev/null +++ b/src/Repository/HasPresetInterface.php @@ -0,0 +1,14 @@ +presetsHelper === null) { + $this->presetsHelper = $this->createPresetsHelper(); + } + return $this->presetsHelper; + } + + /** + * 创建 PresetsHelper + * @return PresetsHelper + */ + abstract protected function createPresetsHelper(): PresetsHelper; +} 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 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 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/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/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/Helper/ArrayHelperTest.php b/tests/Unit/Helper/ArrayHelperTest.php new file mode 100644 index 0000000..8379ee9 --- /dev/null +++ b/tests/Unit/Helper/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 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/Helper/PresetsHelperTest.php b/tests/Unit/Helper/PresetsHelperTest.php new file mode 100644 index 0000000..03cb90b --- /dev/null +++ b/tests/Unit/Helper/PresetsHelperTest.php @@ -0,0 +1,418 @@ + $item->toArray(), $items); +} + +beforeEach(function () { + $this->presetsHelper = new PresetsHelper(); +}); + +test('support withPresets', function () { + $presetsHelper = $this->presetsHelper; + expect(array_keys($presetsHelper->pickLabel()))->toBe([]); + + $presetsHelper->withPresets([ + 'id' => [ + 'label' => 'ID', + ], + ]); + expect($presetsHelper->pickLabel())->toBe(['id' => 'ID']); +}); + +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('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']); +}); + +test('support withScene', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => ['label' => 'ID'], + 'code' => ['label' => 'Code'] + ]) + ->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('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'); +}); + +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(); +}); + +test('support description', function () { + $presetsHelper = $this->presetsHelper + ->withPresets([ + 'id' => [ + 'description' => '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('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('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('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' => [ + 'selectOptions' => ['a' => 'A', 'b' => 'B'], + ], + ]); + 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 = $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('support pickForm multi field', function () { + $presetsHelper = $this->presetsHelper->withPresets([ + 'id' => [ + 'form' => fn(string $column) => FormField::make()->name($column) + ], + 'code' => [ + 'form' => fn(string $column) => [ + FormField::make()->name($column . '1'), + FormField::make()->name($column . '2'), + ], + ] + ]); + expect(components_to_array($presetsHelper->pickForm()))->toBe(components_to_array([ + FormField::make()->name('id'), + FormField::make()->name('code1'), + FormField::make()->name('code2'), + ])); +}); + +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($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('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), + ], + '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'); + + $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 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 @@ +