From fcdacf38c94cc3e4d8d3d64bdc50fb70fe5ec355 Mon Sep 17 00:00:00 2001 From: dante di domenico Date: Wed, 10 Sep 2025 15:03:59 +0200 Subject: [PATCH 1/4] feat: fast create form handle password and roles --- resources/js/app/app.js | 4 + .../app/components/fast-create/form-field.vue | 24 ++++- .../form/field-multiple-checkboxes.vue | 93 +++++++++++++++++++ .../js/app/components/form/field-password.vue | 91 ++++++++++++++++++ src/Controller/AppController.php | 27 +++++- src/Controller/Component/ModulesComponent.php | 2 +- src/Controller/Component/SchemaComponent.php | 8 +- src/Controller/ModulesController.php | 2 - 8 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 resources/js/app/components/form/field-multiple-checkboxes.vue create mode 100644 resources/js/app/components/form/field-password.vue diff --git a/resources/js/app/app.js b/resources/js/app/app.js index 24f1b3a85..e0a9de1fe 100644 --- a/resources/js/app/app.js +++ b/resources/js/app/app.js @@ -103,7 +103,9 @@ const _vueInstance = new Vue({ FieldGeoCoordinates: () => import(/* webpackChunkName: "field-geo-coordinates" */'app/components/form/field-geo-coordinates'), FieldInteger: () => import(/* webpackChunkName: "field-integer" */'app/components/form/field-integer'), FieldJson: () => import(/* webpackChunkName: "field-json" */'app/components/form/field-json'), + FieldMultipleCheckboxes: () => import(/* webpackChunkName: "field-multiple-checkboxes" */'app/components/form/field-multiple-checkboxes'), FieldNumber: () => import(/* webpackChunkName: "field-number" */'app/components/form/field-number'), + FieldPassword: () => import(/* webpackChunkName: "field-password" */'app/components/form/field-password'), FieldPlaintext: () => import(/* webpackChunkName: "field-plaintext" */'app/components/form/field-plaintext'), FieldRadio: () => import(/* webpackChunkName: "field-radio" */'app/components/form/field-radio'), FieldSelect: () => import(/* webpackChunkName: "field-select" */'app/components/form/field-select'), @@ -630,7 +632,9 @@ Vue.component('FieldGeoCoordinates', _vueInstance.$options.components.FieldGeoCo Vue.component('FieldDate', _vueInstance.$options.components.FieldDate); Vue.component('FieldInteger', _vueInstance.$options.components.FieldInteger); Vue.component('FieldJson', _vueInstance.$options.components.FieldJson); +Vue.component('FieldMultipleCheckboxes', _vueInstance.$options.components.FieldMultipleCheckboxes); Vue.component('FieldNumber', _vueInstance.$options.components.FieldNumber); +Vue.component('FieldPassword', _vueInstance.$options.components.FieldPassword); Vue.component('FieldPlaintext', _vueInstance.$options.components.FieldPlaintext); Vue.component('FieldRadio', _vueInstance.$options.components.FieldRadio); Vue.component('FieldSelect', _vueInstance.$options.components.FieldSelect); diff --git a/resources/js/app/components/fast-create/form-field.vue b/resources/js/app/components/fast-create/form-field.vue index f2e109370..20b16785b 100644 --- a/resources/js/app/components/fast-create/form-field.vue +++ b/resources/js/app/components/fast-create/form-field.vue @@ -114,6 +114,15 @@ @change="update" v-if="fieldType === 'geo-coordinates'" /> + + + + @@ -228,6 +247,9 @@ export default { if (this.field === 'date_ranges') { return 'calendar'; } + if (this.jsonSchema?.type === 'array' && this.jsonSchema?.items?.type === 'string' && this.jsonSchema?.items?.enum?.length > 0) { + return 'multiple-checkboxes'; + } if (this.field === 'extra' || ['array','object'].includes(this.jsonSchema?.type) || this.jsonSchema?.oneOf?.filter(one => ['array','object'].includes(one?.type))?.length > 0) { return 'json'; } diff --git a/resources/js/app/components/form/field-multiple-checkboxes.vue b/resources/js/app/components/form/field-multiple-checkboxes.vue new file mode 100644 index 000000000..625272692 --- /dev/null +++ b/resources/js/app/components/form/field-multiple-checkboxes.vue @@ -0,0 +1,93 @@ + + + diff --git a/resources/js/app/components/form/field-password.vue b/resources/js/app/components/form/field-password.vue new file mode 100644 index 000000000..7eb88baaf --- /dev/null +++ b/resources/js/app/components/form/field-password.vue @@ -0,0 +1,91 @@ + + + diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 8bc8f9fc9..959d82b66 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -14,6 +14,7 @@ use App\Form\Form; use App\Utility\DateRangesTools; +use App\Utility\PermissionsTrait; use Authentication\Identity; use BEdita\WebTools\ApiClientProvider; use Cake\Controller\Controller; @@ -36,6 +37,8 @@ */ class AppController extends Controller { + use PermissionsTrait; + /** * BEdita4 API client * @@ -234,7 +237,7 @@ protected function specialAttributes(array &$data): void } $this->decodeJsonAttributes($data); - + $this->prepareRoles($data); $this->prepareDateRanges($data); // prepare categories @@ -299,6 +302,25 @@ protected function prepareDateRanges(array &$data): void $data['date_ranges'] = DateRangesTools::prepare(Hash::get($data, 'date_ranges')); } + /** + * Transform roles data into relations format. + * + * @param array $data The data to prepare + * @return void + */ + protected function prepareRoles(array &$data): void + { + $roles = (string)Hash::get($data, 'roles'); + $roles = empty($roles) ? [] : (array)json_decode($roles, true); + $data = array_filter($data, fn($key) => $key !== 'roles', ARRAY_FILTER_USE_KEY); + if (!empty($roles)) { + $data['relations']['roles']['replaceRelated'] = array_map( + fn($id) => ['id' => $id, 'type' => 'roles'], + array_keys($this->rolesByNames($roles)) + ); + } + } + /** * Prepare request relation data. * @@ -311,7 +333,8 @@ protected function prepareRelations(array &$data): void if (!empty($data['relations'])) { $api = []; foreach ($data['relations'] as $relation => $relationData) { - $id = $data['id']; + $id = (string)Hash::get($data, 'id'); + $id = empty($id) ? null : $id; foreach ($relationData as $method => $ids) { $relatedIds = $this->relatedIds($ids); if ($method === 'replaceRelated' || !empty($relatedIds)) { diff --git a/src/Controller/Component/ModulesComponent.php b/src/Controller/Component/ModulesComponent.php index 22352b21f..14033a3e2 100644 --- a/src/Controller/Component/ModulesComponent.php +++ b/src/Controller/Component/ModulesComponent.php @@ -513,7 +513,7 @@ public function skipSaveRelated(string $id, array &$relatedData): bool $type = $this->getController()->getRequest()->getParam('object_type'); $rr = $relatedData; foreach ($rr as $method => $data) { - $actualRelated = (array)ApiClientProvider::getApiClient()->getRelated($id, $type, $data['relation']); + $actualRelated = empty($id) ? [] : (array)ApiClientProvider::getApiClient()->getRelated($id, $type, $data['relation']); $actualRelated = (array)Hash::get($actualRelated, 'data'); $actualRelated = RelationsTools::toString($actualRelated); $requestRelated = (array)Hash::get($data, 'relatedIds', []); diff --git a/src/Controller/Component/SchemaComponent.php b/src/Controller/Component/SchemaComponent.php index 8ac5bfe35..ec0c57a3d 100644 --- a/src/Controller/Component/SchemaComponent.php +++ b/src/Controller/Component/SchemaComponent.php @@ -145,8 +145,12 @@ protected function fetchSchema(string $type) // add special property `roles` to `users` if ($type === 'users') { $schema['properties']['roles'] = [ - 'type' => 'string', - 'enum' => $this->fetchRoles(), + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => $this->fetchRoles(), + ], + 'uniqueItems' => true, ]; } $categories = $this->fetchCategories($type); diff --git a/src/Controller/ModulesController.php b/src/Controller/ModulesController.php index bc7da310f..f3ea2c756 100644 --- a/src/Controller/ModulesController.php +++ b/src/Controller/ModulesController.php @@ -15,7 +15,6 @@ use App\Utility\ApiConfigTrait; use App\Utility\CacheTools; use App\Utility\Message; -use App\Utility\PermissionsTrait; use BEdita\SDK\BEditaClientException; use BEdita\WebTools\Utility\ApiTools; use Cake\Core\Configure; @@ -45,7 +44,6 @@ class ModulesController extends AppController { use ApiConfigTrait; - use PermissionsTrait; /** * Object type currently used From 570745f6d2aff26196d329b1c7d5f31b0c42889a Mon Sep 17 00:00:00 2001 From: dante di domenico Date: Wed, 10 Sep 2025 15:12:14 +0200 Subject: [PATCH 2/4] tests: fix testFetchRoles --- .../TestCase/Controller/Component/SchemaComponentTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/TestCase/Controller/Component/SchemaComponentTest.php b/tests/TestCase/Controller/Component/SchemaComponentTest.php index 8d53135cc..65eb4f7fb 100644 --- a/tests/TestCase/Controller/Component/SchemaComponentTest.php +++ b/tests/TestCase/Controller/Component/SchemaComponentTest.php @@ -481,8 +481,12 @@ public function testFetchRoles(): void static::assertNotEmpty($result['properties']['roles']); $expected = [ - 'type' => 'string', - 'enum' => ['admin', 'manager'], + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => ['admin', 'manager'], + ], + 'uniqueItems' => true, ]; static::assertEquals($expected, $result['properties']['roles']); } From 0dd30b71270d66e368f8becf955e2b53ee15ccd7 Mon Sep 17 00:00:00 2001 From: dante di domenico Date: Wed, 10 Sep 2025 15:16:44 +0200 Subject: [PATCH 3/4] tests: test prepareRoles --- .../TestCase/Controller/AppControllerTest.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/TestCase/Controller/AppControllerTest.php b/tests/TestCase/Controller/AppControllerTest.php index 3dc064a37..e1b9b76ac 100644 --- a/tests/TestCase/Controller/AppControllerTest.php +++ b/tests/TestCase/Controller/AppControllerTest.php @@ -495,6 +495,31 @@ public function prepareRequestProvider(): array ]), ], ], + 'roles' => [ + 'users', // object_type + [ // expected + 'id' => '5', + 'username' => 'mario', + '_api' => [ + [ + 'method' => 'replaceRelated', + 'id' => '5', + 'relation' => 'roles', + 'relatedIds' => [ + [ + 'id' => '1', + 'type' => 'roles', + ], + ], + ], + ], + ], + [ // data provided + 'id' => '5', + 'username' => 'mario', + 'roles' => json_encode(['admin']), + ], + ], 'relations' => [ 'documents', // object_type [ @@ -704,6 +729,7 @@ public function prepareRequestProvider(): array * @covers ::specialAttributes() * @covers ::decodeJsonAttributes() * @covers ::prepareDateRanges() + * @covers ::prepareRoles() * @covers ::prepareRelations() * @covers ::setupParentsRelation() * @covers ::changedAttributes() From c350efcd56062a17eb0c2fa196a594e785d21658 Mon Sep 17 00:00:00 2001 From: dante di domenico Date: Wed, 10 Sep 2025 15:21:53 +0200 Subject: [PATCH 4/4] chore refactor: less checks --- src/Controller/AppController.php | 5 ++--- src/Controller/Component/ModulesComponent.php | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 959d82b66..347252cd7 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -333,8 +333,7 @@ protected function prepareRelations(array &$data): void if (!empty($data['relations'])) { $api = []; foreach ($data['relations'] as $relation => $relationData) { - $id = (string)Hash::get($data, 'id'); - $id = empty($id) ? null : $id; + $id = Hash::get($data, 'id', null); foreach ($relationData as $method => $ids) { $relatedIds = $this->relatedIds($ids); if ($method === 'replaceRelated' || !empty($relatedIds)) { @@ -344,7 +343,7 @@ protected function prepareRelations(array &$data): void } $data['_api'] = $api; } - unset($data['relations']); + $data = array_filter($data, fn($key) => $key !== 'relations', ARRAY_FILTER_USE_KEY); } /** diff --git a/src/Controller/Component/ModulesComponent.php b/src/Controller/Component/ModulesComponent.php index 14033a3e2..6603c989c 100644 --- a/src/Controller/Component/ModulesComponent.php +++ b/src/Controller/Component/ModulesComponent.php @@ -506,14 +506,14 @@ public function skipSaveRelated(string $id, array &$relatedData): bool return true; } $methods = (array)Hash::extract($relatedData, '{n}.method'); - if (in_array('addRelated', $methods) || in_array('removeRelated', $methods)) { + if (in_array('addRelated', $methods) || in_array('removeRelated', $methods) || empty($id)) { return false; } // check replaceRelated $type = $this->getController()->getRequest()->getParam('object_type'); $rr = $relatedData; foreach ($rr as $method => $data) { - $actualRelated = empty($id) ? [] : (array)ApiClientProvider::getApiClient()->getRelated($id, $type, $data['relation']); + $actualRelated = (array)ApiClientProvider::getApiClient()->getRelated($id, $type, $data['relation']); $actualRelated = (array)Hash::get($actualRelated, 'data'); $actualRelated = RelationsTools::toString($actualRelated); $requestRelated = (array)Hash::get($data, 'relatedIds', []);