From 6336e594b97cf745a4ac95541421cce68e9764a2 Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Thu, 26 Aug 2021 00:37:45 +0200
Subject: [PATCH] [TASK] Move metadata translation to action button in filelist

Creating and editing file metadata translations is usually
done in the filelist module. Therefore a custom toggle
button in the "Localization" column had to be used.

In the expanded state, various flag icons with an hard
to guess overlay icon were displayed. Those could be
used to either create or edit the file metadata records.

This however had some drawbacks:

- The toggle button was not accessible by keyboard
- As toggle icon, the "pages languages overlay" icon
  was used, making it hard to understand for editors
- As soon as one of those toggle buttons was clicked,
  all other toggle buttons jumped to the left

To improve this, the toggle button is moved into the
controls group and does now uses the "actions-translate"
icon. It furthermore does now open a dropdown with
proper link labels - as already known from the secondary
menu. This therefore also increases consistency.

Besides the visual changes, this patch also cleans
up the underlying code and removes the custom
JavaScript component, which was used for the
toggle button.

Resolves: #94997
Releases: master
Change-Id: I898e234d35b125ba37ac6c30874fa206d2515184
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70759
Tested-by: core-ci <typo3@b13.com>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Jochen <rothjochen@gmail.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Jochen <rothjochen@gmail.com>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
---
 .../Public/TypeScript/FileListLocalisation.ts |  32 ----
 .../Application/FileList/AbstractFileCest.php |   2 +-
 .../Classes/Controller/FileListController.php |   1 -
 typo3/sysext/filelist/Classes/FileList.php    | 158 +++++++++++-------
 .../Public/JavaScript/FileListLocalisation.js |  13 --
 5 files changed, 98 insertions(+), 108 deletions(-)
 delete mode 100644 Build/Sources/TypeScript/filelist/Resources/Public/TypeScript/FileListLocalisation.ts
 delete mode 100644 typo3/sysext/filelist/Resources/Public/JavaScript/FileListLocalisation.js

diff --git a/Build/Sources/TypeScript/filelist/Resources/Public/TypeScript/FileListLocalisation.ts b/Build/Sources/TypeScript/filelist/Resources/Public/TypeScript/FileListLocalisation.ts
deleted file mode 100644
index 355df1365898..000000000000
--- a/Build/Sources/TypeScript/filelist/Resources/Public/TypeScript/FileListLocalisation.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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!
- */
-
-import DocumentService = require('TYPO3/CMS/Core/DocumentService');
-import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
-
-/**
- * Module: TYPO3/CMS/Filelist/FileListLocalisation
- * @exports TYPO3/CMS/Filelist/FileListLocalisation
- */
-class FileListLocalisation {
-  constructor() {
-    DocumentService.ready().then((): void => {
-      new RegularEvent('click', (event: Event, target: HTMLElement): void => {
-        const id = target.dataset.fileid;
-        document.querySelector('div[data-fileid="' + id + '"]').classList.toggle('hidden');
-      }).delegateTo(document, 'a.filelist-translationToggler');
-    });
-  }
-}
-
-export = new FileListLocalisation();
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/FileList/AbstractFileCest.php b/typo3/sysext/core/Tests/Acceptance/Application/FileList/AbstractFileCest.php
index ab159ea96da3..11eef7e84b23 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/FileList/AbstractFileCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/FileList/AbstractFileCest.php
@@ -80,7 +80,7 @@ abstract class AbstractFileCest
             function (RemoteWebDriver $webDriver) use ($title) {
                 return $webDriver->findElement(
                     \Facebook\WebDriver\WebDriverBy::xpath(
-                        '//a[contains(text(),"' . $title . '")]/parent::node()/parent::node()//a[@data-bs-toggle="dropdown"]'
+                        '//a[contains(text(),"' . $title . '")]/parent::node()/parent::node()//a[contains(@href, "#actions_") and contains(@data-bs-toggle, "dropdown")]'
                     )
                 );
             }
