diff --git a/.gitignore b/.gitignore
index 8facea8b..3010ebb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,7 @@ node_modules/
# files generated by Codeception
/tests/_output/
/tests/Support/_generated/
+
+# AI
+.claude
+.mcp.json
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..c9a2f3d1
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,153 @@
+# Admin Bundle — Claude Instructions
+
+## Project Overview
+
+OpenDXP Admin Bundle provides the backend UI for OpenDXP. It is built on [ExtJS](https://www.sencha.com/products/extjs/) and depends heavily on the **opendxp core** (`../opendxp`).
+
+**Dependency on opendxp core:** This bundle depends heavily on opendxp core (models, base controllers, test infrastructure). The core location depends on the developer's setup — when you need to read core source files, check these paths in order and use the first one that exists:
+
+1. `../opendxp/` — sibling directory (monorepo / symlinked workspace)
+2. `../../vendor/open-dxp/opendxp/` — project root one level up
+3. `../../../vendor/open-dxp/opendxp/` — project root two levels up
+
+When Claude is started from **opendxp core**, admin-specific topics (UI extension, admin events, perspectives, permissions) are documented in **this** bundle — see `docs/`.
+
+## Source Structure
+
+```
+src/
+├── Command/ # CLI commands
+├── Controller/Admin/ # Admin controllers (Document, Asset, DataObject, GDPR)
+│ └── Document/ # Incl. DocumentController (site panel, document tree)
+├── Controller/Traits/ # Shared controller traits
+├── DependencyInjection/ # Bundle configuration + compiler passes
+├── Enum/ # PHP enums (e.g. SiteCustomConfigNodeType)
+├── Event/ # Event classes + AdminEvents constants
+├── EventListener/ # Symfony event subscribers
+├── Helper/ # Service helpers (e.g. GridHelperService)
+├── Model/ # Admin-specific models (GridConfig, etc.)
+├── Perspective/ # Perspective service logic
+├── Security/ # Auth, authenticators, security tokens
+├── Service/ # Grid data, workflow, etc.
+├── System/ # System-level services
+├── Translation/ # Translation handling
+└── Twig/ # Twig extensions
+```
+
+**Key reference files:**
+- `src/Event/AdminEvents.php` — all event name constants exposed by this bundle
+- `src/Event/SiteCustomSettingsEvent.php` — example of an admin event class
+- `src/Enum/SiteCustomConfigNodeType.php` — example enum for config nodes
+- `src/Controller/Admin/Document/DocumentController.php` — main document/site controller
+
+## Documentation
+
+All documentation lives in `docs/`. See `docs/README.md` for the full index.
+
+### Structure
+
+```
+docs/
+├── README.md ← master index (update when adding sections)
+├── 00_Architecture/
+│ └── README.md ← bundle overview, core dependency explained
+├── 10_Extension_Points/
+│ ├── README.md ← what other bundles can extend
+│ ├── 01_Events.md ← AdminEvents reference + subscription examples
+│ ├── 02_Admin_UI_Assets.md ← loading JS/CSS into the admin UI
+│ ├── 03_Admin_UI_JavaScript.md ← ExtJS event system, menus, key bindings
+│ ├── 04_Perspectives.md ← backend UI perspectives configuration
+│ ├── 05_Permissions.md ← adding custom permissions
+│ ├── 06_Deeplinks.md ← deeplinks into admin interface
+│ └── 07_Custom_Admin_Login.md ← custom login entry point
+├── 20_Documents/
+│ ├── README.md
+│ └── 01_Site_Custom_Settings.md ← site-specific custom settings via events
+└── 90_Testing/
+ └── README.md ← test conventions and how to run tests
+```
+
+### Naming conventions
+- Folders: `NN_FunctionalArea/` — 10-step numbering leaves room for inserts
+- Files: `NN_TopicName.md` — numbered within folder (01, 02, …)
+- Each folder has a `README.md` as section index
+
+### When to document
+
+Document every **extension point** exposed to other bundles or applications:
+- New event constant in `AdminEvents` → update `docs/10_Extension_Points/01_Events.md`
+- New event class → add a doc in the relevant feature section with a full subscriber example
+- New enum for config nodes → document available values and their meaning
+- New controller endpoint used externally → document in the relevant feature section
+
+### opendxp/doc/ vs. admin-bundle/docs/
+
+Content belongs in **admin-bundle/docs/** when it is about:
+- Admin UI extension (JS events, menus, assets)
+- AdminEvents and event subscribers
+- Backend-only features (perspectives, custom login, deeplinks, permissions)
+- Site/document admin panel customization
+
+Content belongs in **opendxp/doc/** when it is about:
+- Core models and PHP API (Documents, Assets, DataObjects)
+- Core events (non-admin: DocumentEvents, AssetEvents, etc.)
+- Routing, MVC, deployment, infrastructure
+
+When opendxp/doc/ contained content that belongs here, the opendxp file becomes a redirect stub pointing to admin-bundle docs.
+
+### Document template
+
+# Feature Name
+
+One-line description: what it does and why it exists.
+
+## Overview
+
+Brief explanation of the feature, when to use it.
+
+## API Reference
+
+| Class / Constant | Description |
+|------------------------------|-----------------------|
+| `AdminEvents::CONSTANT_NAME` | When this event fires |
+| `SomeEvent::addNode()` | What the method does |
+
+## Example
+
+```php
+ 'onEvent',
+ ];
+ }
+
+ public function onEvent(SomeEvent $event): void
+ {
+ // ...
+ }
+}
+```
+
+## Stored Data / Result
+
+Where and how the data is persisted or used downstream.
+
+## See Also
+
+- [Related opendxp/doc page](https://github.com/open-dxp/opendxp/blob/1.x/doc/...)
+- [AdminEvents source](https://github.com/open-dxp/admin-bundle/blob/1.x/src/Event/AdminEvents.php)
+
+
+## Tests
+See `tests/CLAUDE.md` for full test conventions, base classes, and examples.
\ No newline at end of file
diff --git a/README.md b/README.md
index ee512683..68c833d3 100644
--- a/README.md
+++ b/README.md
@@ -31,11 +31,18 @@ And much more ...
Following topics are short-cuts into the documentation for admin interface:
+### Starting with OpenDXP Core
- [Getting Started](https://github.com/open-dxp/opendxp/blob/1.x/doc/01_Getting_Started/06_Create_a_First_Project.md)
- [User & Roles](https://github.com/open-dxp/opendxp/blob/1.x/doc/22_Administration_of_OpenDxp/07_Users_and_Roles.md)
-- [Deeplinks](https://github.com/open-dxp/opendxp/blob/1.x/doc/20_Extending_OpenDxp/23_Deeplinks_into_Admin_Interface.md)
- [Admin Translations](https://github.com/open-dxp/opendxp/blob/1.x/doc/06_Multi_Language_i18n/07_Admin_Translations.md)
-- [Extending Admin UI](https://github.com/open-dxp/opendxp/blob/1.x/doc/20_Extending_OpenDxp/13_Bundle_Developers_Guide/06_Event_Listener_UI.md)
+
+### Admin Documentation
+- [Architecture](docs/00_Architecture/README.md)
+- [Extension_Points](docs/10_Extension_Points)
+- [Deeplinks](docs/10_Extension_Points/06_Deeplinks.md)
+- 🤖 [Testing with AI (Claude)](https://github.com/open-dxp/opendxp/doc/19_Development_Tools_and_Details/50_Testing_with_AI.md) - Write, run and fix tests with Claude Code
+
+=> [Full Documentation](docs/README.md)
***
diff --git a/composer.json b/composer.json
index 8116aaed..391ea3a7 100644
--- a/composer.json
+++ b/composer.json
@@ -21,17 +21,17 @@
"cbschuld/browser.php": "^1.9.6",
"endroid/qr-code": "^4 || ^5.1",
"phpoffice/phpspreadsheet": "^2.2 || ^3.3",
- "open-dxp/opendxp": "^1.2.0",
+ "open-dxp/opendxp": "^1.3.0",
"symfony/webpack-encore-bundle": "^2.1.1"
},
"require-dev": {
- "codeception/codeception": "5.2.2",
- "codeception/module-symfony": "^3.1",
+ "codeception/codeception": "^5.3.5",
+ "codeception/stub": "^4.3",
"codeception/module-asserts": "^3.2",
- "codeception/phpunit-wrapper": "^9",
+ "codeception/module-symfony": "^3.8",
"phpstan/phpstan": "2.1.33",
"phpstan/phpstan-symfony": "^2.0.9",
- "phpunit/phpunit": "^9.3",
+ "phpunit/phpunit": "^10.5",
"symfony/dotenv": "^7.4",
"symfony/runtime": "^7.4"
},
diff --git a/docs/00_Architecture/README.md b/docs/00_Architecture/README.md
new file mode 100644
index 00000000..693cee7b
--- /dev/null
+++ b/docs/00_Architecture/README.md
@@ -0,0 +1,71 @@
+# Architecture
+
+## What Is This Bundle?
+
+The Admin Bundle provides the **backend UI** for OpenDXP. It renders the document tree,
+asset manager, object editor, user management, and all other admin panels using the
+[ExtJS](https://www.sencha.com/products/extjs/) framework on the frontend and Symfony
+controllers on the backend.
+
+## Relationship to opendxp Core
+
+This bundle depends heavily on the **opendxp core** (`open-dxp/opendxp`). The core provides:
+
+| From core | Used by admin-bundle |
+|---------------------------|-------------------------------------------------|
+| `OpenDxp\Model\*` | Document, Asset, DataObject, Site models |
+| `OpenDxp\Event\*` | Core events (DocumentEvents, AssetEvents, etc.) |
+| `OpenDxp\Controller\*` | Base controller classes |
+| `OpenDxp\Tests\Support\*` | Base test classes (ModelTestCase) |
+| `OpenDxp\Config` | Global configuration |
+
+The admin-bundle adds its **own** event layer on top (`src/Event/AdminEvents.php`) for
+events that are specific to the admin UI lifecycle.
+
+## Source Layout
+
+```
+src/
+├── Command/ # CLI commands (e.g. cache warm-up, admin user management)
+├── Controller/
+│ ├── Admin/ # Admin controllers, one subfolder per element type
+│ │ ├── Asset/
+│ │ ├── DataObject/
+│ │ ├── Document/ # DocumentController — tree, site panel, site custom settings
+│ │ └── ...
+│ └── Traits/ # Shared controller traits
+├── DependencyInjection/ # Bundle extension + compiler passes
+├── Enum/ # PHP enums for typed config values
+├── Event/ # Admin event classes + AdminEvents constants
+├── EventListener/ # Symfony event subscribers (internal)
+├── Helper/ # Stateless service helpers (e.g. GridHelperService)
+├── Model/ # Admin-only models (GridConfig, GridConfigShare, etc.)
+├── Perspective/ # Perspective resolution and serialization
+├── Security/ # Admin authentication, authenticators, security tokens
+├── Service/ # Application services (grid data, workflow)
+├── System/ # System-level services
+├── Translation/ # Admin translation handling
+└── Twig/ # Twig extensions for admin templates
+```
+
+## Frontend Assets
+
+JavaScript and CSS live in `public/js/` and `public/css/`. The bundle uses
+[Webpack Encore](https://symfony.com/doc/current/frontend/encore/simple-example.html)
+(`webpack.config.js`) for asset compilation.
+
+Key JS entry points:
+- `public/js/opendxp/events.js` — all frontend event constants
+- `public/js/opendxp/document/tree.js` — document tree rendering
+
+## Configuration Namespace
+
+The bundle registers its configuration under `opendxp_admin`:
+
+```yaml
+opendxp_admin:
+ custom_admin_path_identifier: ~
+ custom_admin_route_name: ~
+ user:
+ default_key_bindings: ~
+```
\ No newline at end of file
diff --git a/docs/10_Extension_Points/01_Events.md b/docs/10_Extension_Points/01_Events.md
new file mode 100644
index 00000000..78d2e718
--- /dev/null
+++ b/docs/10_Extension_Points/01_Events.md
@@ -0,0 +1,95 @@
+# Admin Events
+
+All event constants for the admin-bundle are defined in
+[`src/Event/AdminEvents.php`](https://github.com/open-dxp/admin-bundle/blob/1.x/src/Event/AdminEvents.php).
+
+## How to Subscribe
+
+Implement `EventSubscriberInterface` in your bundle and return the event constant(s) you want to handle:
+
+```php
+ 'onPreSendData',
+ ];
+ }
+
+ public function onPreSendData(ElementAdminStyleEvent $event): void
+ {
+ // ...
+ }
+}
+```
+
+Symfony autoconfiguration registers the subscriber automatically when `EventSubscriberInterface` is implemented.
+
+## Event Categories
+
+### Document Events
+
+| Constant | Event Class | Description |
+|----------------------------------|-------------|----------------------------------------------|
+| `DOCUMENT_GET_PRE_SEND_DATA` | — | Before document data is sent to the frontend |
+| `DOCUMENT_LIST_BEFORE_LIST_LOAD` | — | Before document listing is loaded |
+| `DOCUMENT_LIST_AFTER_LIST_LOAD` | — | After document listing is loaded |
+
+### Asset Events
+
+| Constant | Event Class | Description |
+|-------------------------------|-------------|-------------------------------------------|
+| `ASSET_GET_PRE_SEND_DATA` | — | Before asset data is sent to the frontend |
+| `ASSET_LIST_BEFORE_LIST_LOAD` | — | Before asset listing is loaded |
+| `ASSET_LIST_AFTER_LIST_LOAD` | — | After asset listing is loaded |
+
+### Object Events
+
+| Constant | Event Class | Description |
+|--------------------------------|-------------|-------------------------------------------------|
+| `OBJECT_GET_PRE_SEND_DATA` | — | Before data object data is sent to the frontend |
+| `OBJECT_LIST_BEFORE_LIST_LOAD` | — | Before object listing is loaded |
+| `OBJECT_LIST_AFTER_LIST_LOAD` | — | After object listing is loaded |
+
+### Element Style Events
+
+| Constant | Event Class | Description |
+|----------------------------------------|--------------------------|--------------------------------------------------------|
+| `ELEMENT_ADMIN_STYLE_GET_FOR_DOCUMENT` | `ElementAdminStyleEvent` | Customize admin style (icon, CSS class) for a document |
+| `ELEMENT_ADMIN_STYLE_GET_FOR_ASSET` | `ElementAdminStyleEvent` | Customize admin style for an asset |
+| `ELEMENT_ADMIN_STYLE_GET_FOR_OBJECT` | `ElementAdminStyleEvent` | Customize admin style for a data object |
+
+### Login Events
+
+| Constant | Event Class | Description |
+|---------------------|-------------------------------|---------------------------------------|
+| `LOGIN_CREDENTIALS` | `Login\LoginCredentialsEvent` | After login credentials are submitted |
+| `LOGIN_FAILED` | `Login\LoginFailedEvent` | After a failed login attempt |
+
+### Perspective Events
+
+| Constant | Event Class | Description |
+|--------------------------------|-------------|----------------------------------------------|
+| `PERSPECTIVE_PRE_GET_RUNTIME` | — | Before perspective runtime data is assembled |
+| `PERSPECTIVE_POST_GET_RUNTIME` | — | After perspective runtime data is assembled |
+
+### Site Events
+
+| Constant | Event Class | Description |
+|------------------------|---------------------------|-----------------------------------------------------------------------------------|
+| `SITE_CUSTOM_SETTINGS` | `SiteCustomSettingsEvent` | Fired when the site panel renders or saves, allows injecting custom config fields |
+
+See [Site Custom Settings](../20_Documents/01_Site_Custom_Settings.md) for a full example.
+
+## See Also
+
+- [AdminEvents source](https://github.com/open-dxp/admin-bundle/blob/1.x/src/Event/AdminEvents.php)
+- [Core events (non-admin)](https://github.com/open-dxp/opendxp/blob/1.x/doc/20_Extending_OpenDxp/11_Event_API_and_Event_Manager.md)
\ No newline at end of file
diff --git a/docs/10_Extension_Points/02_Admin_UI_Assets.md b/docs/10_Extension_Points/02_Admin_UI_Assets.md
new file mode 100644
index 00000000..605230de
--- /dev/null
+++ b/docs/10_Extension_Points/02_Admin_UI_Assets.md
@@ -0,0 +1,96 @@
+# Loading Assets in the Admin UI
+
+If you need to load custom JS or CSS into the admin or editmode UI, you have two options.
+
+## Option 1: Via OpenDXP Bundle Interface (recommended)
+
+Add [`OpenDxpBundleAdminClassicInterface`](https://github.com/open-dxp/opendxp/blob/1.x/lib/Extension/Bundle/OpenDxpBundleAdminClassicInterface.php)
+to your bundle class. Use [`BundleAdminClassicTrait`](https://github.com/open-dxp/opendxp/blob/1.x/lib/Extension/Bundle/Traits/BundleAdminClassicTrait.php)
+to implement the four required methods:
+
+```php
+use OpenDxp\Extension\Bundle\AbstractOpenDxpBundle;
+use OpenDxp\Extension\Bundle\OpenDxpBundleAdminClassicInterface;
+use OpenDxp\Extension\Bundle\Traits\BundleAdminClassicTrait;
+
+class MyBundle extends AbstractOpenDxpBundle implements OpenDxpBundleAdminClassicInterface
+{
+ use BundleAdminClassicTrait;
+
+ public function getJsPaths(): array
+ {
+ return ['/bundles/mybundle/js/admin.js'];
+ }
+
+ public function getCssPaths(): array
+ {
+ return ['/bundles/mybundle/css/admin.css'];
+ }
+}
+```
+
+### With Webpack Encore
+
+Use [`EncoreHelper`](https://github.com/open-dxp/opendxp/blob/1.x/lib/Helper/EncoreHelper.php)
+to resolve built file paths from `entrypoints.json`:
+
+```php
+use OpenDxp\Helper\EncoreHelper;
+
+public function getJsPaths(): array
+{
+ return EncoreHelper::getBuildPathsFromEntrypoints(
+ $this->getPath() . '/public/build/mybundle/entrypoints.json'
+ );
+}
+
+public function getCssPaths(): array
+{
+ return EncoreHelper::getBuildPathsFromEntrypoints(
+ $this->getPath() . '/public/build/mybundle/entrypoints.json',
+ 'css'
+ );
+}
+```
+
+## Option 2: Via Event Listener
+
+Subscribe to events from [`BundleManagerEvents`](https://github.com/open-dxp/opendxp/blob/1.x/lib/Event/BundleManagerEvents.php):
+
+```php
+ 'onJsPaths',
+ BundleManagerEvents::CSS_PATHS => 'onCssPaths',
+ ];
+ }
+
+ public function onJsPaths(PathsEvent $event): void
+ {
+ $event->addPaths(['/bundles/app/js/admin.js']);
+ }
+
+ public function onCssPaths(PathsEvent $event): void
+ {
+ $event->addPaths(['/bundles/app/css/admin.css']);
+ }
+}
+```
+
+Assets registered via either method are loaded last on OpenDXP startup, in registration order.
+
+## See Also
+
+- [Admin UI JavaScript](03_Admin_UI_JavaScript.md) — how to use the loaded JS to extend the UI
+- [BundleManagerEvents source](https://github.com/open-dxp/opendxp/blob/1.x/lib/Event/BundleManagerEvents.php)
\ No newline at end of file
diff --git a/docs/10_Extension_Points/03_Admin_UI_JavaScript.md b/docs/10_Extension_Points/03_Admin_UI_JavaScript.md
new file mode 100644
index 00000000..29252547
--- /dev/null
+++ b/docs/10_Extension_Points/03_Admin_UI_JavaScript.md
@@ -0,0 +1,158 @@
+# Admin UI JavaScript
+
+The OpenDXP backend UI is built with [ExtJS](https://www.sencha.com/products/extjs/).
+Custom JS loaded via [Admin UI Assets](02_Admin_UI_Assets.md) runs in the same context
+and can hook into the admin lifecycle using the event system defined in
+[`public/js/opendxp/events.js`](https://github.com/open-dxp/admin-bundle/blob/1.x/public/js/opendxp/events.js).
+
+## Listening to the Ready Event
+
+The entry point for any UI extension is `opendxp.events.opendxpReady`:
+
+```javascript
+document.addEventListener(opendxp.events.opendxpReady, (e) => {
+ console.log('OpenDXP is ready', e.detail);
+});
+```
+
+## Validating Object Data Before Save
+
+Use `preventDefault()` and `stopPropagation()` to cancel a save:
+
+```javascript
+document.addEventListener(opendxp.events.preSaveObject, (e) => {
+ const confirmed = confirm(`Save ${e.detail.object.data.general.className}?`);
+ if (!confirmed) {
+ e.preventDefault();
+ e.stopPropagation();
+ opendxp.helpers.showNotification(t('Info'), t('saving_failed'), 'info');
+ }
+});
+```
+
+## Adding a Main Navigation Item
+
+Hook into `opendxp.events.preMenuBuild` to add top-level navigation:
+
+```javascript
+opendxp.plugin.mybundle = Class.create({
+ initialize: function () {
+ document.addEventListener(opendxp.events.preMenuBuild, this.preMenuBuild.bind(this));
+ },
+
+ preMenuBuild: function (e) {
+ let menu = e.detail.menu;
+
+ menu.mybundle = {
+ label: t('myBundleLabel'),
+ iconCls: 'opendxp_main_nav_icon_myIcon',
+ priority: 42,
+ items: [],
+ shadow: false,
+ handler: this.openMyBundle,
+ noSubmenus: true,
+ cls: 'opendxp_navigation_flyout',
+ };
+ },
+
+ openMyBundle: function (e) {
+ try {
+ opendxp.globalmanager.get('plugin_opendxp_mybundle').activate();
+ } catch (e) {
+ opendxp.globalmanager.add('plugin_opendxp_mybundle', new opendxp.plugin.mybundle());
+ }
+ }
+});
+
+var myBundle = new opendxp.plugin.mybundle();
+```
+
+## Adding a Submenu to an Existing Menu
+
+Push into an existing menu's `items` array:
+
+```javascript
+opendxp.registerNS('opendxp.bundle.glossary.startup');
+
+opendxp.bundle.glossary.startup = Class.create({
+ initialize: function () {
+ document.addEventListener(opendxp.events.preMenuBuild, this.preMenuBuild.bind(this));
+ },
+
+ preMenuBuild: function (e) {
+ let menu = e.detail.menu;
+ const user = opendxp.globalmanager.get('user');
+ const perspectiveCfg = opendxp.globalmanager.get('perspective');
+
+ if (menu.extras && user.isAllowed('glossary') && perspectiveCfg.inToolbar('extras.glossary')) {
+ menu.extras.items.push({
+ text: t('glossary'),
+ iconCls: 'opendxp_nav_icon_glossary',
+ priority: 5,
+ itemId: 'opendxp_menu_extras_glossary',
+ handler: this.editGlossary,
+ });
+ }
+ },
+
+ editGlossary: function () {
+ try {
+ opendxp.globalmanager.get('bundle_glossary').activate();
+ } catch (e) {
+ opendxp.globalmanager.add('bundle_glossary', new opendxp.bundle.glossary.settings());
+ }
+ }
+});
+
+const opendxpBundleGlossary = new opendxp.bundle.glossary.startup();
+```
+
+## Adding Custom Key Bindings
+
+Define the key binding in `config.yaml`:
+
+```yaml
+opendxp_admin:
+ user:
+ default_key_bindings:
+ glossary:
+ key: 'G'
+ action: glossary # must match the function name added to keyBindingMapping
+ alt: true
+ shift: true
+```
+
+Then register the binding in JS using `opendxp.events.preRegisterKeyBindings`:
+
+```javascript
+opendxp.bundle.glossary.startup = Class.create({
+ initialize: function () {
+ document.addEventListener(opendxp.events.preRegisterKeyBindings, this.registerKeyBinding.bind(this));
+ },
+
+ registerKeyBinding: function (e) {
+ const user = opendxp.globalmanager.get('user');
+ if (user.isAllowed('glossary')) {
+ opendxp.helpers.keyBindingMapping.glossary = function () {
+ opendxpBundleGlossary.editGlossary();
+ };
+ }
+ }
+});
+```
+
+## I18n in JavaScript
+
+Translations registered server-side are available via `t()`:
+
+```javascript
+t('my_translation_key')
+```
+
+See [opendxp/doc — i18n for bundles](https://github.com/open-dxp/opendxp/blob/1.x/doc/06_Multi_Language_i18n/07_Admin_Translations.md)
+for how to register translation keys server-side.
+
+## See Also
+
+- [Admin UI Assets](02_Admin_UI_Assets.md) — how to load your JS files
+- [events.js source](https://github.com/open-dxp/admin-bundle/blob/1.x/public/js/opendxp/events.js)
\ No newline at end of file
diff --git a/docs/10_Extension_Points/04_Perspectives.md b/docs/10_Extension_Points/04_Perspectives.md
new file mode 100644
index 00000000..30ad8cc1
--- /dev/null
+++ b/docs/10_Extension_Points/04_Perspectives.md
@@ -0,0 +1,100 @@
+# Perspectives
+
+Perspectives allow creating different views in the backend UI and customizing the standard layout.
+They can be combined with [Custom Views](https://github.com/open-dxp/opendxp/blob/1.x/doc/05_Objects/01_Object_Classes/05_Class_Settings/20_Custom_Views.md).
+
+> **Security Note**
+> Perspectives and Custom Views are not intended to restrict access to data — use permissions for that.
+
+You can define per-perspective:
+- Which trees are visible and where (left/right)
+- Which toolbar menus and items are shown
+- Which portlets are available on the dashboard
+- Navigation and welcome screen elements
+
+Access to individual perspectives can be restricted via user/role settings.
+
+## Configuration File
+
+The configuration lives in `var/config/perspectives/` (YAML format).
+Format follows the environment configuration — see
+[Configuration Environments](https://github.com/open-dxp/opendxp/blob/1.x/doc/21_Deployment/03_Configuration_Environments.md).
+
+## Configuration Reference
+
+| Key | Type | Description |
+|--------------------------------------------------|---------|---------------------------------------------------|
+| `[name]["icon"]` | string | Path to perspective icon |
+| `[name]["iconCls"]` | string | CSS class for the icon |
+| `[name]["elementTree"]` | array | Tree definitions (type, position, expanded, etc.) |
+| `[name]["elementTree"][i]["type"]` | string | `documents`, `objects`, `assets`, `customview` |
+| `[name]["elementTree"][i]["position"]` | string | `left` or `right` |
+| `[name]["elementTree"][i]["id"]` | integer | Custom view ID (only for type `customview`) |
+| `[name]["toolbar"]` | array | Per-menu visibility and item configuration |
+| `[name]["toolbar"][menuName]["hidden"]` | boolean | Hide the entire menu |
+| `[name]["toolbar"][menuName]["items"][itemName]` | boolean | Show/hide individual items |
+
+## Example
+
+A catalog-admin perspective that shows only a product custom view and assets:
+
+**Custom view** (`var/config/perspectives/perspective.yaml`):
+```yaml
+4e9f892c-7734-f5fa-d6f0-31e7f9787ffc:
+ name: Cars
+ treetype: object
+ position: left
+ rootfolder: '/Product Data/Cars'
+ showroot: false
+ sort: 3
+ icon: /bundles/opendxpadmin/img/flat-white-icons/automotive.svg
+ classes: CAR
+```
+
+**Perspective** (`var/config/perspectives/example.yaml`):
+```yaml
+demo:
+ elementTree:
+ -
+ type: customview
+ position: left
+ sort: 0
+ expanded: false
+ hidden: false
+ id: 4e9f892c-7734-f5fa-d6f0-31e7f9787ffc
+ -
+ type: assets
+ position: right
+ sort: 0
+ expanded: false
+ hidden: false
+ iconCls: opendxp_nav_icon_perspective
+ toolbar:
+ file:
+ hidden: true
+ marketing:
+ hidden: true
+ extras:
+ hidden: true
+ settings:
+ hidden: true
+ search:
+ hidden: false
+ items:
+ quickSearch: false
+ documents: true
+ assets: false
+ objects: false
+```
+
+## Perspective Events
+
+Subscribe to `AdminEvents::PERSPECTIVE_PRE_GET_RUNTIME` or `AdminEvents::PERSPECTIVE_POST_GET_RUNTIME`
+to modify perspective data programmatically.
+
+See [01_Events.md](01_Events.md) for subscription examples.
+
+## See Also
+
+- [AdminEvents](01_Events.md)
+- [Custom Views](https://github.com/open-dxp/opendxp/blob/1.x/doc/05_Objects/01_Object_Classes/05_Class_Settings/20_Custom_Views.md)
\ No newline at end of file
diff --git a/docs/10_Extension_Points/05_Permissions.md b/docs/10_Extension_Points/05_Permissions.md
new file mode 100644
index 00000000..0b699b67
--- /dev/null
+++ b/docs/10_Extension_Points/05_Permissions.md
@@ -0,0 +1,64 @@
+# Custom Permissions
+
+You can add custom permission keys to OpenDXP that appear in the user/role settings panel
+and can be checked both server-side and client-side.
+
+## Step 1: Register the Permission
+
+Add your permission key to the `users_permission_definitions` table:
+
+```sql
+INSERT INTO users_permission_definitions (key) VALUES ('my_custom_permission');
+```
+
+After this, the permission appears in the Users/Roles admin panel and can be assigned.
+
+## Step 2: Check the Permission Server-Side
+
+Inside an admin controller that extends `UserAwareController`:
+
+```php
+getOpenDxpUser();
+
+ if ($openDxpUser?->isAllowed('my_custom_permission')) {
+ // authorized
+ }
+
+ return $this->jsonResponse(['success' => true]);
+ }
+}
+```
+
+## Step 3: Check the Permission Client-Side
+
+In your bundle's JavaScript (loaded via [Admin UI Assets](02_Admin_UI_Assets.md)):
+
+```javascript
+document.addEventListener(opendxp.events.opendxpReady, (e) => {
+ if (opendxp.currentuser.permissions.indexOf('my_custom_permission') >= 0) {
+ // user has the permission — show/enable UI element
+ }
+});
+```
+
+## See Also
+
+- [Admin UI JavaScript](03_Admin_UI_JavaScript.md)
+- [Users & Roles](https://github.com/open-dxp/opendxp/blob/1.x/doc/22_Administration_of_OpenDxp/07_Users_and_Roles.md)
\ No newline at end of file
diff --git a/docs/10_Extension_Points/06_Deeplinks.md b/docs/10_Extension_Points/06_Deeplinks.md
new file mode 100644
index 00000000..84fde565
--- /dev/null
+++ b/docs/10_Extension_Points/06_Deeplinks.md
@@ -0,0 +1,44 @@
+# Deeplinks Into the Admin Interface
+
+OpenDXP supports deeplinks that open a specific element directly inside the admin UI
+from an external application.
+
+## URL Schema
+
+```
+https://YOUR-HOST/admin/login/deeplink?TYPE_ID_SUBTYPE
+```
+
+## Examples
+
+### Documents
+
+```text
+https://acme.com/admin/login/deeplink?document_123_page
+https://acme.com/admin/login/deeplink?document_45_snippet
+https://acme.com/admin/login/deeplink?document_67_link
+https://acme.com/admin/login/deeplink?document_8_hardlink
+https://acme.com/admin/login/deeplink?document_9_email
+```
+
+### Assets
+
+```text
+https://acme.com/admin/login/deeplink?asset_23_image
+https://acme.com/admin/login/deeplink?asset_34_document
+https://acme.com/admin/login/deeplink?asset_56_folder
+https://acme.com/admin/login/deeplink?asset_78_video
+```
+
+### Data Objects
+
+```text
+https://acme.com/admin/login/deeplink?object_24_object
+https://acme.com/admin/login/deeplink?object_98_variant
+https://acme.com/admin/login/deeplink?object_66_folder
+```
+
+## Behaviour
+
+If the user is not yet logged in, the deeplink URL redirects to the login page first and
+then opens the target element after successful authentication.
\ No newline at end of file
diff --git a/docs/10_Extension_Points/07_Custom_Admin_Login.md b/docs/10_Extension_Points/07_Custom_Admin_Login.md
new file mode 100644
index 00000000..b4973798
--- /dev/null
+++ b/docs/10_Extension_Points/07_Custom_Admin_Login.md
@@ -0,0 +1,37 @@
+# Custom Admin Login Entry Point
+
+The default admin login is served at `/admin`. You can change this to a custom path
+to reduce exposure of the admin panel.
+
+## Configuration
+
+Add the custom identifier to `config/config.yaml`:
+
+```yaml
+opendxp_admin:
+ custom_admin_path_identifier: min20CharCustomToken
+```
+
+> `custom_admin_path_identifier` must be at least 20 characters long.
+
+## Custom Route
+
+Add a custom route entry in `config/routes.yaml`:
+
+```yaml
+my_custom_admin_entry_point:
+ path: /my-custom-login-page
+ controller: OpenDxp\Bundle\CoreBundle\Controller\PublicServicesController::customAdminEntryPointAction
+```
+
+When this route is called, an admin cookie is set in the browser which is then validated
+for all subsequent `/admin` calls.
+
+## Custom Route Name
+
+If you want to use a different route name (e.g. for the "login as user" link in user administration):
+
+```yaml
+opendxp_admin:
+ custom_admin_route_name: myCustomAdminRoute
+```
\ No newline at end of file
diff --git a/docs/10_Extension_Points/README.md b/docs/10_Extension_Points/README.md
new file mode 100644
index 00000000..7e2db189
--- /dev/null
+++ b/docs/10_Extension_Points/README.md
@@ -0,0 +1,59 @@
+# Extension Points
+
+This section documents how other bundles and applications can extend or customize
+the admin UI provided by this bundle.
+
+## Overview
+
+The admin-bundle exposes extension points at two levels:
+
+**PHP (server-side)**
+- Event system via `AdminEvents` constants and typed event classes
+- Custom permissions registered in the database
+
+**JavaScript (client-side)**
+- JS/CSS injection into the admin UI via bundle interface or event listeners
+- ExtJS UI events for adding menus, panels, key bindings
+
+## Topics
+
+| # | Topic | Summary |
+|---------------------------------|---------------------|------------------------------------------------------------------|
+| [01](01_Events.md) | Events | All `AdminEvents` constants, when they fire, subscriber examples |
+| [02](02_Admin_UI_Assets.md) | Admin UI Assets | How to load custom JS and CSS into the admin backend |
+| [03](03_Admin_UI_JavaScript.md) | Admin UI JavaScript | ExtJS events, adding navigation items, key bindings |
+| [04](04_Perspectives.md) | Perspectives | Configuring different backend UI layouts |
+| [05](05_Permissions.md) | Permissions | Adding custom permission keys |
+| [06](06_Deeplinks.md) | Deeplinks | Linking directly into admin from external apps |
+| [07](07_Custom_Admin_Login.md) | Custom Admin Login | Changing the `/admin` entry point |
+
+## How Events Work
+
+The admin-bundle follows the standard Symfony event dispatcher pattern.
+Event constants are defined as `public const string` on `AdminEvents`.
+Event classes live in `src/Event/` and extend `Symfony\Contracts\EventDispatcher\Event`.
+
+Other bundles subscribe to admin events by implementing `EventSubscriberInterface`:
+
+```php
+use OpenDxp\Bundle\AdminBundle\Event\AdminEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class MyListener implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ AdminEvents::SOME_EVENT => 'onSomeEvent',
+ ];
+ }
+
+ public function onSomeEvent(SomeAdminEvent $event): void
+ {
+ // ...
+ }
+}
+```
+
+Register the listener in your bundle's `services.yaml` — Symfony autoconfiguration
+picks up `EventSubscriberInterface` automatically.
\ No newline at end of file
diff --git a/docs/20_Documents/01_Site_Custom_Settings.md b/docs/20_Documents/01_Site_Custom_Settings.md
new file mode 100644
index 00000000..a5f029d2
--- /dev/null
+++ b/docs/20_Documents/01_Site_Custom_Settings.md
@@ -0,0 +1,137 @@
+# Site Custom Settings
+
+The site configuration panel in the admin UI can be extended with custom fields
+by subscribing to the `AdminEvents::SITE_CUSTOM_SETTINGS` event.
+
+This allows other bundles to add their own configuration fields (inputs, dropdowns, checkboxes)
+that are stored per-site in `Site::getCustomSettings()`.
+
+## When the Event Fires
+
+The event fires in two situations:
+1. **GET** — when the site panel renders (`getSiteCustomSettingsAction`), to know which fields to display
+2. **PUT** — when the site is saved (`updateSiteAction`), to know which request values to persist
+
+## API Reference
+
+### `AdminEvents::SITE_CUSTOM_SETTINGS`
+
+```php
+use OpenDxp\Bundle\AdminBundle\Event\AdminEvents;
+
+AdminEvents::SITE_CUSTOM_SETTINGS // 'opendxp.admin.site.customSettings'
+```
+
+### `SiteCustomSettingsEvent`
+
+| Method | Description |
+|---------------------------------------------------|-----------------------------------------|
+| `getSite(): Site` | The site being configured |
+| `addConfigNode(config, scope, name, label): void` | Register a custom field |
+| `getConfigNodes(): array` | All registered fields, grouped by scope |
+
+### `addConfigNode()` Parameters
+
+| Parameter | Type | Description |
+|-----------|-----------------------|------------------------------------------------------------------------------------------|
+| `$config` | `NodeConfigInterface` | Typed DTO — determines field type and available options |
+| `$scope` | `string` | Groups fields in the panel (e.g. `'app'`, `'seo'`); used as key when reading back values |
+| `$name` | `string` | Field identifier within the scope |
+| `$label` | `string` | Display label in the UI |
+
+### Config DTOs (`src/Dto/SiteCustomSettings/`)
+
+Each DTO corresponds to one ExtJS field type and exposes only the options that are valid for it:
+
+| DTO | ExtJS type | Options |
+|----------------------|-------------|---------------------------------------------------|
+| `InputNodeConfig` | `textfield` | `required` |
+| `TextNodeConfig` | `textarea` | `required` |
+| `CheckboxNodeConfig` | `checkbox` | `checkedValue`, `uncheckedValue` |
+| `DropdownNodeConfig` | `combobox` | `store`, `required`, `displayField`, `valueField` |
+
+## Stored Data
+
+Values are persisted via `Site::setCustomSettings()` after save, grouped by scope.
+
+### Reading Values
+
+`getCustomSettings()` accepts an optional scope argument:
+
+```php
+$site = Site::getById($id);
+
+// read a single scope — returns [] if the scope doesn't exist
+$appSettings = $site->getCustomSettings('app');
+$zone = $appSettings['zone'] ?? null;
+
+// read all scopes at once
+$all = $site->getCustomSettings();
+// $all === ['app' => ['zone' => 'store'], 'seo' => ['tracking_id' => 'UA-123']]
+```
+
+## Example: Adding a Zone Dropdown
+
+```php
+ 'addZoneConfig',
+ ];
+ }
+
+ public function addZoneConfig(SiteCustomSettingsEvent $event): void
+ {
+ $event->addConfigNode(
+ config: new DropdownNodeConfig(
+ store: [
+ ['label' => 'Zone 1', 'value' => 'zone_1'],
+ ['label' => 'Zone 2', 'value' => 'zone_2'],
+ ],
+ required: true,
+ ),
+ scope: 'app',
+ name: 'zone',
+ label: 'Zone',
+ );
+ }
+}
+```
+
+## Multiple Scopes
+
+Multiple bundles can each add their own scoped fields independently:
+
+```php
+// Bundle A
+$event->addConfigNode(new InputNodeConfig(), 'seo', 'tracking_id', 'GA Tracking ID');
+
+// Bundle B
+$event->addConfigNode(new CheckboxNodeConfig(), 'myapp', 'feature_enabled', 'Enable Feature');
+```
+
+Results in:
+```php
+$site->getCustomSettings() === [
+ 'seo' => ['tracking_id' => 'UA-123456'],
+ 'myapp' => ['feature_enabled' => '1'],
+]
+```
+
+## See Also
+
+- [`AdminEvents::SITE_CUSTOM_SETTINGS` source](https://github.com/open-dxp/admin-bundle/blob/1.x/src/Event/AdminEvents.php)
+- [`SiteCustomSettingsEvent` source](https://github.com/open-dxp/admin-bundle/blob/1.x/src/Event/SiteCustomSettingsEvent.php)
+- [Config DTOs source](https://github.com/open-dxp/admin-bundle/blob/1.x/src/Dto/SiteCustomSettings/)
+- [Admin Events overview](../10_Extension_Points/01_Events.md)
\ No newline at end of file
diff --git a/docs/20_Documents/README.md b/docs/20_Documents/README.md
new file mode 100644
index 00000000..57e0f0a1
--- /dev/null
+++ b/docs/20_Documents/README.md
@@ -0,0 +1,7 @@
+# Documents
+
+Admin UI features specific to document and site management.
+
+| Topic | Description |
+|----------------------------------------------------|-------------------------------------------------------------------|
+| [Site Custom Settings](01_Site_Custom_Settings.md) | Inject custom fields into the site configuration panel via events |
\ No newline at end of file
diff --git a/docs/90_Testing/README.md b/docs/90_Testing/README.md
new file mode 100644
index 00000000..16db6eed
--- /dev/null
+++ b/docs/90_Testing/README.md
@@ -0,0 +1,102 @@
+# Testing
+
+This bundle uses **Codeception** for integration tests and plain **PHPUnit** for unit tests.
+
+## Test Structure
+
+```
+tests/
+├── Model/ # Integration tests — require a running OpenDXP environment
+│ ├── GridHelper/ # Grid/listing logic tests
+│ └── Permissions/ # Permission model tests
+└── Support/
+ ├── Helper/ # Codeception module helpers
+ └── UnitTester.php # Tester actor (generated by Codeception)
+```
+
+Configuration: `codeception.dist.yml` in the bundle root.
+
+## Test Base Classes
+
+| Base Class | From | Use When |
+|--------------------------------------------|--------------------|-------------------------------------------------------------------------|
+| `OpenDxp\Tests\Support\Test\ModelTestCase` | `open-dxp/opendxp` | Test needs DB, models, or a running OpenDXP (documents, objects, sites) |
+| `PHPUnit\Framework\TestCase` | `PHPUnit` | Pure unit test — event classes, enums, services without I/O |
+
+## When to Write Which Type
+
+**Unit tests** (no base class, no DB):
+- New event classes (`SiteCustomSettingsEvent`, etc.)
+- New enum cases (`SiteCustomConfigNodeType`)
+- Service methods that only process data structures
+
+**Integration tests** (`ModelTestCase`):
+- Permission checks against real user/role objects
+- Grid helpers that run queries
+- Controller-level behaviour that reads/writes models
+
+## Unit Test Example
+
+Testing `SiteCustomSettingsEvent` in isolation:
+
+```php
+createMock(Site::class);
+ $event = new SiteCustomSettingsEvent($site);
+
+ $event->addConfigNode(SiteCustomConfigNodeType::INPUT, 'seo', 'title', 'SEO Title', []);
+ $event->addConfigNode(SiteCustomConfigNodeType::CHECKBOX, 'seo', 'noindex', 'No Index', []);
+ $event->addConfigNode(SiteCustomConfigNodeType::DROPDOWN, 'i18n', 'zone', 'Zone', ['store' => []]);
+
+ $nodes = $event->getConfigNodes();
+
+ self::assertCount(2, $nodes['seo']);
+ self::assertCount(1, $nodes['i18n']);
+ self::assertSame('input', $nodes['seo'][0]['type']);
+ self::assertSame('zone', $nodes['i18n'][0]['name']);
+ }
+}
+```
+
+Place unit tests in `tests/Unit/` (create the folder if it does not exist yet).
+
+## Integration Test Example
+
+Existing tests in `tests/Model/Permissions/` extend `ModelTestCase`:
+
+```php
+class ModelDocumentPermissionsTest extends AbstractPermissionTest
+{
+ // uses OpenDxp\Tests\Support\Test\ModelTestCase via AbstractPermissionTest
+}
+```
+
+## Running Tests
+
+```bash
+# Run all tests (from bundle root)
+vendor/bin/codecept run
+
+# Run a specific suite
+vendor/bin/codecept run Unit
+
+# Run a specific test file
+vendor/bin/codecept run Unit tests/Unit/Event/SiteCustomSettingsEventTest.php
+```
+
+> Integration tests require a configured OpenDXP environment with a running database.
+> See opendxp core docs: `doc/19_Development_Tools_and_Details/29_Testing/`.
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..5d0881c6
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,43 @@
+# Admin Bundle Documentation
+
+This is the documentation root for the **OpenDXP Admin Bundle**.
+
+The Admin Bundle provides the backend UI for OpenDXP (based on ExtJS) and defines the
+extension points that other bundles use to customize the admin interface.
+
+---
+
+## Sections
+
+### [00 Architecture](00_Architecture/README.md)
+Bundle overview, relationship to opendxp core, and source structure.
+
+### [10 Extension Points](10_Extension_Points/README.md)
+How other bundles and applications can extend the admin UI.
+
+| Topic | Description |
+|----------------------------------------------------------------------|-------------------------------------------------------------------|
+| [Events](10_Extension_Points/01_Events.md) | All `AdminEvents` constants — when they fire and how to subscribe |
+| [Admin UI Assets](10_Extension_Points/02_Admin_UI_Assets.md) | Loading custom JS and CSS into the admin UI |
+| [Admin UI JavaScript](10_Extension_Points/03_Admin_UI_JavaScript.md) | ExtJS event system, adding menus, key bindings |
+| [Perspectives](10_Extension_Points/04_Perspectives.md) | Configuring backend UI perspectives |
+| [Permissions](10_Extension_Points/05_Permissions.md) | Adding custom permissions |
+| [Deeplinks](10_Extension_Points/06_Deeplinks.md) | Deeplinks into the admin interface |
+| [Custom Admin Login](10_Extension_Points/07_Custom_Admin_Login.md) | Custom admin login entry point |
+
+### [20 Documents](20_Documents/README.md)
+Admin UI features specific to document and site management.
+
+| Topic | Description |
+|-----------------------------------------------------------------|------------------------------------------------------|
+| [Site Custom Settings](20_Documents/01_Site_Custom_Settings.md) | Adding custom fields to the site configuration panel |
+
+### [90 Testing](90_Testing/README.md)
+How to write and run tests for this bundle.
+
+---
+
+## Quick Reference
+
+- **Event constants:** `src/Event/AdminEvents.php`
+- **opendxp core docs:** MVC, models, routing, deployment → see opendxp `doc/` (location depends on setup, see `CLAUDE.md`)
\ No newline at end of file
diff --git a/public/js/opendxp/document/tree.js b/public/js/opendxp/document/tree.js
index 61cda219..96497f0a 100644
--- a/public/js/opendxp/document/tree.js
+++ b/public/js/opendxp/document/tree.js
@@ -11,19 +11,19 @@
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3)
*/
- Ext.define('documentreemodel', {
+Ext.define('documentreemodel', {
extend: 'Ext.data.TreeModel',
idProperty: 'id',
fields: [{
- name: "id",
+ name: 'id',
convert: undefined
}, {
- name: "name",
+ name: 'name',
convert: undefined
}]
});
-opendxp.registerNS("opendxp.document.tree");
+opendxp.registerNS('opendxp.document.tree');
/**
* @private
*/
@@ -31,30 +31,29 @@ opendxp.document.tree = Class.create({
treeDataUrl: null,
- initialize: function(config, perspectiveCfg) {
+ initialize: function (config, perspectiveCfg) {
this.treeDataUrl = Routing.generate('opendxp_admin_document_document_treegetchildrenbyid');
this.perspectiveCfg = perspectiveCfg;
if (!perspectiveCfg) {
this.perspectiveCfg = {
- position: "left"
+ position: 'left'
};
}
this.perspectiveCfg = new opendxp.perspective(this.perspectiveCfg);
- this.position = this.perspectiveCfg.position ? this.perspectiveCfg.position : "left";
+ this.position = this.perspectiveCfg.position ? this.perspectiveCfg.position : 'left';
if (!config) {
this.config = {
rootId: 1,
rootVisible: true,
loaderBaseParams: {},
- treeId: "opendxp_panel_tree_documents",
- treeIconCls: "opendxp_icon_main_tree_document opendxp_icon_material",
+ treeId: 'opendxp_panel_tree_documents',
+ treeIconCls: 'opendxp_icon_main_tree_document opendxp_icon_material',
treeTitle: t('documents'),
- parentPanel: Ext.getCmp("opendxp_panel_tree_" + this.position)
+ parentPanel: Ext.getCmp('opendxp_panel_tree_' + this.position)
};
- }
- else {
+ } else {
this.config = config;
}
@@ -66,12 +65,12 @@ opendxp.document.tree = Class.create({
params: {
id: this.config.rootId,
view: this.config.customViewId,
- elementType: "document"
+ elementType: 'document'
},
success: function (response) {
var res = Ext.decode(response.responseText);
var callback = function () {};
- if(res["id"]) {
+ if (res['id']) {
callback = this.init.bind(this, res);
}
opendxp.layout.treepanelmanager.initPanel(this.config.treeId, callback);
@@ -80,14 +79,14 @@ opendxp.document.tree = Class.create({
},
- init: function(rootNodeConfig) {
+ init: function (rootNodeConfig) {
var itemsPerPage = opendxp.settings['document_tree_paging_limit'];
let rootNodeConfigText = t('home');
- let rootNodeConfigIconCls = "opendxp_icon_home";
- if(this.config.customViewId !== undefined && rootNodeConfig.id !== 1) {
+ let rootNodeConfigIconCls = 'opendxp_icon_home';
+ if (this.config.customViewId !== undefined && rootNodeConfig.id !== 1) {
rootNodeConfigText = rootNodeConfig.key;
rootNodeConfigIconCls = rootNodeConfig.iconCls;
}
@@ -95,10 +94,9 @@ opendxp.document.tree = Class.create({
rootNodeConfig.text = rootNodeConfigText;
rootNodeConfig.allowDrag = true;
rootNodeConfig.iconCls = rootNodeConfigIconCls;
- rootNodeConfig.cls = "opendxp_tree_node_root";
+ rootNodeConfig.cls = 'opendxp_tree_node_root';
rootNodeConfig.expanded = true;
-
var store = Ext.create('opendxp.data.PagingTreeStore', {
autoLoad: false,
autoSync: false,
@@ -107,7 +105,7 @@ opendxp.document.tree = Class.create({
url: this.treeDataUrl,
reader: {
type: 'json',
- totalProperty : 'total',
+ totalProperty: 'total',
rootProperty: 'nodes'
},
@@ -123,15 +121,15 @@ opendxp.document.tree = Class.create({
// documents
this.tree = Ext.create('opendxp.tree.Panel', {
- selModel : {
- mode : 'MULTI'
+ selModel: {
+ mode: 'MULTI'
},
- region: "center",
+ region: 'center',
id: this.config.treeId,
title: this.config.treeTitle,
iconCls: this.config.treeIconCls,
cls: this.config['rootVisible'] ? '' : 'opendxp_tree_no_root_node',
- autoScroll:true,
+ autoScroll: true,
autoLoad: false,
animate: false,
containerScroll: true,
@@ -142,7 +140,7 @@ opendxp.document.tree = Class.create({
plugins: {
ptype: 'treeviewdragdrop',
appendOnly: false,
- ddGroup: "element"
+ ddGroup: 'element'
},
listeners: {
nodedragover: this.onTreeNodeOver.bind(this)
@@ -150,13 +148,13 @@ opendxp.document.tree = Class.create({
xtype: 'opendxptreeview'
},
tools: [{
- type: "right",
+ type: 'right',
handler: opendxp.layout.treepanelmanager.toRight.bind(this),
- hidden: this.position == "right"
- },{
- type: "left",
+ hidden: this.position === 'right'
+ }, {
+ type: 'left',
handler: opendxp.layout.treepanelmanager.toLeft.bind(this),
- hidden: this.position == "left"
+ hidden: this.position === 'left'
}],
// root: rootNodeConfig,
store: store,
@@ -165,21 +163,20 @@ opendxp.document.tree = Class.create({
this.tree.loadMask = new Ext.LoadMask({
target: this.tree,
- msg: t("please_wait")
+ msg: t('please_wait')
});
- this.tree.on("itemmouseenter", opendxp.helpers.treeNodeThumbnailPreview.bind(this));
- this.tree.on("itemmouseleave", opendxp.helpers.treeNodeThumbnailPreviewHide.bind(this));
+ this.tree.on('itemmouseenter', opendxp.helpers.treeNodeThumbnailPreview.bind(this));
+ this.tree.on('itemmouseleave', opendxp.helpers.treeNodeThumbnailPreviewHide.bind(this));
- store.on("nodebeforeexpand", function (node) {
- opendxp.helpers.addTreeNodeLoadingIndicator("document", node.data.id, false);
+ store.on('nodebeforeexpand', function (node) {
+ opendxp.helpers.addTreeNodeLoadingIndicator('document', node.data.id, false);
});
- store.on("nodeexpand", function (node, index, item, eOpts) {
- opendxp.helpers.removeTreeNodeLoadingIndicator("document", node.data.id);
+ store.on('nodeexpand', function (node, index, item, eOpts) {
+ opendxp.helpers.removeTreeNodeLoadingIndicator('document', node.data.id);
});
-
this.config.parentPanel.insert(this.config.index, this.tree);
this.config.parentPanel.updateLayout();
@@ -187,28 +184,24 @@ opendxp.document.tree = Class.create({
this.config.parentPanel.alreadyExpanded = true;
this.tree.expand();
}
-
-
},
getTreeNodeListeners: function () {
- var treeNodeListeners = {
- 'itemclick': this.onTreeNodeClick,
- "itemcontextmenu": this.onTreeNodeContextmenu.bind(this),
- "itemmove": this.onTreeNodeMove.bind(this),
- "beforeitemmove": this.onTreeNodeBeforeMove.bind(this),
- "itemmouseenter": function (el, record, item, index, e, eOpts) {
+ return {
+ itemclick: this.onTreeNodeClick,
+ itemcontextmenu: this.onTreeNodeContextmenu.bind(this),
+ itemmove: this.onTreeNodeMove.bind(this),
+ beforeitemmove: this.onTreeNodeBeforeMove.bind(this),
+ itemmouseenter: function (el, record, item, index, e, eOpts) {
opendxp.helpers.treeToolTipShow(el, record, item);
},
- "itemmouseleave": function () {
+ itemmouseleave: function () {
opendxp.helpers.treeToolTipHide();
}
};
-
- return treeNodeListeners;
},
- onTreeNodeClick: function (tree, record, item, index, event, eOpts ) {
+ onTreeNodeClick: function (tree, record, item, index, event, eOpts) {
if (event.ctrlKey === false && event.shiftKey === false && event.altKey === false) {
if (record.data.permissions && record.data.permissions.view) {
opendxp.helpers.treeNodeThumbnailPreviewHide();
@@ -217,15 +210,14 @@ opendxp.document.tree = Class.create({
}
},
- onTreeNodeOver: function (targetNode, position, dragData, e, eOpts ) {
+ onTreeNodeOver: function (targetNode, position, dragData, e, eOpts) {
var node = dragData.records[0];
// check for permission
try {
if (node.data.permissions.settings) {
return true;
}
- }
- catch (e) {
+ } catch (e) {
console.log(e);
}
@@ -233,7 +225,7 @@ opendxp.document.tree = Class.create({
},
- onTreeNodeMove: function (node, oldParent, newParent, index, eOpts ) {
+ onTreeNodeMove: function (node, oldParent, newParent, index, eOpts) {
var tree = node.getOwnerTree();
if (newParent.pagingData) {
@@ -241,35 +233,34 @@ opendxp.document.tree = Class.create({
}
var moveCallback = function (newParent, oldParent, tree, response) {
- try{
+ try {
var rdata = Ext.decode(response.responseText);
if (rdata && rdata.success) {
// set new paths
var newBasePath = newParent.data.path;
- if (newBasePath == "/") {
- newBasePath = "";
+ if (newBasePath === '/') {
+ newBasePath = '';
}
node.data.basePath = newBasePath;
- node.data.path = node.data.basePath + "/" + node.data.text;
+ node.data.path = node.data.basePath + '/' + node.data.text;
if (!node.data.published) {
- node.data.cls = "opendxp_unpublished";
+ node.data.cls = 'opendxp_unpublished';
var view = tree.getView();
var nodeEl = Ext.fly(view.getNodeByRecord(node));
- var nodeElInner = nodeEl.down(".x-grid-td");
+ var nodeElInner = nodeEl.down('.x-grid-td');
if (nodeElInner) {
- nodeElInner.addCls("opendxp_unpublished");
+ nodeElInner.addCls('opendxp_unpublished');
}
} else {
delete node.data.cls;
}
this.updateOpenDocumentPaths(node);
- }
- else {
+ } else {
tree.loadMask.hide();
- opendxp.helpers.showNotification(t("error"), t("cant_move_node_to_target"),
- "error",t(rdata.message));
+ opendxp.helpers.showNotification(t('error'), t('cant_move_node_to_target'),
+ 'error', t(rdata.message));
// we have to delay refresh between two nodes,
// as there could be parent child relationship leading to race condition
window.setTimeout(function () {
@@ -277,9 +268,9 @@ opendxp.document.tree = Class.create({
}, 500);
opendxp.elementservice.refreshNode(newParent);
}
- } catch(e){
+ } catch (e) {
tree.loadMask.hide();
- opendxp.helpers.showNotification(t("error"), t("cant_move_node_to_target"), "error");
+ opendxp.helpers.showNotification(t('error'), t('cant_move_node_to_target'), 'error');
// we have to delay refresh between two nodes,
// as there could be parent child relationship leading to race condition
window.setTimeout(function () {
@@ -299,8 +290,7 @@ opendxp.document.tree = Class.create({
opendxp.elementservice.updateDocument(node.data.id, params, moveCallback);
},
-
- onTreeNodeBeforeMove: function (node, oldParent, newParent, index, eOpts ) {
+ onTreeNodeBeforeMove: function (node, oldParent, newParent, index, eOpts) {
var tree = node.getOwnerTree();
if (oldParent.getOwnerTree().getId() != newParent.getOwnerTree().getId()) {
@@ -308,7 +298,6 @@ opendxp.document.tree = Class.create({
return false;
}
-
// check for locks
if (node.data.locked && oldParent.data.id != newParent.data.id) {
Ext.MessageBox.alert(t('locked'), t('element_cannot_be_move_because_it_is_locked'));
@@ -316,12 +305,12 @@ opendxp.document.tree = Class.create({
}
// check new parent's permission
- if(!newParent.data.permissions.create){
+ if (!newParent.data.permissions.create) {
Ext.MessageBox.alert(' ', t('element_cannot_be_moved'));
return false;
}
- if(opendxp.elementservice.isDisallowedDocumentKey(newParent.id, node.data.text)) {
+ if (opendxp.elementservice.isDisallowedDocumentKey(newParent.id, node.data.text)) {
return false;
}
@@ -330,47 +319,48 @@ opendxp.document.tree = Class.create({
tree.loadMask.show();
return true;
}
+
return false;
},
- onTreeNodeContextmenu: function (tree, record, item, index, e, eOpts ) {
+ onTreeNodeContextmenu: function (tree, record, item, index, e, eOpts) {
e.stopEvent();
- if(opendxp.helpers.hasTreeNodeLoadingIndicator("document", record.data.id)) {
+ if (opendxp.helpers.hasTreeNodeLoadingIndicator('document', record.data.id)) {
return;
}
var menu = new Ext.menu.Menu();
var perspectiveCfg = this.perspectiveCfg;
- if(tree.getSelectionModel().getSelected().length > 1) {
+ if (tree.getSelectionModel().getSelected().length > 1) {
var selectedIds = [];
tree.getSelectionModel().getSelected().each(function (item) {
selectedIds.push(item.id);
});
- if (record.data.permissions && record.data.permissions.remove && record.data.id != 1 && !record.data.locked && perspectiveCfg.inTreeContextMenu("document.delete")) {
+ if (record.data.permissions && record.data.permissions.remove && record.data.id != 1 && !record.data.locked && perspectiveCfg.inTreeContextMenu('document.delete')) {
menu.add(new Ext.menu.Item({
text: t('delete'),
- iconCls: "opendxp_icon_delete",
+ iconCls: 'opendxp_icon_delete',
handler: this.deleteDocument.bind(this, selectedIds.join(','))
}));
}
} else {
var pasteMenu = [];
var pasteInheritanceMenu = [];
- var childSupportedDocument = (record.data.type)?opendxp.helpers.documentTypeHasSpecificRole(record.data.type, "children_supported"):false;
+ var childSupportedDocument = (record.data.type) ? opendxp.helpers.documentTypeHasSpecificRole(record.data.type, 'children_supported') : false;
if (childSupportedDocument && record.data.permissions && record.data.permissions.create) {
- var addDocuments = perspectiveCfg.inTreeContextMenu("document.add");
- var addEmail = perspectiveCfg.inTreeContextMenu("document.addEmail");
- var addSnippet = perspectiveCfg.inTreeContextMenu("document.addSnippet");
- var addLink = perspectiveCfg.inTreeContextMenu("document.addLink");
- var addHardlink = perspectiveCfg.inTreeContextMenu("document.addHardlink");
- var addBlankDocument = perspectiveCfg.inTreeContextMenu("document.addBlankDocument");
- var addBlankEmail = perspectiveCfg.inTreeContextMenu("document.addBlankEmail");
- var addBlankSnippet = perspectiveCfg.inTreeContextMenu("document.addBlankSnippet");
+ var addDocuments = perspectiveCfg.inTreeContextMenu('document.add');
+ var addEmail = perspectiveCfg.inTreeContextMenu('document.addEmail');
+ var addSnippet = perspectiveCfg.inTreeContextMenu('document.addSnippet');
+ var addLink = perspectiveCfg.inTreeContextMenu('document.addLink');
+ var addHardlink = perspectiveCfg.inTreeContextMenu('document.addHardlink');
+ var addBlankDocument = perspectiveCfg.inTreeContextMenu('document.addBlankDocument');
+ var addBlankEmail = perspectiveCfg.inTreeContextMenu('document.addBlankEmail');
+ var addBlankSnippet = perspectiveCfg.inTreeContextMenu('document.addBlankSnippet');
if (addDocuments) {
var documentMenu = {
@@ -385,35 +375,35 @@ opendxp.document.tree = Class.create({
if (addBlankDocument) {
// empty page
documentMenu.page.push({
- text: "> " + t("blank"),
- iconCls: "opendxp_icon_page opendxp_icon_overlay_add",
- handler: this.addDocument.bind(this, tree, record, "page")
+ text: '> ' + t('blank'),
+ iconCls: 'opendxp_icon_page opendxp_icon_overlay_add',
+ handler: this.addDocument.bind(this, tree, record, 'page')
});
}
if (addBlankSnippet) {
// empty snippet
documentMenu.snippet.push({
- text: "> " + t("blank"),
- iconCls: "opendxp_icon_snippet opendxp_icon_overlay_add",
- handler: this.addDocument.bind(this, tree, record, "snippet")
+ text: '> ' + t('blank'),
+ iconCls: 'opendxp_icon_snippet opendxp_icon_overlay_add',
+ handler: this.addDocument.bind(this, tree, record, 'snippet')
});
}
if (addBlankEmail) {
// empty email
documentMenu.email.push({
- text: "> " + t("blank"),
- iconCls: "opendxp_icon_email opendxp_icon_overlay_add",
- handler: this.addDocument.bind(this, tree, record, "email")
+ text: '> ' + t('blank'),
+ iconCls: 'opendxp_icon_email opendxp_icon_overlay_add',
+ handler: this.addDocument.bind(this, tree, record, 'email')
});
}
//don't add pages below print containers - makes no sense
- if (addDocuments && !opendxp.helpers.documentTypeHasSpecificRole(record.data.type, "only_printable_childrens")) {
+ if (addDocuments && !opendxp.helpers.documentTypeHasSpecificRole(record.data.type, 'only_printable_childrens')) {
menu.add(new Ext.menu.Item({
text: t('add_page'),
- iconCls: "opendxp_icon_page opendxp_icon_overlay_add",
+ iconCls: 'opendxp_icon_page opendxp_icon_overlay_add',
menu: documentMenu.page,
hideOnClick: false
}));
@@ -422,26 +412,26 @@ opendxp.document.tree = Class.create({
if (addSnippet) {
menu.add(new Ext.menu.Item({
text: t('add_snippet'),
- iconCls: "opendxp_icon_snippet opendxp_icon_overlay_add",
+ iconCls: 'opendxp_icon_snippet opendxp_icon_overlay_add',
menu: documentMenu.snippet,
hideOnClick: false
}));
}
//don't add emails and links below print containers - makes no sense
- if (addDocuments && !opendxp.helpers.documentTypeHasSpecificRole(record.data.type, "only_printable_childrens")) {
+ if (addDocuments && !opendxp.helpers.documentTypeHasSpecificRole(record.data.type, 'only_printable_childrens')) {
if (addLink) {
menu.add(new Ext.menu.Item({
text: t('add_link'),
- iconCls: "opendxp_icon_link opendxp_icon_overlay_add",
- handler: this.addDocument.bind(this, tree, record, "link")
+ iconCls: 'opendxp_icon_link opendxp_icon_overlay_add',
+ handler: this.addDocument.bind(this, tree, record, 'link')
}));
}
if (addEmail) {
menu.add(new Ext.menu.Item({
text: t('add_email'),
- iconCls: "opendxp_icon_email opendxp_icon_overlay_add",
+ iconCls: 'opendxp_icon_email opendxp_icon_overlay_add',
menu: documentMenu.email,
hideOnClick: false
}));
@@ -451,102 +441,100 @@ opendxp.document.tree = Class.create({
if (addHardlink) {
menu.add(new Ext.menu.Item({
text: t('add_hardlink'),
- iconCls: "opendxp_icon_hardlink opendxp_icon_overlay_add",
- handler: this.addDocument.bind(this, tree, record, "hardlink")
+ iconCls: 'opendxp_icon_hardlink opendxp_icon_overlay_add',
+ handler: this.addDocument.bind(this, tree, record, 'hardlink')
}));
}
}
- if (perspectiveCfg.inTreeContextMenu("document.addFolder")) {
+ if (perspectiveCfg.inTreeContextMenu('document.addFolder')) {
menu.add(new Ext.menu.Item({
text: t('create_folder'),
- iconCls: "opendxp_icon_folder opendxp_icon_overlay_add",
- handler: this.addDocument.bind(this, tree, record, "folder")
+ iconCls: 'opendxp_icon_folder opendxp_icon_overlay_add',
+ handler: this.addDocument.bind(this, tree, record, 'folder')
}));
}
- menu.add("-");
-
+ menu.add('-');
//paste
- if (opendxp.cachedDocumentId && record.data.permissions && record.data.permissions.create && perspectiveCfg.inTreeContextMenu("document.paste")) {
+ if (opendxp.cachedDocumentId && record.data.permissions && record.data.permissions.create && perspectiveCfg.inTreeContextMenu('document.paste')) {
pasteMenu.push({
- text: t("paste_recursive_as_child"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteInfo.bind(this, tree, record, "recursive")
+ text: t('paste_recursive_as_child'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteInfo.bind(this, tree, record, 'recursive')
});
pasteMenu.push({
- text: t("paste_recursive_updating_references"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteInfo.bind(this, tree, record, "recursive-update-references")
+ text: t('paste_recursive_updating_references'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteInfo.bind(this, tree, record, 'recursive-update-references')
});
pasteMenu.push({
- text: t("paste_as_child"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteInfo.bind(this, tree, record, "child")
+ text: t('paste_as_child'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteInfo.bind(this, tree, record, 'child')
});
pasteMenu.push({
- text: t("paste_as_language_variant"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteLanguageDocument.bind(this, tree, record, "child")
+ text: t('paste_as_language_variant'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteLanguageDocument.bind(this, tree, record, 'child')
});
pasteMenu.push({
- text: t("paste_recursive_as_language_variant"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteLanguageDocument.bind(this, tree, record, "recursive")
+ text: t('paste_recursive_as_language_variant'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteLanguageDocument.bind(this, tree, record, 'recursive')
});
pasteMenu.push({
- text: t("paste_recursive_as_language_variant_updating_references"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteLanguageDocument.bind(this, tree, record, "recursive-update-references")
+ text: t('paste_recursive_as_language_variant_updating_references'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteLanguageDocument.bind(this, tree, record, 'recursive-update-references')
});
pasteInheritanceMenu.push({
- text: t("paste_recursive_as_child"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteInfo.bind(this, tree, record, "recursive", true)
+ text: t('paste_recursive_as_child'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteInfo.bind(this, tree, record, 'recursive', true)
});
pasteInheritanceMenu.push({
- text: t("paste_recursive_updating_references"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteInfo.bind(this, tree, record, "recursive-update-references", true)
+ text: t('paste_recursive_updating_references'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteInfo.bind(this, tree, record, 'recursive-update-references', true)
});
pasteInheritanceMenu.push({
- text: t("paste_as_child"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteInfo.bind(this, tree, record, "child", true)
+ text: t('paste_as_child'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteInfo.bind(this, tree, record, 'child', true)
});
pasteInheritanceMenu.push({
- text: t("paste_as_language_variant"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteLanguageDocument.bind(this, tree, record, "child", true)
+ text: t('paste_as_language_variant'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteLanguageDocument.bind(this, tree, record, 'child', true)
});
pasteInheritanceMenu.push({
- text: t("paste_recursive_as_language_variant"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteLanguageDocument.bind(this, tree, record, "recursive", true)
+ text: t('paste_recursive_as_language_variant'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteLanguageDocument.bind(this, tree, record, 'recursive', true)
});
pasteInheritanceMenu.push({
- text: t("paste_recursive_as_language_variant_updating_references"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteLanguageDocument.bind(this, tree, record, "recursive-update-references", true)
+ text: t('paste_recursive_as_language_variant_updating_references'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteLanguageDocument.bind(this, tree, record, 'recursive-update-references', true)
});
}
}
-
//paste
- if (childSupportedDocument && opendxp.cutDocument && record.data.permissions && record.data.permissions.create && perspectiveCfg.inTreeContextMenu("document.pasteCut")) {
+ if (childSupportedDocument && opendxp.cutDocument && record.data.permissions && record.data.permissions.create && perspectiveCfg.inTreeContextMenu('document.pasteCut')) {
pasteMenu.push({
- text: t("paste_cut_element"),
- iconCls: "opendxp_icon_paste",
+ text: t('paste_cut_element'),
+ iconCls: 'opendxp_icon_paste',
handler: function () {
this.pasteCutDocument(opendxp.cutDocument,
opendxp.cutDocumentParentNode, record, this.tree);
@@ -556,13 +544,13 @@ opendxp.document.tree = Class.create({
});
}
- if (opendxp.cachedDocumentId && record.data.permissions && record.data.permissions.create && perspectiveCfg.inTreeContextMenu("document.paste")) {
+ if (opendxp.cachedDocumentId && record.data.permissions && record.data.permissions.create && perspectiveCfg.inTreeContextMenu('document.paste')) {
- if (record.data.type != "folder") {
+ if (record.data.type !== 'folder') {
pasteMenu.push({
- text: t("paste_contents"),
- iconCls: "opendxp_icon_paste",
- handler: this.pasteInfo.bind(this, tree, record, "replace")
+ text: t('paste_contents'),
+ iconCls: 'opendxp_icon_paste',
+ handler: this.pasteInfo.bind(this, tree, record, 'replace')
});
}
}
@@ -570,7 +558,7 @@ opendxp.document.tree = Class.create({
if (pasteMenu.length > 0) {
menu.add(new Ext.menu.Item({
text: t('paste'),
- iconCls: "opendxp_icon_paste",
+ iconCls: 'opendxp_icon_paste',
hideOnClick: false,
menu: pasteMenu
}));
@@ -579,66 +567,65 @@ opendxp.document.tree = Class.create({
if (pasteInheritanceMenu.length > 0) {
menu.add(new Ext.menu.Item({
text: t('paste_inheritance'),
- iconCls: "opendxp_icon_paste",
+ iconCls: 'opendxp_icon_paste',
hideOnClick: false,
menu: pasteInheritanceMenu
}));
}
- if (record.data.permissions && record.data.permissions.view && perspectiveCfg.inTreeContextMenu("document.copy")) {
+ if (record.data.permissions && record.data.permissions.view && perspectiveCfg.inTreeContextMenu('document.copy')) {
menu.add(new Ext.menu.Item({
text: t('copy'),
- iconCls: "opendxp_icon_copy",
+ iconCls: 'opendxp_icon_copy',
handler: this.copy.bind(this, tree, record)
}));
}
- if (record.data.id != 1 && !record.data.locked && record.data.permissions && record.data.permissions.rename && perspectiveCfg.inTreeContextMenu("document.cut")) {
+ if (record.data.id != 1 && !record.data.locked && record.data.permissions && record.data.permissions.rename && perspectiveCfg.inTreeContextMenu('document.cut')) {
menu.add(new Ext.menu.Item({
text: t('cut'),
- iconCls: "opendxp_icon_cut",
+ iconCls: 'opendxp_icon_cut',
handler: this.cut.bind(this, tree, record)
}));
}
- if (record.data.permissions && record.data.permissions.rename && record.data.id != 1 && !record.data.locked && perspectiveCfg.inTreeContextMenu("document.rename")) {
+ if (record.data.permissions && record.data.permissions.rename && record.data.id != 1 && !record.data.locked && perspectiveCfg.inTreeContextMenu('document.rename')) {
menu.add(new Ext.menu.Item({
text: t('rename'),
- iconCls: "opendxp_icon_key opendxp_icon_overlay_go",
+ iconCls: 'opendxp_icon_key opendxp_icon_overlay_go',
handler: this.editDocumentKey.bind(this, tree, record)
}));
}
//publish
- if (record.data.type != "folder" && !record.data.locked) {
- if (record.data.published && record.data.permissions && record.data.permissions.unpublish && perspectiveCfg.inTreeContextMenu("document.unpublish")) {
+ if (record.data.type !== 'folder' && !record.data.locked) {
+ if (record.data.published && record.data.permissions && record.data.permissions.unpublish && perspectiveCfg.inTreeContextMenu('document.unpublish')) {
menu.add(new Ext.menu.Item({
text: t('unpublish'),
- iconCls: "opendxp_icon_unpublish",
+ iconCls: 'opendxp_icon_unpublish',
handler: this.publishDocument.bind(this, tree, record, 'unpublish')
}));
- } else if (!record.data.published && record.data.permissions && record.data.permissions.publish && perspectiveCfg.inTreeContextMenu("document.publish")) {
+ } else if (!record.data.published && record.data.permissions && record.data.permissions.publish && perspectiveCfg.inTreeContextMenu('document.publish')) {
menu.add(new Ext.menu.Item({
text: t('publish'),
- iconCls: "opendxp_icon_publish",
+ iconCls: 'opendxp_icon_publish',
handler: this.publishDocument.bind(this, tree, record, 'publish')
}));
}
}
-
- if (record.data.permissions && record.data.permissions.remove && record.data.id != 1 && !record.data.locked && perspectiveCfg.inTreeContextMenu("document.delete")) {
+ if (record.data.permissions && record.data.permissions.remove && record.data.id != 1 && !record.data.locked && perspectiveCfg.inTreeContextMenu('document.delete')) {
menu.add(new Ext.menu.Item({
text: t('delete'),
- iconCls: "opendxp_icon_delete",
+ iconCls: 'opendxp_icon_delete',
handler: this.deleteDocument.bind(this, record.data.id)
}));
}
- if ((record.data.type == "page" || record.data.type == "hardlink") && record.data.permissions && record.data.permissions.view && perspectiveCfg.inTreeContextMenu("document.open")) {
+ if ((record.data.type === 'page' || record.data.type === 'hardlink') && record.data.permissions && record.data.permissions.view && perspectiveCfg.inTreeContextMenu('document.open')) {
menu.add(new Ext.menu.Item({
text: t('open_in_new_window'),
- iconCls: "opendxp_icon_open_window",
+ iconCls: 'opendxp_icon_open_window',
handler: function () {
window.open(record.data.url);
}.bind(this)
@@ -647,56 +634,56 @@ opendxp.document.tree = Class.create({
// advanced menu
var advancedMenuItems = [];
- var user = opendxp.globalmanager.get("user");
+ var user = opendxp.globalmanager.get('user');
- if (record.data.id != 1 && record.data.permissions && record.data.permissions.publish && !record.data.locked && perspectiveCfg.inTreeContextMenu("document.convert")) {
+ if (record.data.id != 1 && record.data.permissions && record.data.permissions.publish && !record.data.locked && perspectiveCfg.inTreeContextMenu('document.convert')) {
let conversionTargets = [];
- if(addDocuments) {
+ if (addDocuments) {
conversionTargets.push({
- text: t("page"),
- iconCls: "opendxp_icon_page",
- handler: this.convert.bind(this, tree, record, "page"),
- hidden: record.data.type == "page"
+ text: t('page'),
+ iconCls: 'opendxp_icon_page',
+ handler: this.convert.bind(this, tree, record, 'page'),
+ hidden: record.data.type === 'page'
});
}
- if(addSnippet) {
+ if (addSnippet) {
conversionTargets.push({
- text: t("snippet"),
- iconCls: "opendxp_icon_snippet",
- handler: this.convert.bind(this, tree, record, "snippet"),
- hidden: record.data.type == "snippet" || !addSnippet
+ text: t('snippet'),
+ iconCls: 'opendxp_icon_snippet',
+ handler: this.convert.bind(this, tree, record, 'snippet'),
+ hidden: record.data.type === 'snippet' || !addSnippet
});
}
- if(addEmail) {
+ if (addEmail) {
conversionTargets.push({
- text: t("email"),
- iconCls: "opendxp_icon_email",
- handler: this.convert.bind(this, tree, record, "email"),
- hidden: record.data.type == "email" || !addEmail
+ text: t('email'),
+ iconCls: 'opendxp_icon_email',
+ handler: this.convert.bind(this, tree, record, 'email'),
+ hidden: record.data.type === 'email' || !addEmail
});
}
- if(addLink) {
+ if (addLink) {
conversionTargets.push({
- text: t("link"),
- iconCls: "opendxp_icon_link",
- handler: this.convert.bind(this, tree, record, "link"),
- hidden: record.data.type == "link" || !addLink
+ text: t('link'),
+ iconCls: 'opendxp_icon_link',
+ handler: this.convert.bind(this, tree, record, 'link'),
+ hidden: record.data.type === 'link' || !addLink
});
}
- if(addHardlink) {
+ if (addHardlink) {
conversionTargets.push({
- text: t("hardlink"),
- iconCls: "opendxp_icon_hardlink",
- handler: this.convert.bind(this, tree, record, "hardlink"),
- hidden: record.data.type == "hardlink" || !addHardlink
+ text: t('hardlink'),
+ iconCls: 'opendxp_icon_hardlink',
+ handler: this.convert.bind(this, tree, record, 'hardlink'),
+ hidden: record.data.type === 'hardlink' || !addHardlink
});
}
- if(conversionTargets.length > 0) {
+ if (conversionTargets.length > 0) {
advancedMenuItems.push(new Ext.menu.Item({
text: t('convert_to'),
- iconCls: "opendxp_icon_convert",
+ iconCls: 'opendxp_icon_convert',
hideOnClick: false,
menu: conversionTargets
}));
@@ -706,38 +693,38 @@ opendxp.document.tree = Class.create({
if (childSupportedDocument &&
record.data.permissions &&
record.data.permissions.create &&
- perspectiveCfg.inTreeContextMenu("document.searchAndMove") &&
+ perspectiveCfg.inTreeContextMenu('document.searchAndMove') &&
opendxp.helpers.hasSearchImplementation()) {
advancedMenuItems.push({
text: t('search_and_move'),
- iconCls: "opendxp_icon_search opendxp_icon_overlay_go",
+ iconCls: 'opendxp_icon_search opendxp_icon_overlay_go',
handler: this.searchAndMove.bind(this, tree, record)
});
}
- if (record.data.id != 1 && record.data.type == "page" && (user.admin || user.isAllowed("sites"))) {
+ if (record.data.id != 1 && record.data.type === 'page' && (user.admin || user.isAllowed('sites'))) {
if (!record.data.site) {
- if (perspectiveCfg.inTreeContextMenu("document.useAsSite")) {
+ if (perspectiveCfg.inTreeContextMenu('document.useAsSite')) {
advancedMenuItems.push({
- iconCls: "opendxp_icon_site",
+ iconCls: 'opendxp_icon_site',
text: t('use_as_site'),
handler: this.addUpdateSite.bind(this, tree, record)
});
}
} else {
- if (perspectiveCfg.inTreeContextMenu("document.editSite")) {
+ if (perspectiveCfg.inTreeContextMenu('document.editSite')) {
advancedMenuItems.push({
text: t('edit_site'),
handler: this.addUpdateSite.bind(this, tree, record),
- iconCls: "opendxp_icon_edit",
+ iconCls: 'opendxp_icon_edit',
});
}
- if (perspectiveCfg.inTreeContextMenu("document.removeSite")) {
+ if (perspectiveCfg.inTreeContextMenu('document.removeSite')) {
advancedMenuItems.push({
text: t('remove_site'),
handler: this.removeSite.bind(this, tree, record),
- iconCls: "opendxp_icon_delete",
+ iconCls: 'opendxp_icon_delete',
});
}
}
@@ -747,13 +734,13 @@ opendxp.document.tree = Class.create({
if (record.data.id != 1 && user.admin) { // only admins are allowed to change locks in frontend
var lockMenu = [];
if (record.data.lockOwner) { // add unlock
- if (perspectiveCfg.inTreeContextMenu("document.unlock")) {
+ if (perspectiveCfg.inTreeContextMenu('document.unlock')) {
lockMenu.push({
text: t('unlock'),
- iconCls: "opendxp_icon_lock opendxp_icon_overlay_delete",
+ iconCls: 'opendxp_icon_lock opendxp_icon_overlay_delete',
handler: function () {
opendxp.elementservice.lockElement({
- elementType: "document",
+ elementType: 'document',
id: record.data.id,
mode: null
});
@@ -761,30 +748,30 @@ opendxp.document.tree = Class.create({
});
}
} else {
- if (perspectiveCfg.inTreeContextMenu("document.lock")) {
+ if (perspectiveCfg.inTreeContextMenu('document.lock')) {
lockMenu.push({
text: t('lock'),
- iconCls: "opendxp_icon_lock opendxp_icon_overlay_add",
+ iconCls: 'opendxp_icon_lock opendxp_icon_overlay_add',
handler: function () {
opendxp.elementservice.lockElement({
- elementType: "document",
+ elementType: 'document',
id: record.data.id,
- mode: "self"
+ mode: 'self'
});
}.bind(this)
});
}
- if (perspectiveCfg.inTreeContextMenu("document.lockAndPropagate")) {
- if (record.data.type != "snippet") {
+ if (perspectiveCfg.inTreeContextMenu('document.lockAndPropagate')) {
+ if (record.data.type !== 'snippet') {
lockMenu.push({
text: t('lock_and_propagate_to_children'),
- iconCls: "opendxp_icon_lock opendxp_icon_overlay_go",
+ iconCls: 'opendxp_icon_lock opendxp_icon_overlay_go',
handler: function () {
opendxp.elementservice.lockElement({
- elementType: "document",
+ elementType: 'document',
id: record.data.id,
- mode: "propagate"
+ mode: 'propagate'
});
}.bind(this)
});
@@ -792,14 +779,14 @@ opendxp.document.tree = Class.create({
}
}
- if (record.data["locked"] && perspectiveCfg.inTreeContextMenu("document.unlockAndPropagate")) {
+ if (record.data['locked'] && perspectiveCfg.inTreeContextMenu('document.unlockAndPropagate')) {
// add unlock and propagate to children functionality
lockMenu.push({
text: t('unlock_and_propagate_to_children'),
- iconCls: "opendxp_icon_lock opendxp_icon_overlay_delete",
+ iconCls: 'opendxp_icon_lock opendxp_icon_overlay_delete',
handler: function () {
opendxp.elementservice.unlockElement({
- elementType: "document",
+ elementType: 'document',
id: record.data.id
});
}.bind(this)
@@ -809,7 +796,7 @@ opendxp.document.tree = Class.create({
if (lockMenu.length > 0) {
advancedMenuItems.push({
text: t('lock'),
- iconCls: "opendxp_icon_lock",
+ iconCls: 'opendxp_icon_lock',
hideOnClick: false,
menu: lockMenu
});
@@ -821,7 +808,7 @@ opendxp.document.tree = Class.create({
if (record.data.expanded) {
advancedMenuItems.push({
text: t('collapse_children'),
- iconCls: "opendxp_icon_collapse_children",
+ iconCls: 'opendxp_icon_collapse_children',
handler: function () {
record.collapse(true);
}.bind(this, record)
@@ -829,7 +816,7 @@ opendxp.document.tree = Class.create({
} else {
advancedMenuItems.push({
text: t('expand_children'),
- iconCls: "opendxp_icon_expand_children",
+ iconCls: 'opendxp_icon_expand_children',
handler: function () {
record.expand(true);
}.bind(this, record)
@@ -837,21 +824,21 @@ opendxp.document.tree = Class.create({
}
}
- menu.add("-");
+ menu.add('-');
if (advancedMenuItems.length) {
menu.add(new Ext.menu.Item({
text: t('advanced'),
- iconCls: "opendxp_icon_more",
+ iconCls: 'opendxp_icon_more',
hideOnClick: false,
menu: advancedMenuItems
}));
}
- if (!record.data.leaf && perspectiveCfg.inTreeContextMenu("document.reload")) {
+ if (!record.data.leaf && perspectiveCfg.inTreeContextMenu('document.reload')) {
menu.add(new Ext.menu.Item({
text: t('refresh'),
- iconCls: "opendxp_icon_reload",
+ iconCls: 'opendxp_icon_reload',
handler: opendxp.elementservice.refreshNode.bind(this, record)
}));
}
@@ -869,7 +856,7 @@ opendxp.document.tree = Class.create({
document.dispatchEvent(prepareDocumentTreeContextMenu);
- menu.showAt(e.pageX+1, e.pageY+1);
+ menu.showAt(e.pageX + 1, e.pageY + 1);
},
pasteLanguageDocument: function (tree, record, type, enableInheritance) {
@@ -881,59 +868,59 @@ opendxp.document.tree = Class.create({
success: function (response) {
var data = Ext.decode(response.responseText);
- if (data.language === "") {
- opendxp.helpers.showNotification(t("error"), t("source_document_language_missing"), "error");
+ if (data.language === '') {
+ opendxp.helpers.showNotification(t('error'), t('source_document_language_missing'), 'error');
return false;
}
var languagestore = [];
var websiteLanguages = opendxp.settings.websiteLanguages;
- var selectContent = "";
+ var selectContent = '';
- for (var i=0; i
' + t('wildcards_are_supported') + ' (*example.com)',
+ value: data.domains.join("\n")
+ },
+ {
+ xtype: 'textfield',
+ name: 'errorDocument',
+ fieldCls: 'input_drop_target',
+ fieldLabel: t('error_page') + ' (' + t('default') + ')',
+ value: data['errorDocument'],
+ listeners: {
+ render: function (el) {
+ new Ext.dd.DropZone(el.getEl(), {
+ reference: this,
+ ddGroup: 'element',
+ getTargetFromEvent: function (e) {
+ return this.getEl();
+ }.bind(el),
+
+ onNodeOver: function (target, dd, e, data) {
+ if (
+ data.records.length === 1 &&
+ data.records[0].data.elementType === 'document' &&
+ in_array(data.records[0].data.type, ['page', 'link', 'hardlink'])
+ ) {
+ return Ext.dd.DropZone.prototype.dropAllowed;
+ }
+ },
+
+ onNodeDrop: function (target, dd, e, data) {
+
+ if (!opendxp.helpers.dragAndDropValidateSingleItem(data)) {
+ return false;
+ }
+
+ data = data.records[0].data;
+
+ if (
+ data.elementType === 'document' &&
+ in_array(data.type, ['page', 'link', 'hardlink'])
+ ) {
+ this.setValue(data.path);
+ return true;
+ }
+
+ return false;
+ }.bind(el)
+ });
+ }
+ }
+ },
+ {
+ xtype: 'container',
+ style: 'margin-top: 20px;',
+ items: this.renderErrorDocuments(data['localizedErrorDocuments']),
+ },
+ {
+ xtype: 'checkbox',
+ name: 'redirectToMainDomain',
+ fieldLabel: t('redirect_to_main_domain'),
+ checked: data['redirectToMainDomain']
+ },
+ {
+ xtype: 'form',
+ style: 'margin-top: 20px;',
+ title: t('site_custom_settings'),
+ hidden: true,
+ border: false,
+ items: [],
+ listeners: {
+ render: function (el) {
+ this.renderSiteCustomSettings(el, data)
+ }.bind(this)
+ }
+ }
+ ];
+
+ windowCfg = {
width: 600,
height: 600,
- layout: "fit",
- closeAction: "close",
+ layout: 'fit',
+ closeAction: 'close',
items: [{
autoScroll: true,
- xtype: "form",
- bodyStyle: "padding: 10px;",
+ xtype: 'form',
+ bodyStyle: 'padding: 10px;',
defaults: {
labelWidth: 250,
- width: 550
+ width: 560
},
- itemId: "form",
- items: [{
- xtype: "textfield",
- name: "mainDomain",
- fieldLabel: t("main_domain"),
- value: data["mainDomain"]
- }, {
- xtype: "textarea",
- name: "domains",
- height: 150,
- style: "word-wrap: normal;",
- fieldLabel: t("additional_domains") + "
" + t("wildcards_are_supported") + " (*example.com)",
- value: data.domains.join("\n")
- }, {
- xtype: "textfield",
- name: "errorDocument",
- fieldCls: "input_drop_target",
- fieldLabel: t("error_page") + " (" + t("default") + ")",
- value: data["errorDocument"],
- listeners: {
- "render": function (el) {
- new Ext.dd.DropZone(el.getEl(), {
- reference: this,
- ddGroup: "element",
- getTargetFromEvent: function(e) {
- return this.getEl();
- }.bind(el),
-
- onNodeOver : function(target, dd, e, data) {
- if (data.records.length === 1 && data.records[0].data.elementType === "document" && in_array(data.records[0].data.type, ["page", "link", "hardlink"])) {
- return Ext.dd.DropZone.prototype.dropAllowed;
- }
- },
-
- onNodeDrop : function (target, dd, e, data) {
-
- if(!opendxp.helpers.dragAndDropValidateSingleItem(data)) {
- return false;
- }
-
- data = data.records[0].data;
- if (data.elementType === "document" && in_array(data.type, ["page", "link", "hardlink"])) {
- this.setValue(data.path);
- return true;
- }
- return false;
- }.bind(el)
- });
- }
- }
- }, {
- xtype: "fieldset",
- style: "margin-top: 20px;",
- items: this.renderErrorDocuments(data["localizedErrorDocuments"]),
- },{
- xtype: "checkbox",
- name: "redirectToMainDomain",
- fieldLabel: t("redirect_to_main_domain"),
- checked: data["redirectToMainDomain"]
- }]
+ itemId: 'form',
+ items: siteItems
}],
buttons: [{
- text: t("cancel"),
- iconCls: "opendxp_icon_cancel",
+ text: t('cancel'),
+ iconCls: 'opendxp_icon_cancel',
handler: function () {
win.close();
}
}, {
- text: t("apply"),
- iconCls: "opendxp_icon_apply",
+ text: t('apply'),
+ iconCls: 'opendxp_icon_apply',
handler: function () {
- var data = win.getComponent("form").getForm().getFieldValues();
- data["id"] = record.id;
+
+ const form = win.getComponent('form').getForm();
+ const data = form.getFieldValues();
+
+ if (!form.isValid()) {
+ return;
+ }
+
+ data['id'] = record.id;
Ext.Ajax.request({
url: Routing.generate('opendxp_admin_document_document_updatesite'),
method: 'PUT',
params: data,
success: function (tree, record, response) {
- var site = Ext.decode(response.responseText);
- record.data.site = site;
+ record.data.site = Ext.decode(response.responseText);
tree.getStore().load({
node: record.parentNode
});
- opendxp.globalmanager.get("sites").reload();
+ opendxp.globalmanager.get('sites').reload();
}.bind(this, tree, record)
});
@@ -1304,31 +1328,31 @@ opendxp.document.tree = Class.create({
windowCfg.title = title;
}
- var win = new Ext.Window(windowCfg);
+ win = new Ext.Window(windowCfg);
win.show();
},
- addDocument : function (tree, record, type, docTypeId) {
+ addDocument: function (tree, record, type, docTypeId) {
var textKeyTitle;
var textKeyMessage;
- if(type == "page") {
+ if (type === 'page') {
- textKeyTitle = t("add_page");
- textKeyMessage = t("enter_the_name_of_the_new_item");
+ textKeyTitle = t('add_page');
+ textKeyMessage = t('enter_the_name_of_the_new_item');
//create a custom form
var pageForm = new Ext.form.FormPanel({
title: textKeyMessage,
border: false,
- bodyStyle: "padding: 10px;",
+ bodyStyle: 'padding: 10px;',
items: [{
- xtype: "textfield",
- itemId: "title",
+ xtype: 'textfield',
+ itemId: 'title',
fieldLabel: t('title'),
name: 'title',
- width: "100%",
+ width: '100%',
enableKeyEvents: true,
listeners: {
afterrender: function () {
@@ -1337,31 +1361,31 @@ opendxp.document.tree = Class.create({
}.bind(this), 100);
},
keyup: function (el) {
- pageForm.getComponent("name").setValue(el.getValue());
- pageForm.getComponent("key").setValue(el.getValue());
+ pageForm.getComponent('name').setValue(el.getValue());
+ pageForm.getComponent('key').setValue(el.getValue());
}.bind(this)
}
- },{
- xtype: "textfield",
- itemId: "name",
+ }, {
+ xtype: 'textfield',
+ itemId: 'name',
fieldLabel: t('navigation'),
name: 'name',
- width: "100%"
- },{
- xtype: "textfield",
- width: "100%",
+ width: '100%'
+ }, {
+ xtype: 'textfield',
+ width: '100%',
fieldLabel: t('key'),
- itemId: "key",
+ itemId: 'key',
name: 'key'
}]
});
- var submitFunction = function() {
+ var submitFunction = function () {
var params = pageForm.getForm().getFieldValues();
messageBox.close();
- if(params["key"].length >= 1) {
- params["type"] = type;
- params["docTypeId"] = docTypeId;
+ if (params['key'].length >= 1) {
+ params['type'] = type;
+ params['docTypeId'] = docTypeId;
this.addDocumentCreate(tree, record, params);
} else {
return; //ignore
@@ -1377,9 +1401,9 @@ opendxp.document.tree = Class.create({
buttons: [{
text: t('OK'),
handler: submitFunction.bind(this, tree, record)
- },{
+ }, {
text: t('cancel'),
- handler: function() {
+ handler: function () {
messageBox.close();
}
}]
@@ -1389,22 +1413,22 @@ opendxp.document.tree = Class.create({
var map = new Ext.util.KeyMap({
target: messageBox.getEl(),
- key: Ext.event.Event.ENTER,
+ key: Ext.event.Event.ENTER,
fn: submitFunction.bind(this)
});
} else {
- if (type == "folder") {
- textKeyTitle = t("create_folder");
- textKeyMessage = t("enter_the_name_of_the_new_item");
+ if (type === 'folder') {
+ textKeyTitle = t('create_folder');
+ textKeyMessage = t('enter_the_name_of_the_new_item');
} else {
- textKeyTitle = t("add_" + type);
- textKeyMessage = t("enter_the_name_of_the_new_item");
+ textKeyTitle = t('add_' + type);
+ textKeyMessage = t('enter_the_name_of_the_new_item');
}
Ext.MessageBox.prompt(textKeyTitle, textKeyMessage, function (tree, record, type, docTypeId, button, value, object) {
- if (button == "ok") {
+ if (button === 'ok') {
this.addDocumentCreate(
tree, record,
@@ -1425,10 +1449,10 @@ opendxp.document.tree = Class.create({
var parameters = {};
parameters.id = id;
- var doc = opendxp.globalmanager.get("document_" + id);
+ var doc = opendxp.globalmanager.get('document_' + id);
if (doc) {
- if (task == "publish") {
+ if (task === 'publish') {
doc.publish(false);
} else {
doc.unpublish(false);
@@ -1436,30 +1460,29 @@ opendxp.document.tree = Class.create({
} else {
Ext.Ajax.request({
url: Routing.generate('opendxp_admin_document_' + type + '_save', {task: task}),
- method: "PUT",
+ method: 'PUT',
params: parameters,
success: function (task, response) {
try {
var rdata = Ext.decode(response.responseText);
if (rdata && rdata.success) {
var options = {
- elementType: "document",
+ elementType: 'document',
id: record.data.id,
- published: task != "unpublish"
+ published: task !== 'unpublish'
};
opendxp.elementservice.setElementPublishedState(options);
opendxp.elementservice.setElementToolbarButtons(options);
opendxp.elementservice.reloadVersions(options);
- opendxp.helpers.showNotification(t("success"), t("successful_" + task + "_document"),
- "success");
- }
- else {
- opendxp.helpers.showNotification(t("error"), t("error_" + task + "_document"),
- "error", t(rdata.message));
+ opendxp.helpers.showNotification(t('success'), t('successful_' + task + '_document'),
+ 'success');
+ } else {
+ opendxp.helpers.showNotification(t('error'), t('error_' + task + '_document'),
+ 'error', t(rdata.message));
}
} catch (e) {
- opendxp.helpers.showNotification(t("error"), t("error_" + task + "_document"), "error");
+ opendxp.helpers.showNotification(t('error'), t('error_' + task + '_document'), 'error');
}
}.bind(this, task)
@@ -1467,24 +1490,24 @@ opendxp.document.tree = Class.create({
}
},
- addDocumentCreate : function (tree, record, params) {
+ addDocumentCreate: function (tree, record, params) {
- if(params["key"]) {
+ if (params['key']) {
// check for ident filename in current level
- if(opendxp.elementservice.isKeyExistingInLevel(record, params["key"])) {
+ if (opendxp.elementservice.isKeyExistingInLevel(record, params['key'])) {
return;
}
- if(opendxp.elementservice.isDisallowedDocumentKey(record.id, params["key"])) {
+ if (opendxp.elementservice.isDisallowedDocumentKey(record.id, params['key'])) {
return;
}
- params["sourceTree"] = tree;
- params["elementType"] = "document";
- params["key"] = opendxp.helpers.getValidFilename(params["key"], "document");
- params["index"] = record.childNodes.length;
- params["parentId"] = record.id;
- params["url"] = Routing.generate('opendxp_admin_document_document_add');
+ params['sourceTree'] = tree;
+ params['elementType'] = 'document';
+ params['key'] = opendxp.helpers.getValidFilename(params['key'], 'document');
+ params['index'] = record.childNodes.length;
+ params['parentId'] = record.id;
+ params['url'] = Routing.generate('opendxp_admin_document_document_add');
opendxp.elementservice.addDocument(params);
}
},
@@ -1492,7 +1515,7 @@ opendxp.document.tree = Class.create({
editDocumentKey: function (tree, record) {
var options = {
sourceTree: tree,
- elementType: "document",
+ elementType: 'document',
elementSubType: record.data.type,
id: record.data.id,
default: record.data.key
@@ -1500,37 +1523,37 @@ opendxp.document.tree = Class.create({
opendxp.elementservice.editElementKey(options);
},
- deleteDocument : function (ids) {
+ deleteDocument: function (ids) {
var options = {
- "elementType" : "document",
- "id": ids
+ 'elementType': 'document',
+ 'id': ids
};
opendxp.elementservice.deleteElement(options);
},
convert: function (tree, record, type) {
Ext.MessageBox.show({
- title:t('are_you_sure'),
- msg: t("all_content_will_be_lost"),
- buttons: Ext.Msg.OKCANCEL ,
- icon: Ext.MessageBox.INFO ,
+ title: t('are_you_sure'),
+ msg: t('all_content_will_be_lost'),
+ buttons: Ext.Msg.OKCANCEL,
+ icon: Ext.MessageBox.INFO,
fn: function (type, button) {
- if (button == "ok") {
+ if (button === 'ok') {
- if (opendxp.globalmanager.exists("document_" + record.data.id)) {
- var tabPanel = Ext.getCmp("opendxp_panel_tabs");
- tabPanel.remove("document_" + record.data.id);
+ if (opendxp.globalmanager.exists('document_' + record.data.id)) {
+ var tabPanel = Ext.getCmp('opendxp_panel_tabs');
+ tabPanel.remove('document_' + record.data.id);
}
Ext.Ajax.request({
url: Routing.generate('opendxp_admin_document_document_convert'),
- method: "PUT",
+ method: 'PUT',
params: {
id: record.data.id,
type: type
},
success: function () {
- opendxp.elementservice.refreshNodeAllTrees("document", record.parentNode.id);
+ opendxp.elementservice.refreshNodeAllTrees('document', record.parentNode.id);
}.bind(this)
});
}
@@ -1538,14 +1561,13 @@ opendxp.document.tree = Class.create({
});
},
- searchAndMove: function(tree, record) {
+ searchAndMove: function (tree, record) {
var parentId = record.data.id;
- opendxp.helpers.searchAndMove(parentId, function() {
+ opendxp.helpers.searchAndMove(parentId, function () {
opendxp.elementservice.refreshNode(record);
- }.bind(this), "document");
+ }.bind(this), 'document');
},
-
isKeyValid: function (key) {
// key must be at least one character, an maximum 30 characters
@@ -1554,13 +1576,13 @@ opendxp.document.tree = Class.create({
}
},
- updateOpenDocumentPaths: function(node) {
+ updateOpenDocumentPaths: function (node) {
try {
var openTabs = opendxp.helpers.getOpenTab();
for (var i = 0; i < openTabs.length; i++) {
- if(openTabs[i].indexOf("document_") == 0 && (openTabs[i].indexOf("_page") || openTabs[i].indexOf("_snippet") || openTabs[i].indexOf("_email"))) {
- var documentElement = opendxp.globalmanager.get(openTabs[i].replace(/_page|_snippet|_email/gi,''));
- if(typeof documentElement.data != 'undefined' && documentElement.data.idPath.indexOf("/" + node.data.id) > 0) {
+ if (openTabs[i].indexOf('document_') === 0 && (openTabs[i].indexOf('_page') || openTabs[i].indexOf('_snippet') || openTabs[i].indexOf('_email'))) {
+ var documentElement = opendxp.globalmanager.get(openTabs[i].replace(/_page|_snippet|_email/gi, ''));
+ if (typeof documentElement.data !== 'undefined' && documentElement.data.idPath.indexOf('/' + node.data.id) > 0) {
documentElement.resetPath();
}
}
@@ -1570,11 +1592,12 @@ opendxp.document.tree = Class.create({
}
},
- renderErrorDocuments: function(localizedErrorDocumentsData) {
- var localizedErrorDocumentFields = []
- var availableLanguages = opendxp.available_languages
+ renderErrorDocuments: function (localizedErrorDocumentsData) {
+
+ var localizedErrorDocumentFields = [],
+ availableLanguages = opendxp.available_languages,
+ websiteLanguages = opendxp.settings.websiteLanguages;
- var websiteLanguages = opendxp.settings.websiteLanguages;
if (websiteLanguages && websiteLanguages.length > 0) {
Ext.each(websiteLanguages, function (language) {
if (empty(language)) {
@@ -1582,35 +1605,31 @@ opendxp.document.tree = Class.create({
}
localizedErrorDocumentFields.push({
- fieldLabel: t("error_page") + " (" + availableLanguages[language] + ")",
- name: "errorDocument.localized." + language,
- fieldCls: "input_drop_target",
+ fieldLabel: t('error_page') + ' - ' + availableLanguages[language],
+ name: 'errorDocument.localized.' + language,
+ fieldCls: 'input_drop_target',
value: (localizedErrorDocumentsData && localizedErrorDocumentsData[language]) ? localizedErrorDocumentsData[language] : '',
- labelWidth: 200,
- width: 500,
- xtype: "textfield",
+ labelWidth: 250,
+ width: 560,
+ xtype: 'textfield',
listeners: {
- "render": function (el) {
+ render: function (el) {
new Ext.dd.DropZone(el.getEl(), {
reference: this,
- ddGroup: "element",
+ ddGroup: 'element',
getTargetFromEvent: function (e) {
return this.getEl();
}.bind(el),
-
onNodeOver: function (target, dd, e, data) {
- if (data.records.length == 1 && data.records[0].data.elementType == "document") {
+ if (data.records.length === 1 && data.records[0].data.elementType === 'document') {
return Ext.dd.DropZone.prototype.dropAllowed;
}
},
-
onNodeDrop: function (target, dd, e, data) {
if (opendxp.helpers.dragAndDropValidateSingleItem(data)) {
var record = data.records[0];
- var data = record.data;
-
- if (data.elementType == "document") {
- this.setValue(data.path);
+ if (record.data.elementType === 'document') {
+ this.setValue(record.data.path);
return true;
}
}
@@ -1624,5 +1643,89 @@ opendxp.document.tree = Class.create({
}
return localizedErrorDocumentFields;
+ },
+
+ renderSiteCustomSettings: function (container, site) {
+
+ const additionalConfigFactory = {
+ input: (node, nodeValue) => ({
+ value: nodeValue,
+ allowBlank: (node.config.required ?? false) !== true,
+ }),
+ text: (node, nodeValue) => ({
+ xtype: 'textarea',
+ grow: true,
+ value: nodeValue,
+ allowBlank: (node.config.required ?? false) !== true,
+ }),
+ checkbox: (node, nodeValue) => ({
+ checked: nodeValue ?? false,
+ inputValue: node.config.checkedValue ?? true,
+ uncheckedValue: node.config.uncheckedValue ?? false,
+ }),
+ combobox: (node, nodeValue) => ({
+ store: node.config.store ?? [],
+ queryMode: 'local',
+ displayField: node.config.displayField ?? 'label',
+ valueField: node.config.valueField ?? 'value',
+ allowBlank: (node.config.required ?? false) !== true,
+ editable: false,
+ forceSelection: true,
+ value: nodeValue,
+ })
+ };
+
+ Ext.Ajax.request({
+ url: Routing.generate('opendxp_admin_document_document_get_site_custom_settings'),
+ method: 'POST',
+ params: {
+ id: site.id,
+ },
+ success: function (response) {
+
+ const r = Ext.decode(response.responseText);
+
+ if (!Ext.isObject(r.data)) {
+ return;
+ }
+
+ Ext.Object.each(r.data, function (scope, settingsBlock) {
+
+ const fieldset = {
+ xtype: 'fieldset',
+ title: scope,
+ items: []
+ };
+
+ Ext.Array.each(settingsBlock, function (configNode) {
+
+ const nodeValue = site.customSettings?.[scope]?.[configNode.name] ?? null;;
+
+ const baseConfig = {
+ xtype: configNode.type,
+ fieldLabel: configNode.label,
+ name: 'customSettings.' + scope + '.' + configNode.name,
+ labelWidth: 200,
+ anchor: '100%',
+ };
+
+ const additionalConfig =
+ additionalConfigFactory[configNode.type]
+ ? additionalConfigFactory[configNode.type](configNode, nodeValue)
+ : {};
+
+ const nodeConfig = Ext.apply({}, baseConfig, additionalConfig);
+
+ fieldset.items.push(nodeConfig);
+ });
+
+ container.add(fieldset)
+ });
+
+ container.setHidden(false);
+
+ }.bind(this)
+ });
+
}
});
diff --git a/public/js/opendxp/settings/system.js b/public/js/opendxp/settings/system.js
index 31c389bb..36409b1f 100644
--- a/public/js/opendxp/settings/system.js
+++ b/public/js/opendxp/settings/system.js
@@ -11,14 +11,11 @@
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3 (GPLv3)
*/
-opendxp.registerNS("opendxp.settings.system");
-/**
- * @private
- */
+opendxp.registerNS('opendxp.settings.system');
+
opendxp.settings.system = Class.create({
initialize: function () {
-
this.getData();
},
@@ -29,7 +26,6 @@ opendxp.settings.system = Class.create({
this.data = Ext.decode(response.responseText);
- //valid languages
try {
this.languagesStore = new Ext.data.JsonStore({
autoDestroy: true,
@@ -49,7 +45,6 @@ opendxp.settings.system = Class.create({
});
}
-
this.getTabPanel();
}.bind(this)
@@ -58,11 +53,11 @@ opendxp.settings.system = Class.create({
getValue: function (key, ignoreCheck) {
- var nk = key.split("\.");
- var current = this.data.values;
+ var nk = key.split('\.'),
+ current = this.data.values;
for (var i = 0; i < nk.length; i++) {
- if (typeof current[nk[i]] != "undefined") {
+ if (typeof current[nk[i]] !== 'undefined') {
current = current[nk[i]];
} else {
current = null;
@@ -70,31 +65,31 @@ opendxp.settings.system = Class.create({
}
}
- if (ignoreCheck || (typeof current != "object" && typeof current != "array" && typeof current != "function")) {
+ if (ignoreCheck || (typeof current !== 'object' && typeof current !== 'array' && typeof current !== 'function')) {
return current;
}
- return "";
+ return '';
},
getTabPanel: function () {
- let urlToCustomImageField = {};
+
+ var tabPanel;
if (!this.panel) {
this.panel = Ext.create('Ext.panel.Panel', {
- id: "opendxp_settings_system",
- title: t("system_settings"),
- iconCls: "opendxp_icon_system",
+ id: 'opendxp_settings_system',
+ title: t('system_settings'),
+ iconCls: 'opendxp_icon_system',
border: false,
- layout: "fit",
+ layout: 'fit',
closable: true
});
- this.panel.on("destroy", function () {
- opendxp.globalmanager.remove("settings_system");
+ this.panel.on('destroy', function () {
+ opendxp.globalmanager.remove('settings_system');
}.bind(this));
-
this.layout = Ext.create('Ext.form.Panel', {
bodyStyle: 'padding:20px 5px 20px 5px;',
border: false,
@@ -108,16 +103,16 @@ opendxp.settings.system = Class.create({
},
buttons: [
{
- text: t("save"),
+ text: t('save'),
handler: this.save.bind(this),
- iconCls: "opendxp_icon_apply",
- disabled: !this.getValue("writeable")
+ iconCls: 'opendxp_icon_apply',
+ disabled: !this.getValue('writeable')
}
],
items: [
{
xtype: 'fieldset',
- title: t('localization_and_internationalization') + " (i18n/l10n)",
+ title: t('localization_and_internationalization') + ' (i18n/l10n)',
collapsible: true,
collapsed: true,
autoHeight: true,
@@ -125,27 +120,27 @@ opendxp.settings.system = Class.create({
defaultType: 'textfield',
defaults: {width: 300},
items: [{
- xtype: "container",
+ xtype: 'container',
html: '' + t('frontend_languages') + ''
}, {
- xtype: "displayfield",
+ xtype: 'displayfield',
hideLabel: true,
width: 600,
- value: t('valid_languages_frontend_description') + "
" + t('delete_language_note'),
- cls: "opendxp_extra_label_bottom"
+ value: t('valid_languages_frontend_description') + '
' + t('delete_language_note'),
+ cls: 'opendxp_extra_label_bottom'
},
{
- xtype: "fieldset",
- layout: "hbox",
+ xtype: 'fieldset',
+ layout: 'hbox',
border: false,
- style: "border-top: none !important",
+ style: 'border-top: none !important',
padding: 0,
width: 600,
items: [{
labelWidth: 150,
- fieldLabel: t("add_language"),
- xtype: "combo",
- id: "system_settings_general_languageSelection",
+ fieldLabel: t('add_language'),
+ xtype: 'combo',
+ id: 'system_settings_general_languageSelection',
triggerAction: 'all',
queryMode: 'local',
store: this.languagesStore,
@@ -156,38 +151,38 @@ opendxp.settings.system = Class.create({
anyMatch: true,
width: 450
}, {
- xtype: "button",
- iconCls: "opendxp_icon_add",
+ xtype: 'button',
+ iconCls: 'opendxp_icon_add',
handler: function () {
- var combo = Ext.getCmp("system_settings_general_languageSelection");
+ var combo = Ext.getCmp('system_settings_general_languageSelection');
this.addLanguage(combo.getValue());
}.bind(this)
}]
}, {
- xtype: "hidden",
- id: "system_settings_general_validLanguages",
+ xtype: 'hidden',
+ id: 'system_settings_general_validLanguages',
name: 'general.validLanguages',
- value: this.getValue("general.valid_languages", true)
+ value: this.getValue('general.valid_languages', true)
}, {
- xtype: "hidden",
- id: "system_settings_general_requiredLanguages",
+ xtype: 'hidden',
+ id: 'system_settings_general_requiredLanguages',
name: 'general.requiredLanguages',
- value: this.getValue("general.required_languages", true)
+ value: this.getValue('general.required_languages', true)
}, {
- xtype: "hidden",
- id: "system_settings_general_defaultLanguage",
- name: "general.defaultLanguage",
- value: this.getValue("general.default_language")
+ xtype: 'hidden',
+ id: 'system_settings_general_defaultLanguage',
+ name: 'general.defaultLanguage',
+ value: this.getValue('general.default_language')
}, {
- xtype: "container",
+ xtype: 'container',
width: 450,
- style: "margin-top: 20px;",
- id: "system_settings_general_languageContainer",
+ style: 'margin-top: 20px;',
+ id: 'system_settings_general_languageContainer',
items: [],
listeners: {
beforerender: function () {
// add existing language entries
- var locales = this.getValue("general.valid_languages", true);
+ var locales = this.getValue('general.valid_languages', true);
if (locales && locales.length > 0) {
Ext.each(locales, this.addLanguage.bind(this));
}
@@ -197,7 +192,7 @@ opendxp.settings.system = Class.create({
},
{
xtype: 'fieldset',
- title: "Debug",
+ title: 'Debug',
collapsible: true,
collapsed: true,
autoHeight: true,
@@ -205,17 +200,17 @@ opendxp.settings.system = Class.create({
defaultType: 'textfield',
defaults: {width: 600},
items: [{
- boxLabel: t("debug_admin_translations"),
- xtype: "checkbox",
- name: "general.debug_admin_translations",
- checked: this.getValue("general.debug_admin_translations")
+ boxLabel: t('debug_admin_translations'),
+ xtype: 'checkbox',
+ name: 'general.debug_admin_translations',
+ checked: this.getValue('general.debug_admin_translations')
}, {
xtype: 'textfield',
width: 650,
- fieldLabel: t("email_debug_addresses") + "(CSV)" + ' *',
+ fieldLabel: t('email_debug_addresses') + '(CSV)' + ' *',
name: 'email.debug.emailAddresses',
- value: this.getValue("email.debug.email_addresses"),
- emptyText: "john@doe.com,jane@doe.com"
+ value: this.getValue('email.debug.email_addresses'),
+ emptyText: 'john@doe.com,jane@doe.com'
}]
},
{
@@ -229,34 +224,34 @@ opendxp.settings.system = Class.create({
defaults: {width: 500},
items: [
{
- fieldLabel: t("main_domain"),
- name: "general.domain",
- value: this.getValue("general.domain")
+ fieldLabel: t('main_domain'),
+ name: 'general.domain',
+ value: this.getValue('general.domain')
},
{
- xtype: "checkbox",
- boxLabel: t("redirect_unknown_domains_to_main_domain"),
- name: "general.redirect_to_maindomain",
- checked: this.getValue("general.redirect_to_maindomain")
+ xtype: 'checkbox',
+ boxLabel: t('redirect_unknown_domains_to_main_domain'),
+ name: 'general.redirect_to_maindomain',
+ checked: this.getValue('general.redirect_to_maindomain')
},
{
- fieldLabel: t("error_page") + " (" + t("default") + ")",
- name: "documents.error_pages.default",
- fieldCls: "input_drop_target",
- value: this.getValue("documents.error_pages.default"),
+ fieldLabel: t('error_page') + ' (' + t('default') + ')',
+ name: 'documents.error_pages.default',
+ fieldCls: 'input_drop_target',
+ value: this.getValue('documents.error_pages.default'),
width: 600,
- xtype: "textfield",
+ xtype: 'textfield',
listeners: {
- "render": function (el) {
+ render: function (el) {
new Ext.dd.DropZone(el.getEl(), {
reference: this,
- ddGroup: "element",
+ ddGroup: 'element',
getTargetFromEvent: function (e) {
return this.getEl();
}.bind(el),
onNodeOver: function (target, dd, e, data) {
- if (data.records.length == 1 && data.records[0].data.elementType == "document") {
+ if (data.records.length === 1 && data.records[0].data.elementType === 'document') {
return Ext.dd.DropZone.prototype.dropAllowed;
}
},
@@ -264,10 +259,8 @@ opendxp.settings.system = Class.create({
onNodeDrop: function (target, dd, e, data) {
if (opendxp.helpers.dragAndDropValidateSingleItem(data)) {
var record = data.records[0];
- var data = record.data;
-
- if (data.elementType == "document") {
- this.setValue(data.path);
+ if (record.data.elementType === 'document') {
+ this.setValue(record.data.path);
return true;
}
}
@@ -278,15 +271,15 @@ opendxp.settings.system = Class.create({
}
},
{
- xtype: "container",
+ xtype: 'container',
width: 450,
- style: "margin-top: 20px;",
- id: "system_settings_errorPage_languageContainer",
+ style: 'margin-top: 20px;',
+ id: 'system_settings_errorPage_languageContainer',
items: [],
listeners: {
beforerender: function () {
// add existing language entries
- var locales = this.getValue("general.valid_languages", true);
+ var locales = this.getValue('general.valid_languages', true);
if (locales && locales.length > 0) {
Ext.each(locales, this.addErrorPage.bind(this));
}
@@ -303,37 +296,38 @@ opendxp.settings.system = Class.create({
autoHeight: true,
labelWidth: 200,
defaultType: 'textfield',
- defaults: {width: 400},
+ defaults: {
+ width: 400
+ },
items: [
{
fieldLabel: t('store_version_history_in_days'),
name: 'documents.versions.days',
- value: this.getValue("documents.versions.days"),
- xtype: "numberfield",
- id: "system_settings_documents_versions_days",
+ value: this.getValue('documents.versions.days'),
+ xtype: 'numberfield',
+ id: 'system_settings_documents_versions_days',
enableKeyEvents: true,
listeners: {
- "change": this.checkVersionInputs.bind(this, "documents", "days"),
- "afterrender": this.checkVersionInputs.bind(this, "documents", "days", "init")
+ change: this.checkVersionInputs.bind(this, 'documents', 'days'),
+ afterrender: this.checkVersionInputs.bind(this, 'documents', 'days', 'init')
},
minValue: 0
},
{
fieldLabel: t('store_version_history_in_steps'),
name: 'documents.versions.steps',
- value: this.getValue("documents.versions.steps"),
- xtype: "numberfield",
- id: "system_settings_documents_versions_steps",
+ value: this.getValue('documents.versions.steps'),
+ xtype: 'numberfield',
+ id: 'system_settings_documents_versions_steps',
enableKeyEvents: true,
listeners: {
- "change": this.checkVersionInputs.bind(this, "documents", "steps"),
- "afterrender": this.checkVersionInputs.bind(this, "documents", "steps", "init")
+ change: this.checkVersionInputs.bind(this, 'documents', 'steps'),
+ afterrender: this.checkVersionInputs.bind(this, 'documents', 'steps', 'init')
},
minValue: 0
}
]
- }
- ,
+ },
{
xtype: 'fieldset',
title: t('data_objects'),
@@ -347,26 +341,26 @@ opendxp.settings.system = Class.create({
{
fieldLabel: t('store_version_history_in_days'),
name: 'objects.versions.days',
- value: this.getValue("objects.versions.days"),
- xtype: "numberfield",
- id: "system_settings_objects_versions_days",
+ value: this.getValue('objects.versions.days'),
+ xtype: 'numberfield',
+ id: 'system_settings_objects_versions_days',
enableKeyEvents: true,
listeners: {
- "change": this.checkVersionInputs.bind(this, "objects", "days"),
- "afterrender": this.checkVersionInputs.bind(this, "objects", "days", "init")
+ change: this.checkVersionInputs.bind(this, 'objects', 'days'),
+ afterrender: this.checkVersionInputs.bind(this, 'objects', 'days', 'init')
},
minValue: 0
},
{
fieldLabel: t('store_version_history_in_steps'),
name: 'objects.versions.steps',
- value: this.getValue("objects.versions.steps"),
- xtype: "numberfield",
- id: "system_settings_objects_versions_steps",
+ value: this.getValue('objects.versions.steps'),
+ xtype: 'numberfield',
+ id: 'system_settings_objects_versions_steps',
enableKeyEvents: true,
listeners: {
- "change": this.checkVersionInputs.bind(this, "objects", "steps"),
- "afterrender": this.checkVersionInputs.bind(this, "objects", "steps", "init")
+ change: this.checkVersionInputs.bind(this, 'objects', 'steps'),
+ afterrender: this.checkVersionInputs.bind(this, 'objects', 'steps', 'init')
},
minValue: 0
}
@@ -385,13 +379,13 @@ opendxp.settings.system = Class.create({
{
fieldLabel: t('store_version_history_in_days'),
name: 'assets.versions.days',
- value: this.getValue("assets.versions.days"),
- xtype: "numberfield",
- id: "system_settings_assets_versions_days",
+ value: this.getValue('assets.versions.days'),
+ xtype: 'numberfield',
+ id: 'system_settings_assets_versions_days',
enableKeyEvents: true,
listeners: {
- "change": this.checkVersionInputs.bind(this, "assets", "days"),
- "afterrender": this.checkVersionInputs.bind(this, "assets", "days", "init")
+ change: this.checkVersionInputs.bind(this, 'assets', 'days'),
+ afterrender: this.checkVersionInputs.bind(this, 'assets', 'days', 'init')
},
width: 400,
minValue: 0
@@ -399,13 +393,13 @@ opendxp.settings.system = Class.create({
{
fieldLabel: t('store_version_history_in_steps'),
name: 'assets.versions.steps',
- value: this.getValue("assets.versions.steps"),
- xtype: "numberfield",
- id: "system_settings_assets_versions_steps",
+ value: this.getValue('assets.versions.steps'),
+ xtype: 'numberfield',
+ id: 'system_settings_assets_versions_steps',
enableKeyEvents: true,
listeners: {
- "change": this.checkVersionInputs.bind(this, "assets", "steps"),
- "afterrender": this.checkVersionInputs.bind(this, "assets", "steps", "init")
+ change: this.checkVersionInputs.bind(this, 'assets', 'steps'),
+ afterrender: this.checkVersionInputs.bind(this, 'assets', 'steps', 'init')
},
width: 400,
minValue: 0
@@ -417,7 +411,7 @@ opendxp.settings.system = Class.create({
this.panel.add(this.layout);
- var tabPanel = Ext.getCmp("opendxp_panel_tabs");
+ tabPanel = Ext.getCmp('opendxp_panel_tabs');
tabPanel.add(this.panel);
tabPanel.setActiveItem(this.panel);
@@ -428,8 +422,8 @@ opendxp.settings.system = Class.create({
},
activate: function () {
- var tabPanel = Ext.getCmp("opendxp_panel_tabs");
- tabPanel.setActiveItem("opendxp_settings_system");
+ var tabPanel = Ext.getCmp('opendxp_panel_tabs');
+ tabPanel.setActiveItem('opendxp_settings_system');
},
save: function () {
@@ -440,7 +434,7 @@ opendxp.settings.system = Class.create({
Ext.Ajax.request({
url: Routing.generate('opendxp_admin_settings_setsystem'),
- method: "PUT",
+ method: 'PUT',
params: {
data: Ext.encode(values)
},
@@ -451,52 +445,50 @@ opendxp.settings.system = Class.create({
try {
var res = Ext.decode(response.responseText);
if (res.success) {
- opendxp.helpers.showNotification(t("success"), t("saved_successfully"), "success");
+ opendxp.helpers.showNotification(t('success'), t('saved_successfully'), 'success');
- Ext.MessageBox.confirm(t("info"), t("reload_opendxp_changes"), function (buttonValue) {
- if (buttonValue == "yes") {
+ Ext.MessageBox.confirm(t('info'), t('reload_opendxp_changes'), function (buttonValue) {
+ if (buttonValue === 'yes') {
window.location.reload();
}
}.bind(this));
} else {
- opendxp.helpers.showNotification(t("error"), t("saving_failed"),
- "error", t(res.message));
+ opendxp.helpers.showNotification(t('error'), t('saving_failed'),
+ 'error', t(res.message));
}
} catch (e) {
- opendxp.helpers.showNotification(t("error"), t("saving_failed"), "error");
+ opendxp.helpers.showNotification(t('error'), t('saving_failed'), 'error');
}
}.bind(this)
});
},
-
emailMethodSelected: function (type, combo) {
- var smtpFieldSet = combo.ownerCt.getComponent(type + "SmtpSettings");
+ var smtpFieldSet = combo.ownerCt.getComponent(type + 'SmtpSettings');
- if (combo.getValue() == "smtp") {
+ if (combo.getValue() === 'smtp') {
smtpFieldSet.show();
} else {
smtpFieldSet.hide();
- Ext.each(smtpFieldSet.query("textfield"), function (item) {
- item.setValue("");
+ Ext.each(smtpFieldSet.query('textfield'), function (item) {
+ item.setValue('');
});
}
opendxp.layout.refresh();
-
},
smtpAuthSelected: function (type, combo) {
- var username = combo.ownerCt.getComponent(type + "_username");
- var pass = combo.ownerCt.getComponent(type + "_password");
+ var username = combo.ownerCt.getComponent(type + '_username');
+ var pass = combo.ownerCt.getComponent(type + '_password');
if (!combo.getValue()) {
username.hide();
pass.hide();
- username.setValue("");
- pass.setValue("");
+ username.setValue('');
+ pass.setValue('');
} else {
username.show();
pass.show();
@@ -505,24 +497,21 @@ opendxp.settings.system = Class.create({
checkVersionInputs: function (elementType, type, field, event) {
- var mappingOpposite = {
- steps: "days",
- days: "steps"
- };
-
- var value = Ext.getCmp("system_settings_" + elementType + "_versions_" + type).getValue();
+ var value = Ext.getCmp('system_settings_' + elementType + '_versions_' + type).getValue(),
+ mappingOpposite = {
+ steps: 'days',
+ days: 'steps'
+ };
- if (event == "init") {
- if (!value) {
- return;
- }
+ if (event === 'init' && !value) {
+ return;
}
if (value !== null) {
- Ext.getCmp("system_settings_" + elementType + "_versions_" + mappingOpposite[type]).disable();
- Ext.getCmp("system_settings_" + elementType + "_versions_" + mappingOpposite[type]).setValue("");
+ Ext.getCmp('system_settings_' + elementType + '_versions_' + mappingOpposite[type]).disable();
+ Ext.getCmp('system_settings_' + elementType + '_versions_' + mappingOpposite[type]).setValue('');
} else {
- Ext.getCmp("system_settings_" + elementType + "_versions_" + mappingOpposite[type]).enable();
+ Ext.getCmp('system_settings_' + elementType + '_versions_' + mappingOpposite[type]).enable();
}
},
@@ -532,70 +521,70 @@ opendxp.settings.system = Class.create({
return;
}
- // find the language entry in the store, because "language" can be the display value too
- var index = this.languagesStore.findExact("language", language);
+ // find the language entry in the store, because 'language' can be the display value too
+ var index = this.languagesStore.findExact('language', language);
if (index < 0) {
- index = this.languagesStore.findExact("display", language)
+ index = this.languagesStore.findExact('display', language)
}
if (index >= 0) {
var rec = this.languagesStore.getAt(index);
- language = rec.get("language");
+ language = rec.get('language');
// add the language to the hidden field used to send the languages to the action
- var languageField = Ext.getCmp("system_settings_general_validLanguages");
- var addedLanguages = languageField.getValue().split(",");
+ var languageField = Ext.getCmp('system_settings_general_validLanguages');
+ var addedLanguages = languageField.getValue().split(',');
if (!in_array(language, addedLanguages)) {
addedLanguages.push(language);
- languageField.setValue(addedLanguages.join(","));
+ languageField.setValue(addedLanguages.join(','));
}
// add the language to the container, so that further settings for the language can be set (eg. fallback, ...)
- var container = Ext.getCmp("system_settings_general_languageContainer");
+ var container = Ext.getCmp('system_settings_general_languageContainer');
var lang = container.getComponent(language);
if (lang) {
return;
}
container.add({
- xtype: "fieldset",
+ xtype: 'fieldset',
itemId: language,
- title: rec.get("display"),
+ title: rec.get('display'),
labelWidth: 250,
width: 590,
- style: "position: relative;",
+ style: 'position: relative;',
items: [{
- xtype: "textfield",
+ xtype: 'textfield',
width: 450,
- fieldLabel: t("fallback_languages"),
- name: "general.fallbackLanguages." + language,
- value: this.getValue("general.fallback_languages." + language)
+ fieldLabel: t('fallback_languages'),
+ name: 'general.fallbackLanguages.' + language,
+ value: this.getValue('general.fallback_languages.' + language)
}, {
- xtype: "radio",
- name: "general.defaultLanguageRadio",
- boxLabel: t("default_language"),
- checked: this.getValue("general.default_language") == language || (!this.getValue("general.default_language") && container.items.length == 0 ),
+ xtype: 'radio',
+ name: 'general.defaultLanguageRadio',
+ boxLabel: t('default_language'),
+ checked: this.getValue('general.default_language') == language || (!this.getValue('general.default_language') && container.items.length === 0),
listeners: {
change: function (el, checked) {
if (checked) {
- var defaultLanguageField = Ext.getCmp("system_settings_general_defaultLanguage");
+ var defaultLanguageField = Ext.getCmp('system_settings_general_defaultLanguage');
defaultLanguageField.setValue(language);
}
}.bind(this)
}
}, {
- xtype: "checkbox",
- name: "general.requiredLanguage",
- boxLabel: t("required_language"),
- checked: this.getValue("general.required_languages", true).includes(language),
+ xtype: 'checkbox',
+ name: 'general.requiredLanguage',
+ boxLabel: t('required_language'),
+ checked: this.getValue('general.required_languages', true).includes(language),
listeners: {
change: function (el, checked) {
- var requiredLanguagesField = Ext.getCmp("system_settings_general_requiredLanguages");
+ var requiredLanguagesField = Ext.getCmp('system_settings_general_requiredLanguages');
var requiredLanguages = [];
- if (requiredLanguagesField.getValue() != '') {
- requiredLanguages = requiredLanguagesField.getValue().split(",");
+ if (requiredLanguagesField.getValue() !== '') {
+ requiredLanguages = requiredLanguagesField.getValue().split(',');
}
if (checked) {
@@ -608,14 +597,14 @@ opendxp.settings.system = Class.create({
}
}
- requiredLanguagesField.setValue(requiredLanguages.join(","));
+ requiredLanguagesField.setValue(requiredLanguages.join(','));
}.bind(this)
}
}, {
- xtype: "button",
- title: t("delete"),
- iconCls: "opendxp_icon_delete",
- style: "position:absolute; right: 5px; top:40px;",
+ xtype: 'button',
+ title: t('delete'),
+ iconCls: 'opendxp_icon_delete',
+ style: 'position:absolute; right: 5px; top:40px;',
handler: this.removeLanguage.bind(this, language)
}]
});
@@ -626,33 +615,34 @@ opendxp.settings.system = Class.create({
removeLanguage: function (language) {
// remove the language out of the hidden field
- var languageField = Ext.getCmp("system_settings_general_validLanguages");
- var addedLanguages = languageField.getValue().split(",");
+ var languageField = Ext.getCmp('system_settings_general_validLanguages');
+ var addedLanguages = languageField.getValue().split(',');
if (in_array(language, addedLanguages)) {
addedLanguages.splice(array_search(language, addedLanguages), 1);
- languageField.setValue(addedLanguages.join(","));
+ languageField.setValue(addedLanguages.join(','));
}
// remove the required language out of the hidden field
- var requiredLanguagesField = Ext.getCmp("system_settings_general_requiredLanguages");
- var addedRequiredLanguages = requiredLanguagesField.getValue().split(",");
+ var requiredLanguagesField = Ext.getCmp('system_settings_general_requiredLanguages');
+ var addedRequiredLanguages = requiredLanguagesField.getValue().split(',');
if (in_array(language, addedRequiredLanguages)) {
addedRequiredLanguages.splice(array_search(language, addedRequiredLanguages), 1);
- requiredLanguagesField.setValue(addedRequiredLanguages.join(","));
+ requiredLanguagesField.setValue(addedRequiredLanguages.join(','));
}
// remove the default language from hidden field
- var defaultLanguageField = Ext.getCmp("system_settings_general_defaultLanguage");
+ var defaultLanguageField = Ext.getCmp('system_settings_general_defaultLanguage');
if (defaultLanguageField.getValue() == language) {
- defaultLanguageField.setValue("");
+ defaultLanguageField.setValue('');
}
// remove the language from the container
- var container = Ext.getCmp("system_settings_general_languageContainer");
+ var container = Ext.getCmp('system_settings_general_languageContainer');
var lang = container.getComponent(language);
if (lang) {
container.remove(lang);
}
+
container.updateLayout();
},
@@ -662,70 +652,69 @@ opendxp.settings.system = Class.create({
return;
}
- // find the language entry in the store, because "language" can be the display value too
- var index = this.languagesStore.findExact("language", language);
+ // find the language entry in the store, because 'language' can be the display value too
+ var index = this.languagesStore.findExact('language', language);
if (index < 0) {
- index = this.languagesStore.findExact("display", language)
+ index = this.languagesStore.findExact('display', language)
}
- if (index >= 0) {
+ if (index < 0) {
+ return;
+ }
- var rec = this.languagesStore.getAt(index);
- language = rec.get("language");
+ var rec = this.languagesStore.getAt(index);
+ language = rec.get('language');
- var container = Ext.getCmp("system_settings_errorPage_languageContainer");
- var lang = container.getComponent(language);
- if (lang) {
- return;
- }
+ var container = Ext.getCmp('system_settings_errorPage_languageContainer');
+ var lang = container.getComponent(language);
+ if (lang) {
+ return;
+ }
- container.add({
- xtype: "fieldset",
- itemId: language,
- title: rec.get("display"),
- labelWidth: 250,
- width: 600,
- style: "position: relative;",
- items: [{
- fieldLabel: t("error_page"),
- name: "documents.error_pages.localized." + language,
- fieldCls: "input_drop_target",
- value: this.getValue("documents.error_pages.localized." + language),
- width: 550,
- xtype: "textfield",
- listeners: {
- "render": function (el) {
- new Ext.dd.DropZone(el.getEl(), {
- reference: this,
- ddGroup: "element",
- getTargetFromEvent: function (e) {
- return this.getEl();
- }.bind(el),
-
- onNodeOver: function (target, dd, e, data) {
- if (data.records.length == 1 && data.records[0].data.elementType == "document") {
- return Ext.dd.DropZone.prototype.dropAllowed;
+ container.add({
+ xtype: 'fieldset',
+ itemId: language,
+ title: rec.get('display'),
+ labelWidth: 250,
+ width: 600,
+ style: 'position: relative;',
+ items: [{
+ fieldLabel: t('error_page'),
+ name: 'documents.error_pages.localized.' + language,
+ fieldCls: 'input_drop_target',
+ value: this.getValue('documents.error_pages.localized.' + language),
+ width: 550,
+ xtype: 'textfield',
+ listeners: {
+ render: function (el) {
+ new Ext.dd.DropZone(el.getEl(), {
+ reference: this,
+ ddGroup: 'element',
+ getTargetFromEvent: function (e) {
+ return this.getEl();
+ }.bind(el),
+ onNodeOver: function (target, dd, e, data) {
+ if (data.records.length === 1 && data.records[0].data.elementType === 'document') {
+ return Ext.dd.DropZone.prototype.dropAllowed;
+ }
+ },
+ onNodeDrop: function (target, dd, e, data) {
+ if (opendxp.helpers.dragAndDropValidateSingleItem(data)) {
+ var record = data.records[0];
+ if (record.data.elementType === 'document') {
+ this.setValue(record.data.path);
+ return true;
}
- },
+ }
+ return false;
+ }.bind(el)
+ });
+ }
+ }
+ }]
+ });
- onNodeDrop: function (target, dd, e, data) {
- if (opendxp.helpers.dragAndDropValidateSingleItem(data)) {
- var record = data.records[0];
- var data = record.data;
+ container.updateLayout();
- if (data.elementType == "document") {
- this.setValue(data.path);
- return true;
- }
- }
- return false;
- }.bind(el)
- });
- }
- }
- }]
- });
- container.updateLayout();
- }
}
});
diff --git a/src/Controller/Admin/Document/DocumentController.php b/src/Controller/Admin/Document/DocumentController.php
index 49930038..69befdde 100644
--- a/src/Controller/Admin/Document/DocumentController.php
+++ b/src/Controller/Admin/Document/DocumentController.php
@@ -23,6 +23,7 @@
use OpenDxp\Bundle\AdminBundle\Controller\Traits\UserNameTrait;
use OpenDxp\Bundle\AdminBundle\Event\AdminEvents;
use OpenDxp\Bundle\AdminBundle\Event\ElementAdminStyleEvent;
+use OpenDxp\Bundle\AdminBundle\Event\SiteCustomSettingsEvent;
use OpenDxp\Cache\RuntimeCache;
use OpenDxp\Config;
use OpenDxp\Controller\KernelControllerEventInterface;
@@ -725,8 +726,23 @@ public function publishVersionAction(Request $request): JsonResponse
return $this->adminJson(['success' => true, 'treeData' => $treeData]);
}
+ #[Route('/get-site-custom-settings', name: 'opendxp_admin_document_document_get_site_custom_settings', methods: ['POST'])]
+ public function getSiteCustomSettingsAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse
+ {
+ $site = Site::getById($request->request->getInt('id'));
+
+ $event = new SiteCustomSettingsEvent($site);
+ $eventDispatcher->dispatch($event, AdminEvents::SITE_CUSTOM_SETTINGS);
+
+ $customSettings = $event->getConfigNodes();
+
+ return $this->adminJson([
+ 'data' => $customSettings
+ ]);
+ }
+
#[Route('/update-site', name: 'opendxp_admin_document_document_updatesite', methods: ['PUT'])]
- public function updateSiteAction(Request $request): JsonResponse
+ public function updateSiteAction(Request $request, EventDispatcherInterface $eventDispatcher): JsonResponse
{
$domains = $request->request->getString('domains');
$domains = str_replace(' ', '', $domains);
@@ -743,18 +759,37 @@ public function updateSiteAction(Request $request): JsonResponse
foreach ($validLanguages as $language) {
// localized error pages
- $requestValue = $request->request->get('errorDocument_localized_' . $language);
+ $requestValue = $request->request->get(sprintf('errorDocument_localized_%s', $language));
if (isset($requestValue)) {
$localizedErrorDocuments[$language] = $requestValue;
}
}
+ $event = new SiteCustomSettingsEvent($site);
+ $eventDispatcher->dispatch($event, AdminEvents::SITE_CUSTOM_SETTINGS);
+
+ $customSettings = [];
+ foreach ($event->getConfigNodes() as $scope => $nodes) {
+ foreach ($nodes as $node) {
+ $requestValueName = sprintf('customSettings_%s_%s', $scope, $node['name']);
+ if ($request->request->has($requestValueName)) {
+ $value = $request->request->get($requestValueName);
+ if ($node['type'] === OpenDxp\Bundle\AdminBundle\Enum\SiteCustomConfigNodeType::CHECKBOX->value) {
+ $value = $value === 'true';
+ }
+
+ $customSettings[$scope][$node['name']] = $value;
+ }
+ }
+ }
+
$site->setDomains($domains);
$site->setMainDomain($request->request->getString('mainDomain'));
$site->setErrorDocument($request->request->getString('errorDocument'));
$site->setLocalizedErrorDocuments($localizedErrorDocuments);
$site->setRedirectToMainDomain($request->request->getBoolean('redirectToMainDomain'));
+ $site->setCustomSettings(count($customSettings) === 0 ? null : $customSettings);
$site->save();
$site->setRootDocument(null); // do not send the document to the frontend
diff --git a/src/Dto/SiteCustomSettings/CheckboxNodeConfig.php b/src/Dto/SiteCustomSettings/CheckboxNodeConfig.php
new file mode 100644
index 00000000..965c7dbe
--- /dev/null
+++ b/src/Dto/SiteCustomSettings/CheckboxNodeConfig.php
@@ -0,0 +1,41 @@
+ $this->checkedValue,
+ 'uncheckedValue' => $this->uncheckedValue,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/src/Dto/SiteCustomSettings/DropdownNodeConfig.php b/src/Dto/SiteCustomSettings/DropdownNodeConfig.php
new file mode 100644
index 00000000..9661f1e9
--- /dev/null
+++ b/src/Dto/SiteCustomSettings/DropdownNodeConfig.php
@@ -0,0 +1,45 @@
+ $this->store,
+ 'required' => $this->required,
+ 'displayField' => $this->displayField,
+ 'valueField' => $this->valueField,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/src/Dto/SiteCustomSettings/InputNodeConfig.php b/src/Dto/SiteCustomSettings/InputNodeConfig.php
new file mode 100644
index 00000000..5ded1247
--- /dev/null
+++ b/src/Dto/SiteCustomSettings/InputNodeConfig.php
@@ -0,0 +1,39 @@
+ $this->required,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/src/Dto/SiteCustomSettings/NodeConfigInterface.php b/src/Dto/SiteCustomSettings/NodeConfigInterface.php
new file mode 100644
index 00000000..b8f4f303
--- /dev/null
+++ b/src/Dto/SiteCustomSettings/NodeConfigInterface.php
@@ -0,0 +1,26 @@
+ $this->required,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/src/Enum/SiteCustomConfigNodeType.php b/src/Enum/SiteCustomConfigNodeType.php
new file mode 100644
index 00000000..db371dba
--- /dev/null
+++ b/src/Enum/SiteCustomConfigNodeType.php
@@ -0,0 +1,24 @@
+configNodes)) {
+ $this->configNodes[$scope] = [];
+ }
+
+ $this->configNodes[$scope][] = [
+ 'type' => $config->getType()->value,
+ 'name' => $name,
+ 'label' => $label,
+ 'config' => $config->toArray(),
+ ];
+ }
+
+ public function getConfigNodes(): array
+ {
+ return $this->configNodes;
+ }
+
+ public function getSite(): Site
+ {
+ return $this->site;
+ }
+}
\ No newline at end of file
diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md
new file mode 100644
index 00000000..991183ea
--- /dev/null
+++ b/tests/CLAUDE.md
@@ -0,0 +1,267 @@
+# Tests — Claude Instructions
+
+## Framework & Setup
+
+Tests use **Codeception**. Configuration: `codeception.dist.yml` in the bundle root.
+
+The bootstrap (`tests/_bootstrap.php`) resolves the OpenDXP environment in this order:
+1. `vendor/autoload.php` in the bundle itself (standalone)
+2. `../../../../vendor/autoload.php` (installed as part of a project)
+3. `$OPENDXP_PROJECT_ROOT/vendor/autoload.php` (via env variable)
+
+Integration tests require a fully running OpenDXP environment with a database.
+
+## Directory Structure
+
+```
+tests/
+├── Model/ # Integration tests (need DB + running OpenDXP)
+│ ├── GridHelper/
+│ │ └── GridHelperTest.php
+│ └── Permissions/
+│ ├── AbstractPermissionTest.php
+│ ├── ModelAssetPermissionsTest.php
+│ ├── ModelDataObjectPermissionsTest.php
+│ └── ModelDocumentPermissionsTest.php
+├── Unit/ # Unit tests (no DB, no OpenDXP bootstrap)
+│ └── Event/ # ← place event/enum unit tests here
+└── Support/
+ ├── Helper/
+ │ └── Model.php
+ └── UnitTester.php
+```
+
+`tests/Unit/` does not exist yet — create it when adding the first unit test.
+
+## Base Classes
+
+| Situation | Base Class | Location |
+|---|---|---|
+| Test needs DB, models, real OpenDXP objects | `ModelTestCase` | `OpenDxp\Tests\Support\Test\ModelTestCase` (from `../opendxp`) |
+| Pure logic, no I/O, no DB | `TestCase` | `PHPUnit\Framework\TestCase` |
+
+**Rule of thumb:**
+- New event class or enum → `PHPUnit\Framework\TestCase`
+- Controller behaviour, permissions, grid queries → `ModelTestCase`
+
+Never use `ModelTestCase` for things that don't need the DB — it requires a full OpenDXP bootstrap and slows everything down.
+
+## Unit Test — Event Classes
+
+Event classes are pure data containers and easy to test without any infrastructure.
+
+Example: `ElementAdminStyleEvent` carries the element, its admin style, and an optional
+context (tree, editor, search). A unit test verifies the getters, setters, and context constants:
+
+```php
+createMock(ElementInterface::class);
+ $adminStyle = new AdminStyle($element);
+
+ $event = new ElementAdminStyleEvent($element, $adminStyle, ElementAdminStyleEvent::CONTEXT_TREE);
+
+ self::assertSame($element, $event->getElement());
+ self::assertSame($adminStyle, $event->getAdminStyle());
+ self::assertSame(ElementAdminStyleEvent::CONTEXT_TREE, $event->getContext());
+ }
+
+ public function testContextIsNullByDefault(): void
+ {
+ $element = $this->createMock(ElementInterface::class);
+
+ $event = new ElementAdminStyleEvent($element, new AdminStyle($element));
+
+ self::assertNull($event->getContext());
+ }
+
+ public function testSetContextUpdatesValue(): void
+ {
+ $element = $this->createMock(ElementInterface::class);
+ $event = new ElementAdminStyleEvent($element, new AdminStyle($element));
+
+ $event->setContext(ElementAdminStyleEvent::CONTEXT_EDITOR);
+
+ self::assertSame(ElementAdminStyleEvent::CONTEXT_EDITOR, $event->getContext());
+ }
+
+ public function testContextConstantsAreDistinct(): void
+ {
+ self::assertNotSame(ElementAdminStyleEvent::CONTEXT_TREE, ElementAdminStyleEvent::CONTEXT_EDITOR);
+ self::assertNotSame(ElementAdminStyleEvent::CONTEXT_EDITOR, ElementAdminStyleEvent::CONTEXT_SEARCH);
+ self::assertNotSame(ElementAdminStyleEvent::CONTEXT_TREE, ElementAdminStyleEvent::CONTEXT_SEARCH);
+ }
+}
+```
+
+Note: `ElementInterface` is an interface and can be mocked normally. For final model classes
+(like `Site`), use `new ClassName()` directly instead of `createMock()` — see `UnitTestCase::createSite()`
+as an example of how to encapsulate that in the base class.
+
+## Integration Test — Permissions / Model
+
+Integration tests extend `AbstractPermissionTest` which itself extends `ModelTestCase`.
+They use `Codeception\Stub` to wire controllers without a full HTTP stack.
+
+See `tests/Model/Permissions/ModelDocumentPermissionsTest.php` for a working example.
+
+Key pattern:
+```php
+// build a stubbed controller with a mocked user
+$controller = $this->buildController(DocumentController::class, $user);
+
+// call the action directly
+$response = $controller->treeGetChildrenByIdAction($request);
+
+// assert on the JSON response
+$data = json_decode($response->getContent(), true);
+self::assertTrue($data['success']);
+```
+
+## Naming Conventions
+
+- Unit tests: `tests/Unit/{Namespace}/{ClassName}Test.php`
+- Integration tests: `tests/Model/{Feature}/{ClassName}Test.php`
+- Test methods: `test` prefix + descriptive camelCase (`testAddConfigNodeGroupsByScope`)
+- One `Test.php` per source class being tested
+
+***
+
+## Running Tests
+
+### Prerequisites
+First time starts: Is the MCP tool `opendxp-testkit` available in this session?
+
+**No → Run setup now:**
+
+Ask the developer this question and wait for the answer:
+> "Where is your local `docker-testkit` directory? (absolute path)"
+
+Once the path is provided:
+
+- Write `.mcp.json` in the bundle root:
+
+```json
+{
+ "mcpServers": {
+ "opendxp-testkit": {
+ "type": "stdio",
+ "command": "node",
+ "args": [
+ "/mcp-server/index.js"
+ ]
+ }
+ }
+}
+```
+
+- Add `.mcp.json` to `.gitignore` if not already present
+- Tell the developer: "Please restart Claude Code — `opendxp-testkit` will be available after restart."
+- Stop. Wait for restart.
+
+**Yes → Normal workflow:**
+
+---
+
+### Workflow: Running tests
+
+#### Step 1 — Check status
+
+Call `get_status()`. The result shows:
+
+- `Configured bundle` — which bundle is currently set in the testkit
+- `ddev running` — whether ddev is running
+
+Decide based on the result:
+
+| Situation | Action |
+|--------------------------------------|------------------------------------------------------------------------------------------|
+| `ddev running: false` | Call `set_bundle("BUNDLE_DIR_NAME")` → ddev will be started → use `with_composer=true` |
+| `ddev running: true`, wrong bundle | Call `set_bundle("BUNDLE_DIR_NAME")` → ddev will be restarted → use `with_composer=true` |
+| `ddev running: true`, correct bundle | Proceed to step 2 — no `with_composer` needed |
+
+`BUNDLE_DIR_NAME` = `basename` of this directory (e.g. `ecommerce-bundle`)
+
+> **Note on rsync:** Files are **always** synced into the container via rsync — no ddev restart is needed after code changes.
+
+#### Step 2 — Run tests
+
+```
+run_codeception(test_path="tests/...", with_composer=true/false)
+```
+
+- Only set `test_path` when the user specifies a particular test, otherwise omit it (runs all tests)
+- Do **not** set `debug` by default (see rules below)
+
+**`test_path` formats:**
+| What the user wants | `test_path` value |
+|---------------------|-------------------|
+| All tests | *(omit)* |
+| A specific test class | `tests/Model//GridHelper/GridHelperTest.php` |
+| A specific test folder | `tests/Unit/Event` |
+| All unit tests | `tests/Unit` |
+| All model/integration tests | `tests/Model` |
+
+#### Step 3 — On test failure
+
+If tests fail, ask the user:
+> "Tests failed. Should I re-run with `--debug` for detailed output?"
+
+Only if the user agrees: `run_codeception(debug=true, ...)`
+
+---
+
+### Workflow: PHPStan
+
+`run_phpstan()` is a standalone task — only run it when the user explicitly asks for it.
+
+```
+run_phpstan(level=6) ← default level, adjust on request
+```
+
+---
+
+### Rules
+
+- Write code and tests in this directory only — never touch `app/` inside the testkit
+- `with_composer=true` after `set_bundle` calls or if something changed in `composer.json`
+- `debug=true` only with explicit user consent after a failed test run
+- PHPStan only on explicit request
+
+---
+
+### Fallback: Without MCP
+
+Only use these commands if `opendxp-testkit` is not available in this session:
+
+```bash
+# all suites
+vendor/bin/codecept run
+
+# unit tests only (no DB needed)
+vendor/bin/codecept run Unit
+
+# integration tests only (DB required)
+vendor/bin/codecept run Model
+
+# single file
+vendor/bin/codecept run Unit tests/Unit/Event/SiteCustomSettingsEventTest.php
+```
+
+For integration tests, set `OPENDXP_PROJECT_ROOT` if running outside a full project:
+```bash
+OPENDXP_PROJECT_ROOT=/path/to/project vendor/bin/codecept run Model
+```
\ No newline at end of file
diff --git a/tests/Readme.md b/tests/Readme.md
deleted file mode 100644
index 50af476e..00000000
--- a/tests/Readme.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Running and creating tests
-
-Tests can be executed locally in a separate docker-compose. This docker-compose also sets up database
-accordingly.
-
-To run, just execute [/bin/init-tests.sh](./bin/init-tests.sh) script and follow instructions there.
-
-Additional tests may be added following codeception best practises.
diff --git a/tests/Support/Helper/Unit.php b/tests/Support/Helper/Unit.php
new file mode 100644
index 00000000..446133f3
--- /dev/null
+++ b/tests/Support/Helper/Unit.php
@@ -0,0 +1,21 @@
+createSite());
+
+ self::assertSame([], $event->getConfigNodes());
+ }
+
+ public function testGetSiteReturnsSite(): void
+ {
+ $site = $this->createSite();
+ $event = new SiteCustomSettingsEvent($site);
+
+ self::assertSame($site, $event->getSite());
+ }
+
+ public function testAddConfigNodeGroupsByScope(): void
+ {
+ $event = new SiteCustomSettingsEvent($this->createSite());
+
+ $event->addConfigNode(new InputNodeConfig(), 'seo', 'title', 'SEO Title');
+ $event->addConfigNode(new CheckboxNodeConfig(), 'seo', 'noindex', 'No Index');
+ $event->addConfigNode(new DropdownNodeConfig(), 'i18n', 'zone', 'Zone');
+ $event->addConfigNode(new TextNodeConfig(), 'app', 'description', 'Description');
+
+ $nodes = $event->getConfigNodes();
+
+ self::assertCount(2, $nodes['seo']);
+ self::assertCount(1, $nodes['i18n']);
+ self::assertCount(1, $nodes['app']);
+ }
+
+ public function testAddConfigNodeBuildsCorrectStructure(): void
+ {
+ $event = new SiteCustomSettingsEvent($this->createSite());
+
+ $event->addConfigNode(
+ new DropdownNodeConfig(
+ store: [['label' => 'A', 'value' => 'a']],
+ required: true,
+ ),
+ 'app',
+ 'my_field',
+ 'My Field',
+ );
+
+ $node = $event->getConfigNodes()['app'][0];
+
+ self::assertSame(SiteCustomConfigNodeType::DROPDOWN->value, $node['type']);
+ self::assertSame('my_field', $node['name']);
+ self::assertSame('My Field', $node['label']);
+ self::assertTrue($node['config']['required']);
+ self::assertSame([['label' => 'A', 'value' => 'a']], $node['config']['store']);
+ }
+
+ public function testMultipleNodesInSameScopeAreAppended(): void
+ {
+ $event = new SiteCustomSettingsEvent($this->createSite());
+
+ $event->addConfigNode(new InputNodeConfig(), 'app', 'first', 'First');
+ $event->addConfigNode(new InputNodeConfig(), 'app', 'second', 'Second');
+ $event->addConfigNode(new InputNodeConfig(), 'app', 'third', 'Third');
+
+ $nodes = $event->getConfigNodes()['app'];
+
+ self::assertCount(3, $nodes);
+ self::assertSame('first', $nodes[0]['name']);
+ self::assertSame('second', $nodes[1]['name']);
+ self::assertSame('third', $nodes[2]['name']);
+ }
+
+ public function testEachDtoReturnsCorrectType(): void
+ {
+ self::assertSame(SiteCustomConfigNodeType::INPUT, (new InputNodeConfig())->getType());
+ self::assertSame(SiteCustomConfigNodeType::TEXT, (new TextNodeConfig())->getType());
+ self::assertSame(SiteCustomConfigNodeType::CHECKBOX, (new CheckboxNodeConfig())->getType());
+ self::assertSame(SiteCustomConfigNodeType::DROPDOWN, (new DropdownNodeConfig())->getType());
+ }
+}
\ No newline at end of file
diff --git a/tests/bin/docker-compose.yml b/tests/bin/docker-compose.yml
index 7e8028ac..5f598ac7 100644
--- a/tests/bin/docker-compose.yml
+++ b/tests/bin/docker-compose.yml
@@ -1,3 +1,7 @@
+#
+# ATTENTION!
+# This file is deprecated and will be removed with admin-bundle 2.0
+#
version: '3.0'
services:
db:
diff --git a/tests/bin/init-tests.sh b/tests/bin/init-tests.sh
index 0edc2fff..742b8bba 100755
--- a/tests/bin/init-tests.sh
+++ b/tests/bin/init-tests.sh
@@ -1,5 +1,10 @@
#!/bin/bash
+#
+# ATTENTION!
+# This file is deprecated and will be removed with admin-bundle 2.0
+#
+
docker-compose down -v --remove-orphans
docker-compose up -d
diff --git a/translations/admin_ext.ca.yaml b/translations/admin_ext.ca.yaml
index d20e5c93..4f88ce8e 100644
--- a/translations/admin_ext.ca.yaml
+++ b/translations/admin_ext.ca.yaml
@@ -531,6 +531,7 @@ main_domain: Main Domain
error_page: Error Page
additional_domains: Additional Domains (one domain per line)
redirect_to_main_domain: Redirect additional domains to main domain
+site_custom_settings: Additional Settings
debug_mode_on: DEBUG MODE
duration: Duration
scope: Scope
diff --git a/translations/admin_ext.cs.yaml b/translations/admin_ext.cs.yaml
index 1bdf8398..a9f3a88c 100644
--- a/translations/admin_ext.cs.yaml
+++ b/translations/admin_ext.cs.yaml
@@ -551,6 +551,7 @@ error_page: "Chybov\xE1 str\xE1nka"
additional_domains: "Dal\u0161\xED dom\xE9ny (jedna na \u0159\xE1dek)"
redirect_to_main_domain: "P\u0159esm\u011Brovat dal\u0161\xED dom\xE9ny na hlavn\xED
dom\xE9nu"
+site_custom_settings: Additional Settings
debug_mode_on: "DEBUG m\xF3d"
duration: "Trv\xE1n\xED"
scope: Kontext (Scope)
diff --git a/translations/admin_ext.de.yaml b/translations/admin_ext.de.yaml
index a024f5d7..e9aa333d 100644
--- a/translations/admin_ext.de.yaml
+++ b/translations/admin_ext.de.yaml
@@ -555,6 +555,7 @@ main_domain: Hauptdomain
error_page: Fehlerseite
additional_domains: "Zus\xE4tzliche Domains (eine Domain pro Zeile)"
redirect_to_main_domain: "Leite zus\xE4tzliche Domains auf die Haupt-Domain um."
+site_custom_settings: "Zusätzliche Einstellungen"
debug_mode_on: DEBUGMODUS
duration: Dauer
scope: Umfang
diff --git a/translations/admin_ext.en.yaml b/translations/admin_ext.en.yaml
index 10ec4ab0..614e232d 100644
--- a/translations/admin_ext.en.yaml
+++ b/translations/admin_ext.en.yaml
@@ -564,6 +564,7 @@ main_domain: Main Domain
error_page: Error Page
additional_domains: Additional Domains (one domain per line)
redirect_to_main_domain: Redirect additional domains to main domain
+site_custom_settings: Additional Settings
debug_mode_on: DEBUG MODE
duration: Duration
scope: Scope
diff --git a/translations/admin_ext.es.yaml b/translations/admin_ext.es.yaml
index 6e6fc35c..53c79a03 100644
--- a/translations/admin_ext.es.yaml
+++ b/translations/admin_ext.es.yaml
@@ -528,6 +528,7 @@ main_domain: Dominio principal
error_page: "P\xE1gina de error"
additional_domains: "Dominios adicionales (un dominio por l\xEDnea)"
redirect_to_main_domain: Redirije dominios adicionales hacia el dominio principal
+site_custom_settings: Additional Settings
debug_mode_on: MODO DE DEPURACION
duration: "Duraci\xF3n"
scope: alcance
diff --git a/translations/admin_ext.fr.yaml b/translations/admin_ext.fr.yaml
index 7974f53c..7528a45b 100644
--- a/translations/admin_ext.fr.yaml
+++ b/translations/admin_ext.fr.yaml
@@ -560,6 +560,7 @@ error_page: Page d'erreur
additional_domains: "Domaines suppl\xE9mentaires (un domaine par ligne)"
redirect_to_main_domain: "Rediriger les domains suppl\xE9mentaires vers le domaine
principal"
+site_custom_settings: Additional Settings
debug_mode_on: MODE DEBUG
duration: "Dur\xE9e"
scope: "Port\xE9e"
diff --git a/translations/admin_ext.hu.yaml b/translations/admin_ext.hu.yaml
index adecfdd7..35ac45b7 100644
--- a/translations/admin_ext.hu.yaml
+++ b/translations/admin_ext.hu.yaml
@@ -557,6 +557,7 @@ error_page: Hiba oldal
additional_domains: "Tov\xE1bbi domainek (soronk\xE9nt egy)"
redirect_to_main_domain: "A tov\xE1bbi domainek \xE1tir\xE1ny\xEDt\xE1sa a f\u0151
domainre"
+site_custom_settings: Additional Settings
debug_mode_on: "DEBUG M\xD3D"
duration: "Hossz (id\u0151)"
scope: Scope
diff --git a/translations/admin_ext.it.yaml b/translations/admin_ext.it.yaml
index 8eff9d88..aa9b1466 100644
--- a/translations/admin_ext.it.yaml
+++ b/translations/admin_ext.it.yaml
@@ -541,6 +541,7 @@ main_domain: Dominio principale
error_page: Pagina di errore
additional_domains: Domini aggiuntivi (un dominio per linea)
redirect_to_main_domain: Redireziona domini aggiuntivi verso il dominio principale
+site_custom_settings: Additional Settings
debug_mode_on: "MODALIT\xC0 DEBUG"
duration: Durata
scope: Scopo
diff --git a/translations/admin_ext.nl.yaml b/translations/admin_ext.nl.yaml
index 9ec7b5e0..addfb42f 100644
--- a/translations/admin_ext.nl.yaml
+++ b/translations/admin_ext.nl.yaml
@@ -536,6 +536,7 @@ main_domain: Hoofddomein
error_page: Error-pagina
additional_domains: "Extra domeinen (\xE9\xE9n domein per regel)"
redirect_to_main_domain: Verwijs extra domeinen naar het hoofddomein
+site_custom_settings: Additional Settings
debug_mode_on: DEBUG-MODUS
duration: Tijdsduur
scope: Bereik
diff --git a/translations/admin_ext.pl.yaml b/translations/admin_ext.pl.yaml
index a083e8ed..e1c47ec1 100644
--- a/translations/admin_ext.pl.yaml
+++ b/translations/admin_ext.pl.yaml
@@ -546,6 +546,7 @@ main_domain: "Domena g\u0142\xF3wna"
error_page: "Strona b\u0142\u0119du"
additional_domains: "Dodatkowe domeny (jedna na lini\u0119)"
redirect_to_main_domain: "Przekieruj dodatkowe domeny na domen\u0119 g\u0142\xF3wn\u0105"
+site_custom_settings: Additional Settings
debug_mode_on: TRYB DEBUGOWANIA
duration: "Czas dzia\u0142ania"
scope: Obszar
diff --git a/translations/admin_ext.pt_br.yaml b/translations/admin_ext.pt_br.yaml
index 348e3492..2e35ac3b 100644
--- a/translations/admin_ext.pt_br.yaml
+++ b/translations/admin_ext.pt_br.yaml
@@ -533,6 +533,7 @@ main_domain: Main Domain
error_page: Error Page
additional_domains: Additional Domains (one domain per line)
redirect_to_main_domain: Redirect additional domains to main domain
+site_custom_settings: Additional Settings
debug_mode_on: DEBUG MODE
duration: Duration
scope: Scope
diff --git a/translations/admin_ext.ro.yaml b/translations/admin_ext.ro.yaml
index d20e5c93..4f88ce8e 100644
--- a/translations/admin_ext.ro.yaml
+++ b/translations/admin_ext.ro.yaml
@@ -531,6 +531,7 @@ main_domain: Main Domain
error_page: Error Page
additional_domains: Additional Domains (one domain per line)
redirect_to_main_domain: Redirect additional domains to main domain
+site_custom_settings: Additional Settings
debug_mode_on: DEBUG MODE
duration: Duration
scope: Scope
diff --git a/translations/admin_ext.sk.yaml b/translations/admin_ext.sk.yaml
index 696d6180..aa83298e 100644
--- a/translations/admin_ext.sk.yaml
+++ b/translations/admin_ext.sk.yaml
@@ -550,6 +550,7 @@ main_domain: "Hlavn\xE1 dom\xE9na"
error_page: "Chybov\xE1 str\xE1nka"
additional_domains: "\u010Eal\u0161ie dom\xE9ny (jedna dom\xE9na na riadok)"
redirect_to_main_domain: "\u010Eal\u0161ie dom\xE9ny presmerova\u0165 na hlavn\xFA
+site_custom_settings: Additional Settings
dom\xE9nu"
debug_mode_on: "DEBUG m\xF3d"
duration: Trvanie
diff --git a/translations/admin_ext.sv.yaml b/translations/admin_ext.sv.yaml
index de4eb1a2..18925bc8 100644
--- a/translations/admin_ext.sv.yaml
+++ b/translations/admin_ext.sv.yaml
@@ -531,6 +531,7 @@ main_domain: "Huvuddom\xE4n"
error_page: Error Page
additional_domains: Additional Domains (one domain per line)
redirect_to_main_domain: Redirect additional domains to main domain
+site_custom_settings: Additional Settings
debug_mode_on: DEBUG MODE
duration: "L\xE4ngd"
scope: Scope
diff --git a/translations/admin_ext.th.yaml b/translations/admin_ext.th.yaml
index 000010ca..7d5eb170 100644
--- a/translations/admin_ext.th.yaml
+++ b/translations/admin_ext.th.yaml
@@ -579,6 +579,7 @@ error_page: "\u0E2B\u0E19\u0E49\u0E32\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\
additional_domains: "\u0E42\u0E14\u0E40\u0E21\u0E19\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E40\u0E15\u0E34\u0E21
(\u0E2B\u0E19\u0E36\u0E48\u0E07\u0E42\u0E14\u0E40\u0E21\u0E19\u0E15\u0E48\u0E2D\u0E1A\u0E23\u0E23\u0E17\u0E31\u0E14)"
redirect_to_main_domain: "\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E40\u0E2A\u0E49\u0E19\u0E17\u0E32\u0E07\u0E42\u0E14\u0E40\u0E21\u0E19\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E40\u0E15\u0E34\u0E21\u0E44\u0E1B\u0E22\u0E31\u0E07\u0E42\u0E14\u0E40\u0E21\u0E19\u0E2B\u0E25\u0E31\u0E01"
+site_custom_settings: Additional Settings
debug_mode_on: "\u0E42\u0E2B\u0E21\u0E14\u0E14\u0E35\u0E1A\u0E31\u0E01"
duration: "\u0E23\u0E30\u0E22\u0E30\u0E40\u0E27\u0E25\u0E32"
scope: "\u0E02\u0E2D\u0E1A\u0E40\u0E02\u0E15"
diff --git a/translations/admin_ext.zh_Hans.yaml b/translations/admin_ext.zh_Hans.yaml
index fb74f944..7943d811 100644
--- a/translations/admin_ext.zh_Hans.yaml
+++ b/translations/admin_ext.zh_Hans.yaml
@@ -531,6 +531,7 @@ main_domain: Main Domain
error_page: Error Page
additional_domains: Additional Domains (one domain per line)
redirect_to_main_domain: Redirect additional domains to main domain
+site_custom_settings: Additional Settings
debug_mode_on: DEBUG MODE
duration: Duration
scope: Scope