diff --git a/config/app_local.example.php b/config/app_local.example.php index a6ab5d46d..264320146 100644 --- a/config/app_local.example.php +++ b/config/app_local.example.php @@ -809,4 +809,26 @@ // 'default' => 'webvtt', // ], // ], + + /** + * Configuration for "TreePreview", to enable anchors on specific positions on the tree. + * - '123' is the root id + * - 'title' is the title for the preview anchor + * - 'url' is the href for the preview anchor + * - 'color' is the color of the icon for the preview anchor (default is 'white') + */ + // 'TreePreview' => [ + // '123' => [ + // [ + // 'title' => 'Staging url', + // 'url' => 'https://staging.example.com', + // 'color' => 'orange', + // ], + // [ + // 'title' => 'Production url', + // 'url' => 'https://example.com', + // 'color' => 'red', + // ], + // ], + // ], ]; diff --git a/config/routes.php b/config/routes.php index 749fdb6ce..cbe995235 100644 --- a/config/routes.php +++ b/config/routes.php @@ -116,6 +116,12 @@ ['controller' => 'Tree', 'action' => 'parents'], ['_name' => 'tree:parents', 'pass' => ['type', 'id']] ); + // Slug + $routes->connect( + '/tree/slug', + ['controller' => 'Tree', 'action' => 'slug'], + ['_name' => 'tree:slug'] + ); // Admin. $routes->prefix('admin', ['_namePrefix' => 'admin:'], function (RouteBuilder $routes) { diff --git a/resources/js/app/app.js b/resources/js/app/app.js index 913fcbade..dac0252c3 100644 --- a/resources/js/app/app.js +++ b/resources/js/app/app.js @@ -48,6 +48,7 @@ const _vueInstance = new Vue({ DateRangesView: () => import(/* webpackChunkName: "date-ranges-view" */'app/components/date-ranges-view/date-ranges-view'), DateRangesList: () => import(/* webpackChunkName: "date-ranges-list" */'app/components/date-ranges-list/date-ranges-list'), TreeView: () => import(/* webpackChunkName: "tree-view" */'app/components/tree-view/tree-view'), + TreeSlug: () => import(/* webpackChunkName: "tree-slug" */'app/components/tree-slug/tree-slug'), IndexCell: () => import(/* webpackChunkName: "index-cell" */'app/components/index-cell/index-cell'), ModulesIndex: () => import(/* webpackChunkName: "modules-index" */'app/pages/modules/index'), ModulesView: () => import(/* webpackChunkName: "modules-view" */'app/pages/modules/view'), diff --git a/resources/js/app/components/tree-slug/tree-slug.vue b/resources/js/app/components/tree-slug/tree-slug.vue new file mode 100644 index 000000000..5d41ea89c --- /dev/null +++ b/resources/js/app/components/tree-slug/tree-slug.vue @@ -0,0 +1,183 @@ + + + \ No newline at end of file diff --git a/resources/js/app/components/tree-view/tree-view.vue b/resources/js/app/components/tree-view/tree-view.vue index 344fc99ea..3e0c8b7d6 100644 --- a/resources/js/app/components/tree-view/tree-view.vue +++ b/resources/js/app/components/tree-view/tree-view.vue @@ -1,13 +1,37 @@ @@ -117,7 +212,9 @@ const API_OPTIONS = { export default { name: 'TreeView', - + components: { + TreeSlug: () => import(/* webpackChunkName: "tree-slug" */'app/components/tree-slug/tree-slug'), + }, props: { store: { type: Object, @@ -168,6 +265,10 @@ export default { type: Boolean, default: false, }, + previewConfig: { + type: Object, + default: () => ({}), + }, search: { type: String, default: '', @@ -324,6 +425,7 @@ export default { relation: { menu: this.menu, canonical: this.canonical, + slug_path: this.node?.meta?.relation?.slug_path, }, }, }); diff --git a/src/Controller/TreeController.php b/src/Controller/TreeController.php index 5dd48ca04..fbf6053ac 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -17,6 +17,7 @@ use BEdita\SDK\BEditaClientException; use BEdita\WebTools\ApiClientProvider; use Cake\Cache\Cache; +use Cake\Http\Response; use Cake\Utility\Hash; use Psr\Log\LogLevel; @@ -25,6 +26,16 @@ */ class TreeController extends AppController { + /** + * @inheritDoc + */ + public function initialize(): void + { + parent::initialize(); + + $this->Security->setConfig('unlockedActions', ['slug']); + } + /** * Get tree data. * Use this for /tree?filter[roots]&... and /tree?filter[parent]=x&... @@ -91,6 +102,47 @@ public function parents(string $type, string $id): void $this->setSerialize(['parents']); } + /** + * Saves the current slug + */ + public function slug(): ?Response + { + $this->getRequest()->allowMethod(['post']); + $this->viewBuilder()->setClassName('Json'); + $response = $error = null; + try { + $data = (array)$this->getRequest()->getData(); + $body = [ + 'data' => [ + [ + 'id' => (string)Hash::get($data, 'id'), + 'type' => (string)Hash::get($data, 'type'), + 'meta' => [ + 'relation' => [ + 'slug' => (string)Hash::get($data, 'slug'), + ], + ], + ], + ], + ]; + $response = $this->apiClient->post( + sprintf('/folders/%s/relationships/children', (string)Hash::get($data, 'parent')), + json_encode($body) + ); + // Clearing cache after successful save + Cache::clearGroup('tree', TreeCacheEventHandler::CACHE_CONFIG); + } catch (BEditaClientException $err) { + $error = $err->getMessage(); + $this->log($error, 'error'); + $this->set('error', $error); + } + $this->set('response', $response); + $this->set('error', $error); + $this->setSerialize(['response', 'error']); + + return null; + } + /** * Get tree data by query params. * Use cache to store data. @@ -239,7 +291,7 @@ function () use ($id, $type) { */ protected function fetchTreeData(array $query): array { - $fields = 'id,status,title'; + $fields = 'id,status,title,perms,relation,slug_path'; $response = ApiClientProvider::getApiClient()->get('/folders', compact('fields') + $query); $data = (array)Hash::get($response, 'data'); $meta = (array)Hash::get($response, 'meta'); @@ -261,6 +313,8 @@ protected function minimalData(array $fullData): array if (empty($fullData)) { return []; } + $meta = (array)Hash::get($fullData, 'meta'); + $meta['slug_path_compact'] = $this->slugPathCompact((array)Hash::get($meta, 'slug_path')); return [ 'id' => (string)Hash::get($fullData, 'id'), @@ -269,6 +323,7 @@ protected function minimalData(array $fullData): array 'title' => (string)Hash::get($fullData, 'attributes.title'), 'status' => (string)Hash::get($fullData, 'attributes.status'), ], + 'meta' => $meta, ]; } @@ -293,12 +348,31 @@ protected function minimalDataWithMeta(array $fullData): ?array ], 'meta' => [ 'path' => (string)Hash::get($fullData, 'meta.path'), + 'slug_path' => (array)Hash::get($fullData, 'meta.slug_path'), + 'slug_path_compact' => $this->slugPathCompact((array)Hash::get($fullData, 'meta.slug_path')), 'relation' => [ 'canonical' => (string)Hash::get($fullData, 'meta.relation.canonical'), 'depth_level' => (string)Hash::get($fullData, 'meta.relation.depth_level'), 'menu' => (string)Hash::get($fullData, 'meta.relation.menu'), + 'slug' => (string)Hash::get($fullData, 'meta.relation.slug'), ], ], ]; } + + /** + * Get compact slug path. + * + * @param array $slugPath Slug path. + * @return string + */ + protected function slugPathCompact(array $slugPath): string + { + $slugPathCompact = ''; + foreach ($slugPath as $item) { + $slugPathCompact = sprintf('%s/%s', $slugPathCompact, (string)Hash::get($item, 'slug')); + } + + return $slugPathCompact; + } } diff --git a/templates/Element/Form/trees.twig b/templates/Element/Form/trees.twig index a9307ed75..fac8e32ee 100644 --- a/templates/Element/Form/trees.twig +++ b/templates/Element/Form/trees.twig @@ -49,12 +49,13 @@ class="mt-1" relation-name={{ relationName }} relation-label="{{ Layout.tr(relationName) }}" - :object='{{ { id: object.id, type: object.type }|json_encode }}' + :object='{{ { id: object.id, type: object.type, uname: object.attributes.uname }|json_encode }}' :multiple-choice={{ options.multiple }} :user-roles="{{ user.roles|json_encode }}" :has-permissions="{{ hasPermissions|json_encode }}" :search="searchInPosition" :search-in-position-active="searchInPositionActive" + :preview-config="{{ config('TreePreview')|json_encode }}" @changed-parents="updatePositions"> diff --git a/tests/TestCase/Controller/TreeControllerTest.php b/tests/TestCase/Controller/TreeControllerTest.php index 8b03950f7..c74591e96 100644 --- a/tests/TestCase/Controller/TreeControllerTest.php +++ b/tests/TestCase/Controller/TreeControllerTest.php @@ -104,6 +104,7 @@ public function fetchTreeData(array $query): array * @covers ::node() * @covers ::fetchNodeData() * @covers ::minimalData() + * @covers ::slugPathCompact() */ public function testNode(): void { @@ -349,4 +350,90 @@ public function minData(array $data): array $actual = $tree->minData([]); static::assertEmpty($actual); } + + /** + * Test `slug` method + * + * @return void + * @covers ::initialize() + * @covers ::slug() + */ + public function testSlug(): void + { + $this->setupApi(); + $parent = $this->createTestFolder(); + $response = $this->client->save('folders', [ + 'title' => 'controller test folder child', + 'parent_id' => (int)Hash::get($parent, 'id'), + ]); + $child = $response['data']; + $parentId = (string)Hash::get($parent, 'id'); + $childId = (string)Hash::get($child, 'id'); + $type = (string)Hash::get($child, 'type'); + $slug = 'test_slug'; + $data = [ + 'parent' => $parentId, + 'slug' => $slug, + 'type' => $type, + 'id' => $childId, + ]; + $config = [ + 'environment' => [ + 'REQUEST_METHOD' => 'POST', + ], + 'post' => $data, + ]; + $request = new ServerRequest($config); + $tree = new TreeController($request); + $tree->slug(); + $actualResponse = $tree->viewBuilder()->getVar('response'); + static::assertNotEmpty($actualResponse); + $actualError = $tree->viewBuilder()->getVar('error'); + static::assertNull($actualError); + static::assertArrayHasKey('links', $actualResponse); + static::assertArrayHasKey('self', $actualResponse['links']); + static::assertArrayHasKey('home', $actualResponse['links']); + $this->assertNotEmpty($actualResponse['links']['self']); + $this->assertNotEmpty($actualResponse['links']['home']); + } + + /** + * Test `slug` method on exception + * + * @return void + * @covers ::initialize() + * @covers ::slug() + */ + public function testSlugException(): void + { + $this->setupApi(); + $parent = $this->createTestFolder(); + $response = $this->client->save('folders', [ + 'title' => 'controller test folder child', + 'parent_id' => (int)Hash::get($parent, 'id'), + ]); + $child = $response['data']; + $childId = (string)Hash::get($child, 'id'); + $type = (string)Hash::get($child, 'type'); + $slug = 'test_slug'; + $data = [ + 'parent' => null, + 'slug' => $slug, + 'type' => $type, + 'id' => $childId, + ]; + $config = [ + 'environment' => [ + 'REQUEST_METHOD' => 'POST', + ], + 'post' => $data, + ]; + $request = new ServerRequest($config); + $tree = new TreeController($request); + $tree->slug(); + $actualResponse = $tree->viewBuilder()->getVar('response'); + $actualError = $tree->viewBuilder()->getVar('error'); + static::assertEmpty($actualResponse); + static::assertNotEmpty($actualError); + } }