diff --git a/typo3/sysext/filelist/Classes/Controller/FileListController.php b/typo3/sysext/filelist/Classes/Controller/FileListController.php
index 93520758bf26..fd1c1eb3d55c 100644
--- a/typo3/sysext/filelist/Classes/Controller/FileListController.php
+++ b/typo3/sysext/filelist/Classes/Controller/FileListController.php
@@ -265,7 +265,6 @@ class FileListController implements LoggerAwareInterface
         $this->view->assign('currentIdentifier', $this->folderObject ? $this->folderObject->getCombinedIdentifier() : '');
 
         // @todo: These modules should be merged into one module
-        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileListLocalisation');
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileList');
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileDelete');
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
diff --git a/typo3/sysext/filelist/Classes/FileList.php b/typo3/sysext/filelist/Classes/FileList.php
index 459fcf7aff35..c621195c11e5 100644
--- a/typo3/sysext/filelist/Classes/FileList.php
+++ b/typo3/sysext/filelist/Classes/FileList.php
@@ -140,7 +140,6 @@ class FileList
         '_SELECTOR_' => 'col-selector',
         'icon' => 'col-icon',
         'name' => 'col-title col-responsive',
-        '_LOCALIZATION_' => 'col-localizationa',
     ];
 
     /**
@@ -225,7 +224,7 @@ class FileList
         $this->sortRev = $sortRev;
         $this->firstElementNumber = $pointer;
         $this->fieldArray = [
-            '_SELECTOR_', 'icon', 'name', '_LOCALIZATION_', '_CONTROL_', 'record_type', 'size', 'rw', '_REF_'
+            '_SELECTOR_', 'icon', 'name', '_CONTROL_', 'record_type', 'size', 'rw', '_REF_'
         ];
     }
 
@@ -622,17 +621,6 @@ class FileList
         return $this->selectedElements;
     }
 
-    protected function getAvailableSystemLanguages(): array
-    {
-        // first two keys are "0" (default) and "-1" (multiple), after that comes the "other languages"
-        $allSystemLanguages = $this->translateTools->getSystemLanguages();
-        return array_filter($allSystemLanguages, function ($languageRecord) {
-            if ($languageRecord['uid'] === -1 || $languageRecord['uid'] === 0 || !$this->getBackendUser()->checkLanguageAccess($languageRecord['uid'])) {
-                return false;
-            }
-            return true;
-        });
-    }
     /**
      * This returns tablerows for the files in the array $items['sorting'].
      *
@@ -642,7 +630,6 @@ class FileList
     public function formatFileList(array $files)
     {
         $out = '';
-        $systemLanguages = $this->getAvailableSystemLanguages();
         foreach ($files as $fileObject) {
             // Initialization
             $this->counter++;
@@ -684,52 +671,6 @@ class FileList
                     case '_SELECTOR_':
                         $theData[$field] = $this->makeCheckbox($fileObject);
                         break;
-                    case '_LOCALIZATION_':
-                        if (!empty($systemLanguages) && $fileObject->isIndexed() && $fileObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata') && !empty($GLOBALS['TCA']['sys_file_metadata']['ctrl']['languageField'] ?? null)) {
-                            $metaDataRecord = $fileObject->getMetaData()->get();
-                            $translations = $this->getTranslationsForMetaData($metaDataRecord);
-                            $languageCode = '';
-
-                            foreach ($systemLanguages as $language) {
-                                $languageId = $language['uid'];
-                                $flagIcon = $language['flagIcon'];
-                                if (array_key_exists($languageId, $translations)) {
-                                    $title = htmlspecialchars(sprintf($this->getLanguageService()->getLL('editMetadataForLanguage'), $language['title']));
-                                    $urlParameters = [
-                                        'edit' => [
-                                            'sys_file_metadata' => [
-                                                $translations[$languageId]['uid'] => 'edit'
-                                            ]
-                                        ],
-                                        'returnUrl' => $this->listURL()
-                                    ];
-                                    $flagButtonIcon = $this->iconFactory->getIcon($flagIcon, Icon::SIZE_SMALL, 'overlay-edit')->render();
-                                    $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
-                                    $languageCode .= '<a href="' . htmlspecialchars($url) . '" class="btn btn-default" title="' . $title . '">'
-                                        . $flagButtonIcon . '</a>';
-                                } elseif ($metaDataRecord['uid'] ?? false) {
-                                    $parameters = [
-                                        'justLocalized' => 'sys_file_metadata:' . $metaDataRecord['uid'] . ':' . $languageId,
-                                        'returnUrl' => $this->listURL()
-                                    ];
-                                    $href = BackendUtility::getLinkToDataHandlerAction(
-                                        '&cmd[sys_file_metadata][' . $metaDataRecord['uid'] . '][localize]=' . $languageId,
-                                        (string)$this->uriBuilder->buildUriFromRoute('record_edit', $parameters)
-                                    );
-                                    $flagButtonIcon = '<span title="' . htmlspecialchars(sprintf($this->getLanguageService()->getLL('createMetadataForLanguage'), $language['title'])) . '">' . $this->iconFactory->getIcon($flagIcon, Icon::SIZE_SMALL, 'overlay-new')->render() . '</span>';
-                                    $languageCode .= '<a href="' . htmlspecialchars($href) . '" class="btn btn-default">' . $flagButtonIcon . '</a> ';
-                                }
-                            }
-
-                            // Hide flag button bar when not translated yet
-                            $theData[$field] = ' <div class="localisationData btn-group' . (empty($translations) ? ' hidden' : '') . '" data-fileid="' . $fileUid . '">'
-                                . $languageCode . '</div>';
-                            $theData[$field] .= '<a class="btn btn-default filelist-translationToggler" data-fileid="' . $fileUid . '">' .
-                                '<span title="' . htmlspecialchars($this->getLanguageService()->getLL('translateMetadata')) . '">'
-                                . $this->iconFactory->getIcon('mimetypes-x-content-page-language-overlay', Icon::SIZE_SMALL)->render() . '</span>'
-                                . '</a>';
-                        }
-                        break;
                     case '_REF_':
                         $theData[$field] = $this->makeRef($fileObject);
                         break;
@@ -1018,6 +959,11 @@ class FileList
             $cells['metadata'] = '<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . $title . '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
         }
 
+        // Get translation actions
+        if ($fileOrFolderObject instanceof File && ($translations = $this->makeTranslations($fileOrFolderObject))) {
+            $cells['translations'] = $translations;
+        }
+
         // document view
         if ($fileOrFolderObject instanceof File) {
             $fileUrl = $fileOrFolderObject->getPublicUrl();
@@ -1134,7 +1080,7 @@ class FileList
         $cellOutput = '';
         $output = '';
         foreach ($cells as $key => $action) {
-            if (in_array($key, ['view', 'metadata', 'delete'])) {
+            if (in_array($key, ['view', 'metadata', 'translations', 'delete'])) {
                 $output .= $action;
                 continue;
             }
@@ -1228,6 +1174,96 @@ class FileList
         return htmlspecialchars($folder->$method());
     }
 
+    /**
+     * Creates the file metadata translation dropdown. Each item links
+     * to the corresponding metadata translation, while depending on
+     * the current state, either a new translation can be created or
+     * an existing translation can be edited.
+     *
+     * @param File $file
+     * @return string
+     */
+    protected function makeTranslations(File $file): string
+    {
+        $backendUser = $this->getBackendUser();
+
+        // Fetch all system languages except "default (0)" and "all languages (-1)"
+        $systemLanguages = array_filter(
+            $this->translateTools->getSystemLanguages(),
+            static fn (array $languageRecord): bool => $languageRecord['uid'] > 0 && $backendUser->checkLanguageAccess($languageRecord['uid'])
+        );
+
+        if ($systemLanguages === []
+            || !($GLOBALS['TCA']['sys_file_metadata']['ctrl']['languageField'] ?? false)
+            || !$file->isIndexed()
+            || !$file->checkActionPermission('editMeta')
+            || !$backendUser->check('tables_modify', 'sys_file_metadata')
+        ) {
+            // Early return in case no system languages exists or metadata
+            // of this file can not be created / edited by the current user.
+            return '';
+        }
+
+        $translations = [];
+        $metaDataRecord = $file->getMetaData()->get();
+        $existingTranslations = $this->getTranslationsForMetaData($metaDataRecord);
+
+        foreach ($systemLanguages as $languageId => $language) {
+            if (!isset($existingTranslations[$languageId]) && !($metaDataRecord['uid'] ?? false)) {
+                // Skip if neither a translation nor the metadata uid exists
+                continue;
+            }
+
+            if (isset($existingTranslations[$languageId])) {
+                // Set options for edit action of an existing translation
+                $title = sprintf($this->getLanguageService()->getLL('editMetadataForLanguage'), $language['title']);
+                $actionType = 'edit';
+                $url = (string)$this->uriBuilder->buildUriFromRoute(
+                    'record_edit',
+                    [
+                        'edit' => [
+                            'sys_file_metadata' => [
+                                $existingTranslations[$languageId]['uid'] => 'edit'
+                            ]
+                        ],
+                        'returnUrl' => $this->listURL()
+                    ]
+                );
+            } else {
+                // Set options for "create new" action of a new translation
+                $title = sprintf($this->getLanguageService()->getLL('createMetadataForLanguage'), $language['title']);
+                $actionType = 'new';
+                $url = BackendUtility::getLinkToDataHandlerAction(
+                    '&cmd[sys_file_metadata][' . $metaDataRecord['uid'] . '][localize]=' . $languageId,
+                    (string)$this->uriBuilder->buildUriFromRoute(
+                        'record_edit',
+                        [
+                            'justLocalized' => 'sys_file_metadata:' . $metaDataRecord['uid'] . ':' . $languageId,
+                            'returnUrl' => $this->listURL()
+                        ]
+                    )
+                );
+            }
+
+            $translations[] = '
+                <li>
+                    <a href="' . htmlspecialchars($url) . '" class="dropdown-item" title="' . htmlspecialchars($title) . '">
+                        ' . $this->iconFactory->getIcon($language['flagIcon'], Icon::SIZE_SMALL, 'overlay-' . $actionType)->render() . ' ' . htmlspecialchars($title) . '
+                    </a>
+                </li>';
+        }
+
+        return $translations !== [] ? '
+            <div class="btn-group dropdown position-static">
+                <button class="btn btn-default dropdown-toggle dropdown-toggle-no-chevron" type="button" id="translations_' . $file->getHashedIdentifier() . '" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">
+                    ' . $this->iconFactory->getIcon('actions-translate', Icon::SIZE_SMALL)->render() . '
+                </button>
+                <ul  class="dropdown-menu dropdown-list" aria-labelledby="translations_' . $file->getHashedIdentifier() . '">
+                    ' . implode(LF, $translations) . '
+                </ul>
+            </div>' : '';
+    }
+
     /**
      * Generates HTML code for a Reference tooltip out of
      * sys_refindex records you hand over
diff --git a/typo3/sysext/filelist/Resources/Public/JavaScript/FileListLocalisation.js b/typo3/sysext/filelist/Resources/Public/JavaScript/FileListLocalisation.js
deleted file mode 100644
index ea5da4f23409..000000000000
--- a/typo3/sysext/filelist/Resources/Public/JavaScript/FileListLocalisation.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * 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!
- */
-define(["require","exports","TYPO3/CMS/Core/DocumentService","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,n,r){"use strict";return new class{constructor(){n.ready().then(()=>{new r("click",(e,t)=>{const n=t.dataset.fileid;document.querySelector('div[data-fileid="'+n+'"]').classList.toggle("hidden")}).delegateTo(document,"a.filelist-translationToggler")})}}}));
\ No newline at end of file
-- 
GitLab