diff --git a/Build/Sources/Sass/component/_tree.scss b/Build/Sources/Sass/component/_tree.scss index fc53f018c6e619af096084d9dae603ba0ae16629..0a3093a5c45374add2bf303f892582210fa47ffd 100644 --- a/Build/Sources/Sass/component/_tree.scss +++ b/Build/Sources/Sass/component/_tree.scss @@ -312,6 +312,13 @@ background-color: var(--typo3-component-match-highlight-bg); } +.node-information { + display: flex; + gap: .15rem; + padding-inline-start: .25rem; + opacity: .75; +} + .node-action { display: none; cursor: pointer; diff --git a/Build/Sources/TypeScript/backend/tree/page-tree-element.ts b/Build/Sources/TypeScript/backend/tree/page-tree-element.ts index 82ff0db909e298f12105634dfcfe52750cb0fa7b..3639a14832c12670ee5c910a9601b70ca585b88e 100644 --- a/Build/Sources/TypeScript/backend/tree/page-tree-element.ts +++ b/Build/Sources/TypeScript/backend/tree/page-tree-element.ts @@ -573,6 +573,7 @@ class PageTreeToolbar extends TreeToolbar { tooltip: '', type: 'PageTreeItem', doktype: item.nodeType, + statusInformation: [], }; this.tree.draggingNode = newNode; this.tree.nodeDragMode = TreeNodeCommandEnum.NEW; diff --git a/Build/Sources/TypeScript/backend/tree/tree-node.ts b/Build/Sources/TypeScript/backend/tree/tree-node.ts index dfd7825e085687de9dd4ab99f43b1f5a7bb86b8a..7c3a0a1a4576b9c272e1e2fe83189efabce8230d 100644 --- a/Build/Sources/TypeScript/backend/tree/tree-node.ts +++ b/Build/Sources/TypeScript/backend/tree/tree-node.ts @@ -25,6 +25,15 @@ export enum TreeNodePositionEnum { AFTER = 'after' } + +export interface TreeNodeStatusInformation { + label: string, + icon: string, + severity: number, + overlayIcon: string, + priority: number +} + /** * Represents a single node in the tree that is rendered. */ @@ -48,6 +57,7 @@ export interface TreeNodeInterface { deletable: boolean, icon: string, overlayIcon: string, + statusInformation: Array<TreeNodeStatusInformation>, // Calculated Internal __treeIdentifier: string, diff --git a/Build/Sources/TypeScript/backend/tree/tree.ts b/Build/Sources/TypeScript/backend/tree/tree.ts index 0b89451d0b24e32fce8167b41970414f3c47be89..a3d90ce96c64bffe7344b27e6e1d39284081f230 100644 --- a/Build/Sources/TypeScript/backend/tree/tree.ts +++ b/Build/Sources/TypeScript/backend/tree/tree.ts @@ -15,7 +15,8 @@ import { html, LitElement, TemplateResult, nothing } from 'lit'; import { property, state, query } from 'lit/decorators'; import { repeat } from 'lit/directives/repeat'; import { styleMap } from 'lit/directives/style-map'; -import { TreeNodeInterface, TreeNodeCommandEnum, TreeNodePositionEnum } from './tree-node'; +import { ifDefined } from 'lit/directives/if-defined'; +import { TreeNodeInterface, TreeNodeCommandEnum, TreeNodePositionEnum, TreeNodeStatusInformation } from './tree-node'; import AjaxRequest from '@typo3/core/ajax/ajax-request'; import Notification from '../notification'; import { KeyTypesEnum as KeyTypes } from '../enum/key-types'; @@ -24,6 +25,7 @@ import '@typo3/backend/element/icon-element'; import ClientStorage from '@typo3/backend/storage/client'; import { DataTransferTypes } from '@typo3/backend/enum/data-transfer-types'; import type { DragTooltipMetadata } from '@typo3/backend/drag-tooltip'; +import Severity from '@typo3/backend/severity'; interface TreeNodeStatus { expanded: boolean @@ -610,6 +612,7 @@ export class Tree extends LitElement { ${this.createNodeGuides(node)} ${this.createNodeLoader(node) || this.createNodeToggle(node) || nothing} ${this.createNodeContent(node)} + ${this.createNodeStatusInformation(node)} ${this.createNodeDeleteDropZone(node)} </div> `)} @@ -1011,6 +1014,30 @@ export class Tree extends LitElement { </div>`; } + protected createNodeStatusInformation(node: TreeNodeInterface): TemplateResult + { + const statusInformation = this.getNodeStatusInformation(node); + if (statusInformation.length === 0) { + return html`${nothing}`; + } + + const firstInformation = statusInformation[0]; + const severityClass = Severity.getCssClass(firstInformation.severity); + const iconIdentifier = firstInformation.icon !== '' ? firstInformation.icon : 'actions-dot'; + const overlayIconIdentifier = firstInformation.overlayIcon !== '' ? firstInformation.overlayIcon : undefined; + + return html` + <span class="node-information"> + <typo3-backend-icon + class="text-${severityClass}" + identifier=${iconIdentifier} + overlay=${ifDefined(overlayIconIdentifier)} + size="small" + ></typo3-backend-icon> + </span> + `; + } + protected createNodeDeleteDropZone(node: TreeNodeInterface): TemplateResult { return this.draggingNode === node && node.deletable @@ -1142,6 +1169,21 @@ export class Tree extends LitElement { return classList; } + protected getNodeStatusInformation(node: TreeNodeInterface): TreeNodeStatusInformation[] { + if (node.statusInformation.length === 0) { + return []; + } + + const statusInformation = node.statusInformation.sort((a, b) => { + if (a.severity !== b.severity) { + return b.severity - a.severity; + } + return b.priority - a.priority; + }); + + return statusInformation; + } + protected getNodeDepth(node: TreeNodeInterface): number { return node.depth; } @@ -1225,7 +1267,13 @@ export class Tree extends LitElement { } protected getNodeTitle(node: TreeNodeInterface): string { - return node.tooltip ? node.tooltip : 'uid=' + node.identifier + ' ' + node.name; + let baseNodeTitle = node.tooltip ? node.tooltip : 'uid=' + node.identifier + ' ' + node.name; + const statusInformation = this.getNodeStatusInformation(node); + if (statusInformation.length) { + baseNodeTitle += '; ' + statusInformation.map(node => node.label).join('; '); + } + + return baseNodeTitle; } /** diff --git a/typo3/sysext/backend/Classes/Controller/FileStorage/TreeController.php b/typo3/sysext/backend/Classes/Controller/FileStorage/TreeController.php index 9618e682b5e81eecb9c676812f9ebe5e8f3ee851..5343b1e0ce1780865e6ae139d425f38c9fdddaa8 100644 --- a/typo3/sysext/backend/Classes/Controller/FileStorage/TreeController.php +++ b/typo3/sysext/backend/Classes/Controller/FileStorage/TreeController.php @@ -162,6 +162,7 @@ class TreeController loaded: (bool)($item['loaded'] ?? false), icon: $item['icon'], overlayIcon: $item['overlayIcon'], + statusInformation: (array)($item['statusInformation'] ?? []), ), pathIdentifier: (string)($item['pathIdentifier'] ?? ''), storage: (int)($item['storage'] ?? 0), diff --git a/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php b/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php index 3601f028371aaf21746b1d400042d8b185f9b456..f2e8826dc0f63355326f1e1cf0aae69f629727ad 100644 --- a/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php +++ b/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php @@ -210,6 +210,7 @@ class FormSelectTreeAjaxController loaded: true, icon: (string)($item['icon'] ?? ''), overlayIcon: (string)($item['overlayIcon'] ?? ''), + statusInformation: (array)($item['statusInformation'] ?? []), ), checked: (bool)($item['checked'] ?? false), selectable: (bool)($item['selectable'] ?? false), diff --git a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php index 4a8a9c97267315156002ffe8336b0306f7c499f8..65227c5e4a59c73d7ed25aad0e2ded04645db670 100644 --- a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php +++ b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php @@ -634,6 +634,7 @@ class TreeController deletable: (bool)($item['deletable'] ?? false), icon: (string)($item['icon'] ?? ''), overlayIcon: (string)($item['overlayIcon'] ?? ''), + statusInformation: (array)($item['statusInformation'] ?? []), ), // PageTreeItem doktype: (int)($item['doktype'] ?? ''), diff --git a/typo3/sysext/backend/Classes/Dto/Tree/Status/StatusInformation.php b/typo3/sysext/backend/Classes/Dto/Tree/Status/StatusInformation.php new file mode 100644 index 0000000000000000000000000000000000000000..75a89256e6c44445fd56c1415aef7ee01e18eefb --- /dev/null +++ b/typo3/sysext/backend/Classes/Dto/Tree/Status/StatusInformation.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Dto\Tree\Status; + +use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; + +/** + * @internal + */ +final readonly class StatusInformation implements \JsonSerializable +{ + public function __construct( + public string $label, + public ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::INFO, + public int $priority = 0, + public string $icon = '', + public string $overlayIcon = '', + ) {} + + public function jsonSerialize(): array + { + return get_object_vars($this); + } +} diff --git a/typo3/sysext/backend/Classes/Dto/Tree/TreeItem.php b/typo3/sysext/backend/Classes/Dto/Tree/TreeItem.php index 89ce903fb8af2401527622df871991e207be88c3..0d376f115886ba4f5ec244fd040404d0e44111db 100644 --- a/typo3/sysext/backend/Classes/Dto/Tree/TreeItem.php +++ b/typo3/sysext/backend/Classes/Dto/Tree/TreeItem.php @@ -17,11 +17,16 @@ declare(strict_types=1); namespace TYPO3\CMS\Backend\Dto\Tree; +use TYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation; + /** * @internal */ final readonly class TreeItem implements \JsonSerializable { + /** + * @param StatusInformation[] $statusInformation + **/ public function __construct( public string $identifier, public string $parentIdentifier, @@ -37,6 +42,7 @@ final readonly class TreeItem implements \JsonSerializable public string $icon, public string $overlayIcon, public string $note = '', + public array $statusInformation = [], public bool $editable = false, public bool $deletable = false, ) {} diff --git a/typo3/sysext/backend/Resources/Public/Css/backend.css b/typo3/sysext/backend/Resources/Public/Css/backend.css index 22a9cd4fcafcf3001f34c71fe4a8e4e6c73eb42e..3ea3e7090ae8bde94341aca933229842d03a196c 100644 --- a/typo3/sysext/backend/Resources/Public/Css/backend.css +++ b/typo3/sysext/backend/Resources/Public/Css/backend.css @@ -3374,6 +3374,7 @@ to{opacity:1;transform:translate3d(0,0,0)} .node-note{margin-top:-.65em;font-size:10px;opacity:.65} .node-edit{display:flex;flex-grow:1;width:100%;padding:0;padding-inline-start:calc(.25rem - 1px);border:1px solid var(--tree-node-border-color);color:var(--typo3-component-color);background:var(--typo3-component-bg);outline:0} .node-highlight-text{color:var(--typo3-component-match-highlight-color);background-color:var(--typo3-component-match-highlight-bg)} +.node-information{display:flex;gap:.15rem;padding-inline-start:.25rem;opacity:.75} .node-action{display:none;cursor:pointer} .node:hover .node-action{display:flex} .node-dropzone-delete{position:absolute;display:flex;justify-content:center;align-items:center;inset-inline-end:0;inset-block-start:0;height:100%;padding:0 .5rem;color:var(--tree-drag-dropzone-delete-color);background-color:var(--tree-drag-dropzone-delete-bg);gap:.25rem;z-index:1} diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/tree/page-tree-element.js b/typo3/sysext/backend/Resources/Public/JavaScript/tree/page-tree-element.js index b1578543bdf935e5982847c57e24502a829fb7bd..15e9e7094ec22dc58179edb21110d74780ffd132 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/tree/page-tree-element.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/tree/page-tree-element.js @@ -79,4 +79,4 @@ var __decorate=function(e,t,o,n){var a,r=arguments.length,i=r<3?t:null===n?n=Obj </ul> </div> </div> - `}handleDragStart(e,t){const o={__hidden:!1,__indeterminate:!1,__loading:!1,__processed:!1,__treeDragAction:"",__treeIdentifier:"",__treeParents:[""],__parents:[""],__x:0,__y:0,deletable:!1,depth:0,editable:!0,expanded:!1,hasChildren:!1,icon:t.icon,overlayIcon:"",identifier:"NEW"+Math.floor(1e9*Math.random()).toString(16),loaded:!1,name:"",note:"",parentIdentifier:"",prefix:"",recordType:"pages",suffix:"",tooltip:"",type:"PageTreeItem",doktype:t.nodeType};this.tree.draggingNode=o,this.tree.nodeDragMode=TreeNodeCommandEnum.NEW,e.dataTransfer.clearData();const n={statusIconIdentifier:this.tree.getNodeDragStatusIcon(),tooltipIconIdentifier:t.icon,tooltipLabel:t.title};e.dataTransfer.setData(DataTransferTypes.dragTooltip,JSON.stringify(n)),e.dataTransfer.setData(DataTransferTypes.newTreenode,JSON.stringify(o)),e.dataTransfer.effectAllowed="move"}};__decorate([property({type:EditablePageTree})],PageTreeToolbar.prototype,"tree",void 0),PageTreeToolbar=__decorate([customElement("typo3-backend-navigation-component-pagetree-toolbar")],PageTreeToolbar); \ No newline at end of file + `}handleDragStart(e,t){const o={__hidden:!1,__indeterminate:!1,__loading:!1,__processed:!1,__treeDragAction:"",__treeIdentifier:"",__treeParents:[""],__parents:[""],__x:0,__y:0,deletable:!1,depth:0,editable:!0,expanded:!1,hasChildren:!1,icon:t.icon,overlayIcon:"",identifier:"NEW"+Math.floor(1e9*Math.random()).toString(16),loaded:!1,name:"",note:"",parentIdentifier:"",prefix:"",recordType:"pages",suffix:"",tooltip:"",type:"PageTreeItem",doktype:t.nodeType,statusInformation:[]};this.tree.draggingNode=o,this.tree.nodeDragMode=TreeNodeCommandEnum.NEW,e.dataTransfer.clearData();const n={statusIconIdentifier:this.tree.getNodeDragStatusIcon(),tooltipIconIdentifier:t.icon,tooltipLabel:t.title};e.dataTransfer.setData(DataTransferTypes.dragTooltip,JSON.stringify(n)),e.dataTransfer.setData(DataTransferTypes.newTreenode,JSON.stringify(o)),e.dataTransfer.effectAllowed="move"}};__decorate([property({type:EditablePageTree})],PageTreeToolbar.prototype,"tree",void 0),PageTreeToolbar=__decorate([customElement("typo3-backend-navigation-component-pagetree-toolbar")],PageTreeToolbar); \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js b/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js index 3dc165346b5d6742144427769a61b7011a222001..ace5876850adde18db271ce625e7762d771e8677 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js @@ -10,7 +10,7 @@ * * The TYPO3 project - inspiring people to share! */ -var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(e,t,i,o);else for(var d=e.length-1;d>=0;d--)(n=e[d])&&(r=(s<3?n(r):s>3?n(t,i,r):n(t,i))||r);return s>3&&r&&Object.defineProperty(t,i,r),r};import{html,LitElement,nothing}from"lit";import{property,state,query}from"lit/decorators.js";import{repeat}from"lit/directives/repeat.js";import{styleMap}from"lit/directives/style-map.js";import{TreeNodeCommandEnum,TreeNodePositionEnum}from"@typo3/backend/tree/tree-node.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Notification from"@typo3/backend/notification.js";import{KeyTypesEnum as KeyTypes}from"@typo3/backend/enum/key-types.js";import"@typo3/backend/element/icon-element.js";import ClientStorage from"@typo3/backend/storage/client.js";import{DataTransferTypes}from"@typo3/backend/enum/data-transfer-types.js";export class Tree extends LitElement{constructor(){super(...arguments),this.setup=null,this.settings={showIcons:!1,width:300,dataUrl:"",filterUrl:"",defaultProperties:{},expandUpToLevel:null,actions:[]},this.nodes=[],this.currentScrollPosition=0,this.currentVisibleHeight=0,this.searchTerm=null,this.loading=!1,this.hoveredNode=null,this.nodeDragAllowed=!1,this.isOverRoot=!1,this.nodeDragPosition=null,this.nodeDragMode=null,this.draggingNode=null,this.nodeHeight=32,this.indentWidth=20,this.displayNodes=[],this.focusedNode=null,this.editingNode=null,this.openNodeTimeout={targetNode:null,timeout:null},this.unfilteredNodes="",this.muteErrorNotifications=!1,this.networkErrorTitle=top.TYPO3.lang.tree_networkError,this.networkErrorMessage=top.TYPO3.lang.tree_networkErrorDescription,this.allowNodeEdit=!1,this.allowNodeDrag=!1,this.allowNodeSorting=!1}getNodeFromElement(e){return null!==e&&"treeId"in e.dataset?this.getNodeByTreeIdentifier(e.dataset.treeId):null}getElementFromNode(e){return this.querySelector('[data-tree-id="'+this.getNodeTreeIdentifier(e)+'"]')}hideChildren(e){e.expanded=!1,this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}async showChildren(e){e.expanded=!0,await this.loadChildren(e),this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}getDataUrl(e=null){return null===e?this.settings.dataUrl:this.settings.dataUrl+"&parent="+e.identifier+"&depth="+e.depth}getFilterUrl(){return this.settings.filterUrl+"&q="+this.searchTerm}async loadData(){this.loading=!0,this.nodes=this.prepareNodes(await this.fetchData()),this.loading=!1}async fetchData(e=null){try{const t=await new AjaxRequest(this.getDataUrl(e)).get({cache:"no-cache"});let i=await t.resolve();if(!Array.isArray(i))return[];null!==e&&(i=i.filter((t=>t.identifier!==e.identifier)),i.unshift(e)),i=this.enhanceNodes(i),null!==e&&i.shift();const o=await Promise.all(i.map((async e=>{const t=e.__parents.join("_"),o=i.find((e=>e.__treeIdentifier===t))||null,n=null===o||o.expanded;if(!e.loaded&&e.hasChildren&&e.expanded&&n){const t=await this.fetchData(e);return e.loaded=!0,[e,...t]}return[e]})));return o.flat()}catch(e){return this.errorNotification(e),[]}}async loadChildren(e){try{if(e.loaded)return void await Promise.all(this.nodes.filter((t=>t.__parents.join("_")===e.__treeIdentifier&&!t.loaded&&t.hasChildren&&t.expanded)).map((e=>this.loadChildren(e))));e.__loading=!0;const t=await this.fetchData(e),i=this.nodes.indexOf(e)+1;let o=0;for(let t=i;t<this.nodes.length&&!(this.nodes[t].depth<=e.depth);++t)o++;this.nodes.splice(i,o,...t),e.__loading=!1,e.loaded=!0}catch(t){throw this.errorNotification(t),e.__loading=!1,t}}getIdentifier(){return this.id??this.setup.id}getLocalStorageIdentifier(){return"tree-state-"+this.getIdentifier()}getNodeStatus(e){return(JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier()))??{})[e.__treeIdentifier]??{expanded:!1}}saveNodeStatus(e){const t=JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier()))??{};t[e.__treeIdentifier]={expanded:e.expanded},ClientStorage.set(this.getLocalStorageIdentifier(),JSON.stringify(t))}refreshOrFilterTree(){""!==this.searchTerm?this.filter(this.searchTerm):this.loadData()}selectFirstNode(){const e=this.getFirstNode();this.selectNode(e,!0),this.focusNode(e)}selectNode(e,t=!0){this.isNodeSelectable(e)&&(this.resetSelectedNodes(),e.checked=!0,this.dispatchEvent(new CustomEvent("typo3:tree:node-selected",{detail:{node:e,propagate:t}})))}async focusNode(e){this.focusedNode=e;const t=this.getElementFromNode(this.focusedNode);t?t.focus():(this.requestUpdate(),this.updateComplete.then((()=>{this.getElementFromNode(this.focusedNode)?.focus()})))}async editNode(e){this.isNodeEditable(e)&&(this.editingNode=e,this.requestUpdate(),this.updateComplete.then((()=>{const e=this.getElementFromNode(this.editingNode)?.querySelector(".node-edit");e&&(e.focus(),e.select())})))}async deleteNode(e){e.deletable?this.handleNodeDelete(e):console.error("The Node cannot be deleted.")}async moveNode(e,t,i){this.handleNodeMove(e,t,i)}async addNode(e,t,i){let o=this.nodes.indexOf(t);const n=i===TreeNodePositionEnum.INSIDE?t:this.getParentNode(t),s=this.enhanceNodes([n,{...e,depth:n?n.depth+1:0}]).pop();n&&(t.hasChildren&&!t.expanded&&await this.showChildren(t),t.hasChildren||(t.hasChildren=!0,t.expanded=!0)),i!==TreeNodePositionEnum.INSIDE&&i!==TreeNodePositionEnum.AFTER||o++,this.nodes.splice(o,0,s),this.handleNodeAdd(s,t,i)}async removeNode(e){const t=this.nodes.indexOf(e);t>-1&&this.nodes.splice(t,1)}filter(e){"string"==typeof e&&(this.searchTerm=e),this.searchTerm&&this.settings.filterUrl?(this.loading=!0,new AjaxRequest(this.getFilterUrl()).get({cache:"no-cache"}).then((e=>e.resolve())).then((e=>{const t=Array.isArray(e)?e:[];t.length>0&&(""===this.unfilteredNodes&&(this.unfilteredNodes=JSON.stringify(this.nodes)),this.nodes=this.enhanceNodes(t))})).catch((e=>{throw this.errorNotification(e),e})).then((()=>{this.loading=!1}))):(this.resetFilter(),this.loading=!1)}resetFilter(){if(this.searchTerm="",this.unfilteredNodes.length>0){const e=this.getSelectedNodes()[0];if(void 0===e)return void this.loadData();this.nodes=this.enhanceNodes(JSON.parse(this.unfilteredNodes)),this.unfilteredNodes="";const t=this.getNodeByTreeIdentifier(e.__treeIdentifier);t?this.selectNode(t,!1):this.loadData()}else this.loadData()}errorNotification(e=null){if(!this.muteErrorNotifications)if(Array.isArray(e))e.forEach((e=>{Notification.error(e.title,e.message)}));else{let t=this.networkErrorTitle;e&&e.target&&(e.target.status||e.target.statusText)&&(t+=" - "+(e.target.status||"")+" "+(e.target.statusText||"")),Notification.error(t,this.networkErrorMessage)}}getSelectedNodes(){return this.nodes.filter((e=>e.checked))}getNodeByTreeIdentifier(e){return this.nodes.find((t=>t.__treeIdentifier===e))}getNodeDragStatusIcon(){return this.nodeDragMode===TreeNodeCommandEnum.DELETE?"actions-delete":this.nodeDragMode===TreeNodeCommandEnum.NEW?"actions-add":this.nodeDragPosition===TreeNodePositionEnum.BEFORE?"apps-pagetree-drag-move-above":this.nodeDragPosition===TreeNodePositionEnum.INSIDE?"apps-pagetree-drag-move-into":this.nodeDragPosition===TreeNodePositionEnum.AFTER?"apps-pagetree-drag-move-below":"actions-ban"}prepareNodes(e){const t=new CustomEvent("typo3:tree:nodes-prepared",{detail:{nodes:e},bubbles:!1});return this.dispatchEvent(t),t.detail.nodes}enhanceNodes(e){const t=e.reduce(((e,t)=>{if(!0===t.__processed)return[...e,t];(t=Object.assign({},this.settings.defaultProperties,t)).__parents=[];const i=t.depth>0?e.findLast((e=>e.depth<t.depth)):null;i&&(t.__parents=[...i.__parents,i.identifier]),t.__treeIdentifier=t.identifier,t.__loading=!1,t.__treeParents=[],i&&(t.__treeIdentifier=i.__treeIdentifier+"_"+t.__treeIdentifier,t.__treeParents=[...i.__treeParents,i.__treeIdentifier]),t.expanded=!0===t.expanded||(null!==this.settings.expandUpToLevel?t.depth<this.settings.expandUpToLevel:Boolean(this.getNodeStatus(t).expanded)),t.__processed=!0;const o=this;return[...e,new Proxy(t,{set:(e,t,i)=>(e[t]!==i&&(e[t]=i,o.requestUpdate()),!0)})]}),[]);return 1===t.filter((e=>0===e.depth)).length&&(t[0].expanded=!0),t}createRenderRoot(){return this}render(){const e=this.loading?html` +var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(e,t,i,o);else for(var d=e.length-1;d>=0;d--)(n=e[d])&&(r=(s<3?n(r):s>3?n(t,i,r):n(t,i))||r);return s>3&&r&&Object.defineProperty(t,i,r),r};import{html,LitElement,nothing}from"lit";import{property,state,query}from"lit/decorators.js";import{repeat}from"lit/directives/repeat.js";import{styleMap}from"lit/directives/style-map.js";import{ifDefined}from"lit/directives/if-defined.js";import{TreeNodeCommandEnum,TreeNodePositionEnum}from"@typo3/backend/tree/tree-node.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Notification from"@typo3/backend/notification.js";import{KeyTypesEnum as KeyTypes}from"@typo3/backend/enum/key-types.js";import"@typo3/backend/element/icon-element.js";import ClientStorage from"@typo3/backend/storage/client.js";import{DataTransferTypes}from"@typo3/backend/enum/data-transfer-types.js";import Severity from"@typo3/backend/severity.js";export class Tree extends LitElement{constructor(){super(...arguments),this.setup=null,this.settings={showIcons:!1,width:300,dataUrl:"",filterUrl:"",defaultProperties:{},expandUpToLevel:null,actions:[]},this.nodes=[],this.currentScrollPosition=0,this.currentVisibleHeight=0,this.searchTerm=null,this.loading=!1,this.hoveredNode=null,this.nodeDragAllowed=!1,this.isOverRoot=!1,this.nodeDragPosition=null,this.nodeDragMode=null,this.draggingNode=null,this.nodeHeight=32,this.indentWidth=20,this.displayNodes=[],this.focusedNode=null,this.editingNode=null,this.openNodeTimeout={targetNode:null,timeout:null},this.unfilteredNodes="",this.muteErrorNotifications=!1,this.networkErrorTitle=top.TYPO3.lang.tree_networkError,this.networkErrorMessage=top.TYPO3.lang.tree_networkErrorDescription,this.allowNodeEdit=!1,this.allowNodeDrag=!1,this.allowNodeSorting=!1}getNodeFromElement(e){return null!==e&&"treeId"in e.dataset?this.getNodeByTreeIdentifier(e.dataset.treeId):null}getElementFromNode(e){return this.querySelector('[data-tree-id="'+this.getNodeTreeIdentifier(e)+'"]')}hideChildren(e){e.expanded=!1,this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}async showChildren(e){e.expanded=!0,await this.loadChildren(e),this.saveNodeStatus(e),this.dispatchEvent(new CustomEvent("typo3:tree:expand-toggle",{detail:{node:e}}))}getDataUrl(e=null){return null===e?this.settings.dataUrl:this.settings.dataUrl+"&parent="+e.identifier+"&depth="+e.depth}getFilterUrl(){return this.settings.filterUrl+"&q="+this.searchTerm}async loadData(){this.loading=!0,this.nodes=this.prepareNodes(await this.fetchData()),this.loading=!1}async fetchData(e=null){try{const t=await new AjaxRequest(this.getDataUrl(e)).get({cache:"no-cache"});let i=await t.resolve();if(!Array.isArray(i))return[];null!==e&&(i=i.filter((t=>t.identifier!==e.identifier)),i.unshift(e)),i=this.enhanceNodes(i),null!==e&&i.shift();const o=await Promise.all(i.map((async e=>{const t=e.__parents.join("_"),o=i.find((e=>e.__treeIdentifier===t))||null,n=null===o||o.expanded;if(!e.loaded&&e.hasChildren&&e.expanded&&n){const t=await this.fetchData(e);return e.loaded=!0,[e,...t]}return[e]})));return o.flat()}catch(e){return this.errorNotification(e),[]}}async loadChildren(e){try{if(e.loaded)return void await Promise.all(this.nodes.filter((t=>t.__parents.join("_")===e.__treeIdentifier&&!t.loaded&&t.hasChildren&&t.expanded)).map((e=>this.loadChildren(e))));e.__loading=!0;const t=await this.fetchData(e),i=this.nodes.indexOf(e)+1;let o=0;for(let t=i;t<this.nodes.length&&!(this.nodes[t].depth<=e.depth);++t)o++;this.nodes.splice(i,o,...t),e.__loading=!1,e.loaded=!0}catch(t){throw this.errorNotification(t),e.__loading=!1,t}}getIdentifier(){return this.id??this.setup.id}getLocalStorageIdentifier(){return"tree-state-"+this.getIdentifier()}getNodeStatus(e){return(JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier()))??{})[e.__treeIdentifier]??{expanded:!1}}saveNodeStatus(e){const t=JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier()))??{};t[e.__treeIdentifier]={expanded:e.expanded},ClientStorage.set(this.getLocalStorageIdentifier(),JSON.stringify(t))}refreshOrFilterTree(){""!==this.searchTerm?this.filter(this.searchTerm):this.loadData()}selectFirstNode(){const e=this.getFirstNode();this.selectNode(e,!0),this.focusNode(e)}selectNode(e,t=!0){this.isNodeSelectable(e)&&(this.resetSelectedNodes(),e.checked=!0,this.dispatchEvent(new CustomEvent("typo3:tree:node-selected",{detail:{node:e,propagate:t}})))}async focusNode(e){this.focusedNode=e;const t=this.getElementFromNode(this.focusedNode);t?t.focus():(this.requestUpdate(),this.updateComplete.then((()=>{this.getElementFromNode(this.focusedNode)?.focus()})))}async editNode(e){this.isNodeEditable(e)&&(this.editingNode=e,this.requestUpdate(),this.updateComplete.then((()=>{const e=this.getElementFromNode(this.editingNode)?.querySelector(".node-edit");e&&(e.focus(),e.select())})))}async deleteNode(e){e.deletable?this.handleNodeDelete(e):console.error("The Node cannot be deleted.")}async moveNode(e,t,i){this.handleNodeMove(e,t,i)}async addNode(e,t,i){let o=this.nodes.indexOf(t);const n=i===TreeNodePositionEnum.INSIDE?t:this.getParentNode(t),s=this.enhanceNodes([n,{...e,depth:n?n.depth+1:0}]).pop();n&&(t.hasChildren&&!t.expanded&&await this.showChildren(t),t.hasChildren||(t.hasChildren=!0,t.expanded=!0)),i!==TreeNodePositionEnum.INSIDE&&i!==TreeNodePositionEnum.AFTER||o++,this.nodes.splice(o,0,s),this.handleNodeAdd(s,t,i)}async removeNode(e){const t=this.nodes.indexOf(e);t>-1&&this.nodes.splice(t,1)}filter(e){"string"==typeof e&&(this.searchTerm=e),this.searchTerm&&this.settings.filterUrl?(this.loading=!0,new AjaxRequest(this.getFilterUrl()).get({cache:"no-cache"}).then((e=>e.resolve())).then((e=>{const t=Array.isArray(e)?e:[];t.length>0&&(""===this.unfilteredNodes&&(this.unfilteredNodes=JSON.stringify(this.nodes)),this.nodes=this.enhanceNodes(t))})).catch((e=>{throw this.errorNotification(e),e})).then((()=>{this.loading=!1}))):(this.resetFilter(),this.loading=!1)}resetFilter(){if(this.searchTerm="",this.unfilteredNodes.length>0){const e=this.getSelectedNodes()[0];if(void 0===e)return void this.loadData();this.nodes=this.enhanceNodes(JSON.parse(this.unfilteredNodes)),this.unfilteredNodes="";const t=this.getNodeByTreeIdentifier(e.__treeIdentifier);t?this.selectNode(t,!1):this.loadData()}else this.loadData()}errorNotification(e=null){if(!this.muteErrorNotifications)if(Array.isArray(e))e.forEach((e=>{Notification.error(e.title,e.message)}));else{let t=this.networkErrorTitle;e&&e.target&&(e.target.status||e.target.statusText)&&(t+=" - "+(e.target.status||"")+" "+(e.target.statusText||"")),Notification.error(t,this.networkErrorMessage)}}getSelectedNodes(){return this.nodes.filter((e=>e.checked))}getNodeByTreeIdentifier(e){return this.nodes.find((t=>t.__treeIdentifier===e))}getNodeDragStatusIcon(){return this.nodeDragMode===TreeNodeCommandEnum.DELETE?"actions-delete":this.nodeDragMode===TreeNodeCommandEnum.NEW?"actions-add":this.nodeDragPosition===TreeNodePositionEnum.BEFORE?"apps-pagetree-drag-move-above":this.nodeDragPosition===TreeNodePositionEnum.INSIDE?"apps-pagetree-drag-move-into":this.nodeDragPosition===TreeNodePositionEnum.AFTER?"apps-pagetree-drag-move-below":"actions-ban"}prepareNodes(e){const t=new CustomEvent("typo3:tree:nodes-prepared",{detail:{nodes:e},bubbles:!1});return this.dispatchEvent(t),t.detail.nodes}enhanceNodes(e){const t=e.reduce(((e,t)=>{if(!0===t.__processed)return[...e,t];(t=Object.assign({},this.settings.defaultProperties,t)).__parents=[];const i=t.depth>0?e.findLast((e=>e.depth<t.depth)):null;i&&(t.__parents=[...i.__parents,i.identifier]),t.__treeIdentifier=t.identifier,t.__loading=!1,t.__treeParents=[],i&&(t.__treeIdentifier=i.__treeIdentifier+"_"+t.__treeIdentifier,t.__treeParents=[...i.__treeParents,i.__treeIdentifier]),t.expanded=!0===t.expanded||(null!==this.settings.expandUpToLevel?t.depth<this.settings.expandUpToLevel:Boolean(this.getNodeStatus(t).expanded)),t.__processed=!0;const o=this;return[...e,new Proxy(t,{set:(e,t,i)=>(e[t]!==i&&(e[t]=i,o.requestUpdate()),!0)})]}),[]);return 1===t.filter((e=>0===e.depth)).length&&(t[0].expanded=!0),t}createRenderRoot(){return this}render(){const e=this.loading?html` <div class="nodes-loader"> <div class="nodes-loader-inner"> <typo3-backend-icon identifier="spinner-circle" size="medium"></typo3-backend-icon> @@ -61,6 +61,7 @@ var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Obj ${this.createNodeGuides(e)} ${this.createNodeLoader(e)||this.createNodeToggle(e)||nothing} ${this.createNodeContent(e)} + ${this.createNodeStatusInformation(e)} ${this.createNodeDeleteDropZone(e)} </div> `))} @@ -108,7 +109,16 @@ var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Obj <div class="node-label"> <div class="node-name" .innerHTML="${t}"></div> ${e.note?html`<div class="node-note">${e.note}</div>`:nothing} - </div>`}createNodeDeleteDropZone(e){return this.draggingNode===e&&e.deletable?html` + </div>`}createNodeStatusInformation(e){const t=this.getNodeStatusInformation(e);if(0===t.length)return html`${nothing}`;const i=t[0],o=Severity.getCssClass(i.severity),n=""!==i.icon?i.icon:"actions-dot",s=""!==i.overlayIcon?i.overlayIcon:void 0;return html` + <span class="node-information"> + <typo3-backend-icon + class="text-${o}" + identifier=${n} + overlay=${ifDefined(s)} + size="small" + ></typo3-backend-icon> + </span> + `}createNodeDeleteDropZone(e){return this.draggingNode===e&&e.deletable?html` <div class="node-dropzone-delete" data-tree-dropzone="delete"> <typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon> ${TYPO3.lang.deleteItem} @@ -121,4 +131,4 @@ var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Obj @keydown="${i=>{const o=i.key;if([KeyTypes.ENTER,KeyTypes.TAB].includes(o)){const o=i.target.value.trim();this.editingNode=null,this.requestUpdate(),o!==e.name&&""!==o?(this.handleNodeEdit(e,o),this.focusNode(e)):t===TreeNodeCommandEnum.NEW&&""===o?this.removeNode(e):this.focusNode(e)}else[KeyTypes.ESCAPE].includes(o)&&(this.editingNode=null,this.requestUpdate(),t===TreeNodeCommandEnum.NEW?this.removeNode(e):this.focusNode(e))}}" value="${e.name}" /> - `}async handleNodeEdit(e,t){console.error("The function Tree->handleNodeEdit is not implemented.")}handleNodeDelete(e){console.error("The function Tree->handleNodeDelete is not implemented.")}handleNodeMove(e,t,i){console.error("The function Tree->handleNodeMove is not implemented.")}async handleNodeAdd(e,t,i){console.error("The function Tree->handleNodeAdd is not implemented.")}createNodeContentAction(e){return html`${nothing}`}createDataTransferItemsFromNode(e){throw new Error("The function Tree->createDataTransferItemFromNode is not implemented.")}getNodeIdentifier(e){return e.identifier}getNodeTreeIdentifier(e){return e.__treeIdentifier}getNodeParentTreeIdentifier(e){return e.__parents.join("_")}getNodeClasses(e){const t=["node"];return e.checked&&t.push("node-selected"),this.draggingNode===e&&t.push("node-dragging"),t}getNodeDepth(e){return e.depth}getNodeChildren(e){return e.hasChildren?this.displayNodes.filter((t=>e===this.getParentNode(t))):[]}getNodeSetsize(e){if(0===e.depth)return this.displayNodes.filter((e=>0===e.depth)).length;const t=this.getParentNode(e);return this.getNodeChildren(t).length}getNodePositionInSet(e){const t=this.getParentNode(e);let i=[];return 0===e.depth?i=this.displayNodes.filter((e=>0===e.depth)):null!==t&&(i=this.getNodeChildren(t)),i.indexOf(e)+1}getFirstNode(){return this.displayNodes.length?this.displayNodes[0]:null}getPreviousNode(e){const t=this.displayNodes.indexOf(e)-1;return this.displayNodes[t]?this.displayNodes[t]:null}getNextNode(e){const t=this.displayNodes.indexOf(e)+1;return this.displayNodes[t]?this.displayNodes[t]:null}getLastNode(){return this.displayNodes.length?this.displayNodes[this.displayNodes.length-1]:null}getParentNode(e){return e.__parents.length?this.getNodeByTreeIdentifier(this.getNodeParentTreeIdentifier(e)):null}getNodeTitle(e){return e.tooltip?e.tooltip:"uid="+e.identifier+" "+e.name}handleNodeToggle(e){e.expanded?this.hideChildren(e):this.showChildren(e)}isRTL(){return"rtl"===window.getComputedStyle(document.documentElement).getPropertyValue("direction")}handleKeyboardInteraction(e){if(null!==this.editingNode)return;if(!1===[KeyTypes.ENTER,KeyTypes.SPACE,KeyTypes.END,KeyTypes.HOME,KeyTypes.LEFT,KeyTypes.UP,KeyTypes.RIGHT,KeyTypes.DOWN].includes(e.key))return;const t=e.target,i=this.getNodeFromElement(t);if(null===i)return;const o=this.getParentNode(i),n=this.getFirstNode(),s=this.getPreviousNode(i),r=this.getNextNode(i),d=this.getLastNode();switch(e.preventDefault(),e.key){case KeyTypes.HOME:null!==n&&(this.scrollNodeIntoVisibleArea(n),this.focusNode(n));break;case KeyTypes.END:null!==d&&(this.scrollNodeIntoVisibleArea(d),this.focusNode(d));break;case KeyTypes.UP:null!==s&&(this.scrollNodeIntoVisibleArea(s),this.focusNode(s));break;case KeyTypes.DOWN:null!==r&&(this.scrollNodeIntoVisibleArea(r),this.focusNode(r));break;case KeyTypes.LEFT:i.expanded?i.hasChildren&&this.hideChildren(i):o&&(this.scrollNodeIntoVisibleArea(o),this.focusNode(o));break;case KeyTypes.RIGHT:i.expanded&&r?(this.scrollNodeIntoVisibleArea(r),this.focusNode(r)):i.hasChildren&&this.showChildren(i);break;case KeyTypes.ENTER:case KeyTypes.SPACE:this.selectNode(i)}}scrollNodeIntoVisibleArea(e){const t=e.__y,i=e.__y+this.nodeHeight,o=t>=this.currentScrollPosition,n=i<=this.currentScrollPosition+this.currentVisibleHeight;if(!(o&&n)){let e=this.currentScrollPosition;o||n?o?n||(e=i-this.currentVisibleHeight):e=t:e=i-this.currentVisibleHeight,e<0&&(e=0),this.root.scrollTo({top:e})}}registerUnloadHandler(){try{if(!window.frameElement)return;window.addEventListener("pagehide",(()=>this.muteErrorNotifications=!0),{once:!0})}catch(e){console.error("Failed to check the existence of window.frameElement – using a foreign origin?")}}}__decorate([property({type:Object})],Tree.prototype,"setup",void 0),__decorate([state()],Tree.prototype,"settings",void 0),__decorate([query(".nodes-root")],Tree.prototype,"root",void 0),__decorate([state()],Tree.prototype,"nodes",void 0),__decorate([state()],Tree.prototype,"currentScrollPosition",void 0),__decorate([state()],Tree.prototype,"currentVisibleHeight",void 0),__decorate([state()],Tree.prototype,"searchTerm",void 0),__decorate([state()],Tree.prototype,"loading",void 0),__decorate([state()],Tree.prototype,"hoveredNode",void 0),__decorate([state()],Tree.prototype,"nodeDragAllowed",void 0); \ No newline at end of file + `}async handleNodeEdit(e,t){console.error("The function Tree->handleNodeEdit is not implemented.")}handleNodeDelete(e){console.error("The function Tree->handleNodeDelete is not implemented.")}handleNodeMove(e,t,i){console.error("The function Tree->handleNodeMove is not implemented.")}async handleNodeAdd(e,t,i){console.error("The function Tree->handleNodeAdd is not implemented.")}createNodeContentAction(e){return html`${nothing}`}createDataTransferItemsFromNode(e){throw new Error("The function Tree->createDataTransferItemFromNode is not implemented.")}getNodeIdentifier(e){return e.identifier}getNodeTreeIdentifier(e){return e.__treeIdentifier}getNodeParentTreeIdentifier(e){return e.__parents.join("_")}getNodeClasses(e){const t=["node"];return e.checked&&t.push("node-selected"),this.draggingNode===e&&t.push("node-dragging"),t}getNodeStatusInformation(e){if(0===e.statusInformation.length)return[];return e.statusInformation.sort(((e,t)=>e.severity!==t.severity?t.severity-e.severity:t.priority-e.priority))}getNodeDepth(e){return e.depth}getNodeChildren(e){return e.hasChildren?this.displayNodes.filter((t=>e===this.getParentNode(t))):[]}getNodeSetsize(e){if(0===e.depth)return this.displayNodes.filter((e=>0===e.depth)).length;const t=this.getParentNode(e);return this.getNodeChildren(t).length}getNodePositionInSet(e){const t=this.getParentNode(e);let i=[];return 0===e.depth?i=this.displayNodes.filter((e=>0===e.depth)):null!==t&&(i=this.getNodeChildren(t)),i.indexOf(e)+1}getFirstNode(){return this.displayNodes.length?this.displayNodes[0]:null}getPreviousNode(e){const t=this.displayNodes.indexOf(e)-1;return this.displayNodes[t]?this.displayNodes[t]:null}getNextNode(e){const t=this.displayNodes.indexOf(e)+1;return this.displayNodes[t]?this.displayNodes[t]:null}getLastNode(){return this.displayNodes.length?this.displayNodes[this.displayNodes.length-1]:null}getParentNode(e){return e.__parents.length?this.getNodeByTreeIdentifier(this.getNodeParentTreeIdentifier(e)):null}getNodeTitle(e){let t=e.tooltip?e.tooltip:"uid="+e.identifier+" "+e.name;const i=this.getNodeStatusInformation(e);return i.length&&(t+="; "+i.map((e=>e.label)).join("; ")),t}handleNodeToggle(e){e.expanded?this.hideChildren(e):this.showChildren(e)}isRTL(){return"rtl"===window.getComputedStyle(document.documentElement).getPropertyValue("direction")}handleKeyboardInteraction(e){if(null!==this.editingNode)return;if(!1===[KeyTypes.ENTER,KeyTypes.SPACE,KeyTypes.END,KeyTypes.HOME,KeyTypes.LEFT,KeyTypes.UP,KeyTypes.RIGHT,KeyTypes.DOWN].includes(e.key))return;const t=e.target,i=this.getNodeFromElement(t);if(null===i)return;const o=this.getParentNode(i),n=this.getFirstNode(),s=this.getPreviousNode(i),r=this.getNextNode(i),d=this.getLastNode();switch(e.preventDefault(),e.key){case KeyTypes.HOME:null!==n&&(this.scrollNodeIntoVisibleArea(n),this.focusNode(n));break;case KeyTypes.END:null!==d&&(this.scrollNodeIntoVisibleArea(d),this.focusNode(d));break;case KeyTypes.UP:null!==s&&(this.scrollNodeIntoVisibleArea(s),this.focusNode(s));break;case KeyTypes.DOWN:null!==r&&(this.scrollNodeIntoVisibleArea(r),this.focusNode(r));break;case KeyTypes.LEFT:i.expanded?i.hasChildren&&this.hideChildren(i):o&&(this.scrollNodeIntoVisibleArea(o),this.focusNode(o));break;case KeyTypes.RIGHT:i.expanded&&r?(this.scrollNodeIntoVisibleArea(r),this.focusNode(r)):i.hasChildren&&this.showChildren(i);break;case KeyTypes.ENTER:case KeyTypes.SPACE:this.selectNode(i)}}scrollNodeIntoVisibleArea(e){const t=e.__y,i=e.__y+this.nodeHeight,o=t>=this.currentScrollPosition,n=i<=this.currentScrollPosition+this.currentVisibleHeight;if(!(o&&n)){let e=this.currentScrollPosition;o||n?o?n||(e=i-this.currentVisibleHeight):e=t:e=i-this.currentVisibleHeight,e<0&&(e=0),this.root.scrollTo({top:e})}}registerUnloadHandler(){try{if(!window.frameElement)return;window.addEventListener("pagehide",(()=>this.muteErrorNotifications=!0),{once:!0})}catch(e){console.error("Failed to check the existence of window.frameElement – using a foreign origin?")}}}__decorate([property({type:Object})],Tree.prototype,"setup",void 0),__decorate([state()],Tree.prototype,"settings",void 0),__decorate([query(".nodes-root")],Tree.prototype,"root",void 0),__decorate([state()],Tree.prototype,"nodes",void 0),__decorate([state()],Tree.prototype,"currentScrollPosition",void 0),__decorate([state()],Tree.prototype,"currentVisibleHeight",void 0),__decorate([state()],Tree.prototype,"searchTerm",void 0),__decorate([state()],Tree.prototype,"loading",void 0),__decorate([state()],Tree.prototype,"hoveredNode",void 0),__decorate([state()],Tree.prototype,"nodeDragAllowed",void 0); \ No newline at end of file diff --git a/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103186-IntroduceTreeNodeStatusInformation.rst b/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103186-IntroduceTreeNodeStatusInformation.rst new file mode 100644 index 0000000000000000000000000000000000000000..328aed3d27de1dc8be5ff0fcca877b05c28dadfe --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103186-IntroduceTreeNodeStatusInformation.rst @@ -0,0 +1,48 @@ +.. include:: /Includes.rst.txt + +.. _feature-103186-1708686767: + +========================================================= +Feature: #103186 - Introduce tree node status information +========================================================= + +See :issue:`103186` + +Description +=========== + +We've enhanced the backend tree component by extending tree nodes to +incorporate status information. These details serve to indicate the +status of nodes and provide supplementary information. + +For instance, if a page undergoes changes within a workspace, it will +now display an indicator on the respective tree node. Additionally, +the status is appended to the node's title. This enhancement not only +improves visual clarity but also enhances information accessibility. + +Each node can accommodate multiple status information, prioritized by +severity and urgency. Critical messages take precedence over other +status notifications. + +.. code-block:: php + + new TreeItem( + ... + statusInformation: [ + new StatusInformation( + label: 'A warning message', + severity: ContextualFeedbackSeverity::WARNING, + priority: 0, + icon: 'actions-dot', + overlayIcon: '' + ) + ] + ), + +Impact +====== + +Tree nodes can now have status information. Workspace changes are +now reflected in the title of the node in addition to the indicator. + +.. index:: Backend, JavaScript, ext:backend diff --git a/typo3/sysext/workspaces/Classes/EventListener/PageTreeItemsHighlighter.php b/typo3/sysext/workspaces/Classes/EventListener/PageTreeItemsHighlighter.php index 564638f36cb8e10bdac1c4f2bae6f53896dcafc0..09381509a4b31a3cbc376b2b1a4f940e47ae5c22 100644 --- a/typo3/sysext/workspaces/Classes/EventListener/PageTreeItemsHighlighter.php +++ b/typo3/sysext/workspaces/Classes/EventListener/PageTreeItemsHighlighter.php @@ -18,8 +18,11 @@ declare(strict_types=1); namespace TYPO3\CMS\Workspaces\EventListener; use TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent; +use TYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation; use TYPO3\CMS\Core\Attribute\AsEventListener; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Versioning\VersionState; use TYPO3\CMS\Workspaces\Service\WorkspaceService; @@ -56,14 +59,24 @@ final class PageTreeItemsHighlighter || VersionState::tryFrom($page['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER ) ) { - $item['class'] = 'ver-element ver-versions'; + $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:status.has_changes'); + if (VersionState::tryFrom($page['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER) { + $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:status.is_new'); + } + $item['statusInformation'][] = new StatusInformation( + label: $label, + severity: ContextualFeedbackSeverity::WARNING, + ); } elseif ( $this->workspaceService->hasPageRecordVersions( $workspaceId, (int)(($page['t3ver_oid'] ?? 0) ?: ($page['uid'] ?? 0)) ) ) { - $item['class'] = 'ver-versions'; + $item['statusInformation'][] = new StatusInformation( + label: $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:status.contains_changes'), + severity: ContextualFeedbackSeverity::WARNING, + ); } } unset($item); @@ -71,6 +84,11 @@ final class PageTreeItemsHighlighter $event->setItems($items); } + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; diff --git a/typo3/sysext/workspaces/Resources/Private/Language/locallang.xlf b/typo3/sysext/workspaces/Resources/Private/Language/locallang.xlf index 6afc8f35dc23d8f23757751c4f6d6f03c38e0e33..375a9c2e57df3961803fb818f7424fe325093e3c 100644 --- a/typo3/sysext/workspaces/Resources/Private/Language/locallang.xlf +++ b/typo3/sysext/workspaces/Resources/Private/Language/locallang.xlf @@ -384,6 +384,15 @@ <trans-unit id="workingTable.stages" resname="workingTable.stages"> <source>Stages</source> </trans-unit> + <trans-unit id="status.has_changes" resname="status.has_changes"> + <source>Record was changed in this workspace</source> + </trans-unit> + <trans-unit id="status.contains_changes" resname="status.contains_changes"> + <source>Record contains version records</source> + </trans-unit> + <trans-unit id="status.is_new" resname="status.is_new"> + <source>Record was created in this workspace</source> + </trans-unit> </body> </file> </xliff> diff --git a/typo3/sysext/workspaces/Tests/Functional/EventListener/PageTreeItemsHighlighterTest.php b/typo3/sysext/workspaces/Tests/Functional/EventListener/PageTreeItemsHighlighterTest.php index 46a91c33101de45cddf5706d6c55bfedf46b57a1..a64423d5d5ec8141c2d602baf23b4322ca0ddb3a 100644 --- a/typo3/sysext/workspaces/Tests/Functional/EventListener/PageTreeItemsHighlighterTest.php +++ b/typo3/sysext/workspaces/Tests/Functional/EventListener/PageTreeItemsHighlighterTest.php @@ -18,11 +18,14 @@ declare(strict_types=1); namespace TYPO3\CMS\Workspaces\Tests\Functional\EventListener; use TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent; +use TYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\WorkspaceAspect; use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Http\Uri; +use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Core\Localization\LanguageServiceFactory; +use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Workspaces\EventListener\PageTreeItemsHighlighter; use TYPO3\CMS\Workspaces\Service\WorkspaceService; @@ -43,7 +46,7 @@ final class PageTreeItemsHighlighterTest extends FunctionalTestCase /** * @test */ - public function classesAreAppliedToPageItems(): void + public function statusInformationAddedToPageItems(): void { $this->setWorkspaceId(91); $this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv'); @@ -130,13 +133,33 @@ final class PageTreeItemsHighlighterTest extends FunctionalTestCase (new PageTreeItemsHighlighter(new WorkspaceService()))($afterPageTreeItemsPreparedEvent); $expected = $input; - $expected[1]['class'] = 'ver-versions'; - $expected[4]['class'] = 'ver-element ver-versions'; - $expected[6]['class'] = 'ver-element ver-versions'; + $expected[1]['statusInformation'] = [ + new StatusInformation( + label: $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:status.contains_changes'), + severity: ContextualFeedbackSeverity::WARNING + ), + ]; + $expected[4]['statusInformation'] = [ + new StatusInformation( + label: $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:status.has_changes'), + severity: ContextualFeedbackSeverity::WARNING + ), + ]; + $expected[6]['statusInformation'] = [ + new StatusInformation( + label: $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:status.is_new'), + severity: ContextualFeedbackSeverity::WARNING + ), + ]; self::assertEquals($expected, $afterPageTreeItemsPreparedEvent->getItems()); } + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + protected function setWorkspaceId(int $workspaceId): void { $GLOBALS['BE_USER']->workspace = $workspaceId;