Skip to content

Commit

Permalink
[FEATURE] Improve Usability in Workspaces module
Browse files Browse the repository at this point in the history
The workspaces module has a better style and some
improvements for editors and administrators:

* Editors now get feedback if an AJAX call shows no results
* Editors + Administrators now switch workspace via a selector,
  very helpful when having more than a couple of workspaces
* Administrators can now edit a workspace record
  directly from the module

The change cleans up a lot of unused code in the main workspaces
module and brings a few UX improvements within the module.

* Dropdowns instead of tabs (good when having a lot of workspaces)
* Language + Depth + Action selection is now rendered via
  Controller+Action instead of waiting for a first AJAX round trip
* Properly using "moduleData" from "uc" to store information
* Solved issues related to language icon rendering
* Removed unused inline settings
* Consistent usage of Persistent JS module accessing BE_Users' UC
* nProgress for showing progress of loading AJAX requests

Next steps in this area:
* Hand in the first payload as JSON to avoid AJAX call on
  initial page load
* Remove leftover inline JavaScript

Resolves: #94819
Releases: master
Change-Id: Ie533656a14af56dad4a4039fcbc9b08bde693500
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70452
Tested-by: core-ci <typo3@b13.com>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
  • Loading branch information
bmack authored and lolli42 committed Aug 12, 2021
1 parent b906f76 commit cc0c62e
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 344 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@

import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import $ from 'jquery';
import 'nprogress';
import {SeverityEnum} from 'TYPO3/CMS/Backend/Enum/Severity';
import 'TYPO3/CMS/Backend/Input/Clearable';
import Workspaces from './Workspaces';
import Modal = require('TYPO3/CMS/Backend/Modal');
import Persistent = require('TYPO3/CMS/Backend/Storage/Persistent');
import Tooltip = require('TYPO3/CMS/Backend/Tooltip');
import Utility = require('TYPO3/CMS/Backend/Utility');
import Viewport = require('TYPO3/CMS/Backend/Viewport');
import Wizard = require('TYPO3/CMS/Backend/Wizard');
import SecurityUtility = require('TYPO3/CMS/Core/SecurityUtility');
import windowManager = require('TYPO3/CMS/Backend/WindowManager');
Expand All @@ -36,6 +34,8 @@ enum Identifiers {
chooseSelectionAction = '#workspace-actions-form [name="selection-action"]',
chooseMassAction = '#workspace-actions-form [name="mass-action"]',
container = '#workspace-panel',
contentsContainer = '#workspace-contents',
noContentsContainer = '#workspace-contents-empty',
actionIcons = '#workspace-action-icons',
toggleAll = '.t3js-toggle-all',
previewLinksButton = '.t3js-preview-link',
Expand All @@ -51,7 +51,8 @@ class Backend extends Workspaces {
private settings: { [key: string]: string | number } = {
dir: 'ASC',
id: TYPO3.settings.Workspaces.id,
language: TYPO3.settings.Workspaces.language,
depth: 1,
language: 'all',
limit: 30,
query: '',
sort: 'label_Live',
Expand Down Expand Up @@ -193,30 +194,50 @@ class Backend extends Workspaces {
super();

$((): void => {
let persistedDepth;
this.getElements();
this.registerEvents();
this.notifyWorkspaceSwitchAction();

if (Persistent.isset('this.Module.depth')) {
persistedDepth = Persistent.get('this.Module.depth');
this.elements.$depthSelector.val(persistedDepth);
this.settings.depth = persistedDepth;
} else {
this.settings.depth = TYPO3.settings.Workspaces.depth;
}

this.loadWorkspaceComponents();
// Set the depth from the main element
this.settings.depth = this.elements.$depthSelector.val();
this.settings.language = this.elements.$languageSelector.val();
this.getWorkspaceInfos();
});
}

private notifyWorkspaceSwitchAction(): void {
const mainElement = document.querySelector('main[data-workspace-switch-action]') as HTMLElement;
if (mainElement.dataset.workspaceSwitchAction) {
const workspaceSwitchInformation = JSON.parse(mainElement.dataset.workspaceSwitchAction);
// we need to do this manually, but this should be done better via proper events
top.TYPO3.WorkspacesMenu.performWorkspaceSwitch(workspaceSwitchInformation.id, workspaceSwitchInformation.title);
top.document.dispatchEvent(new CustomEvent('typo3:pagetree:refresh'));
top.TYPO3.ModuleMenu.App.refreshMenu();
}
}

/**
* Checks the integrity of a record
*
* @param {Array} payload
* @return {$}
*/
private checkIntegrity(payload: object): Promise<AjaxResponse> {
return this.sendRemoteRequest(
this.generateRemotePayload('checkIntegrity', payload),
);
}

private getElements(): void {
this.elements.$searchForm = $(Identifiers.searchForm);
this.elements.$searchTextField = $(Identifiers.searchTextField);
this.elements.$searchSubmitBtn = $(Identifiers.searchSubmitBtn);
this.elements.$depthSelector = $(Identifiers.depthSelector);
this.elements.$languageSelector = $(Identifiers.languageSelector);
this.elements.$container = $(Identifiers.container);
this.elements.$tableBody = this.elements.$container.find('tbody');
this.elements.$contentsContainer = $(Identifiers.contentsContainer);
this.elements.$noContentsContainer = $(Identifiers.noContentsContainer);
this.elements.$tableBody = this.elements.$contentsContainer.find('tbody');
this.elements.$actionIcons = $(Identifiers.actionIcons);
this.elements.$toggleAll = $(Identifiers.toggleAll);
this.elements.$chooseStageAction = $(Identifiers.chooseStageAction);
Expand Down Expand Up @@ -347,7 +368,7 @@ class Backend extends Workspaces {
// Listen for depth changes
this.elements.$depthSelector.on('change', (e: JQueryEventObject): void => {
const depth = (<HTMLSelectElement>e.target).value;
Persistent.set('this.Module.depth', depth);
Persistent.set('moduleData.workspaces.settings.depth', depth);
this.settings.depth = depth;
this.getWorkspaceInfos();
});
Expand All @@ -358,14 +379,14 @@ class Backend extends Workspaces {
// Listen for language changes
this.elements.$languageSelector.on('change', (e: JQueryEventObject): void => {
const $me = $(e.target);
Persistent.set('moduleData.workspaces.settings.language', $me.val());
this.settings.language = $me.val();

this.sendRemoteRequest([
this.generateRemoteActionsPayload('saveLanguageSelection', [$me.val()]),
this.sendRemoteRequest(
this.generateRemotePayload('getWorkspaceInfos', this.settings),
]).then((response: any): void => {
).then(async (response: AjaxResponse): Promise<void> => {
const actionResponse = await response.resolve();
this.elements.$languageSelector.prev().html($me.find(':selected').data('icon'));
this.renderWorkspaceInfos(response[1].result);
this.renderWorkspaceInfos(actionResponse[0].result);
});
});

Expand Down Expand Up @@ -489,60 +510,7 @@ class Backend extends Workspaces {
}

/**
* Loads the workspace components, like available stage actions and items of the workspace
*/
private loadWorkspaceComponents(): void {
this.sendRemoteRequest([
this.generateRemotePayload('getWorkspaceInfos', this.settings),
this.generateRemotePayload('getStageActions', {}),
this.generateRemoteMassActionsPayload('getMassStageActions', {}),
this.generateRemotePayload('getSystemLanguages', {
pageUid: this.elements.$container.data('pageUid'),
}),
]).then(async (response: AjaxResponse): Promise<void> => {
const resolvedResponse = await response.resolve();
this.elements.$depthSelector.prop('disabled', false);

// Records
this.renderWorkspaceInfos(resolvedResponse[0].result);

// Stage actions
const stageActions = resolvedResponse[1].result.data;
let i;
for (i = 0; i < stageActions.length; ++i) {
this.elements.$chooseStageAction.append(
$('<option />').val(stageActions[i].uid).text(stageActions[i].title),
);
}

// Mass actions
const massActions = resolvedResponse[2].result.data;
for (i = 0; i < massActions.length; ++i) {
this.elements.$chooseSelectionAction.append(
$('<option />').val(massActions[i].action).text(massActions[i].title),
);

this.elements.$chooseMassAction.append(
$('<option />').val(massActions[i].action).text(massActions[i].title),
);
}

// Languages
const languages = resolvedResponse[3].result.data;
for (i = 0; i < languages.length; ++i) {
const $option = $('<option />').val(languages[i].uid).text(languages[i].title).data('icon', languages[i].icon);
if (String(languages[i].uid) === String(TYPO3.settings.Workspaces.language)) {
$option.prop('selected', true);
this.elements.$languageSelector.prev().html(languages[i].icon);
}
this.elements.$languageSelector.append($option);
}
this.elements.$languageSelector.prop('disabled', false);
});
}

/**
* Gets the workspace infos
* Gets the workspace infos (= filling the contents).
*
* @return {Promise}
* @protected
Expand All @@ -556,7 +524,7 @@ class Backend extends Workspaces {
}

/**
* Renders fetched workspace informations
* Renders fetched workspace information
*
* @param {Object} result
*/
Expand All @@ -569,6 +537,15 @@ class Backend extends Workspaces {

this.buildPagination(result.total);

// disable the contents area
if (result.total === 0) {
this.elements.$contentsContainer.hide();
this.elements.$noContentsContainer.show();
} else {
this.elements.$contentsContainer.show();
this.elements.$noContentsContainer.hide();
}

for (let i = 0; i < result.data.length; ++i) {
const item = result.data[i];
const $actions = $('<div />', {class: 'btn-group'});
Expand Down Expand Up @@ -1267,6 +1244,7 @@ class Backend extends Workspaces {
private getPreRenderedIcon(identifier: string): JQuery {
return this.elements.$actionIcons.find('[data-identifier="' + identifier + '"]').clone();
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import {SeverityEnum} from 'TYPO3/CMS/Backend/Enum/Severity';
import $ from 'jquery';
import NProgress = require('nprogress');
import Modal = require('TYPO3/CMS/Backend/Modal');

export default class Workspaces {
Expand Down Expand Up @@ -118,33 +119,23 @@ export default class Workspaces {
return $modal;
}

/**
* Checks the integrity of a record
*
* @param {Array} payload
* @return {$}
*/
protected checkIntegrity(payload: object): Promise<AjaxResponse> {
return this.sendRemoteRequest(
this.generateRemotePayload('checkIntegrity', payload),
);
}

/**
* Sends an AJAX request
*
* @param {Object} payload
* @return {$}
*/
protected sendRemoteRequest(payload: object): Promise<AjaxResponse> {
NProgress.configure({ parent: '#workspace-content-wrapper', showSpinner: false });
NProgress.start();
return (new AjaxRequest(TYPO3.settings.ajaxUrls.workspace_dispatch)).post(
payload,
{
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
}
);
).finally(() => NProgress.done());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.. include:: ../../Includes.txt

============================================
Feature: #94819 - Improved Workspaces module
============================================

See :issue:`94819`

Description
===========

The workspaces module has been improved in usability:

For the initial loading of the module, the AJAX request has
to process less data as all information is already loaded with
the module.

A loading indicator is now visible during AJAX requests to
show editors that there is work in progress.

A dropdown is now used to choose between multiple workspaces,
which is especially useful when having multiple workspaces.

Administrators can edit workspace settings directly
from the module's docheader area.


Impact
======

The overall user experience has been improved and administrators
do not need to use the list module to manage workspaces anymore.

.. index:: Backend, ext:workspaces
Original file line number Diff line number Diff line change
Expand Up @@ -209,20 +209,6 @@ public function loadColumnModel()
return [];
}

/**
* Saves the selected language.
*
* @param int|string $language
*/
public function saveLanguageSelection($language)
{
if (MathUtility::canBeInterpretedAsInteger($language) === false && $language !== 'all') {
$language = 'all';
}
$this->getBackendUser()->uc['moduleData']['Workspaces'][$this->getBackendUser()->workspace]['language'] = $language;
$this->getBackendUser()->writeUC();
}

/**
* Gets the dialog window to be displayed before a record can be sent to the next stage.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@ class MassActionHandler
{
const MAX_RECORDS_TO_PROCESS = 30;

/**
* Path to the locallang file
*
* @var string
*/
private $pathToLocallang = 'LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf';

/**
* @var WorkspaceService
*/
Expand All @@ -47,33 +40,6 @@ public function __construct()
$this->workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
}

/**
* Get list of available mass workspace actions.
*
* @return array $data
*/
public function getMassStageActions()
{
$actions = [];
$currentWorkspace = $this->getCurrentWorkspace();
$backendUser = $this->getBackendUser();
$massActionsEnabled = (bool)($backendUser->getTSConfig()['options.']['workspaces.']['enableMassActions'] ?? true);
if ($massActionsEnabled) {
$publishAccess = $backendUser->workspacePublishAccess($currentWorkspace);
if ($publishAccess && !(($backendUser->workspaceRec['publish_access'] ?? 0) & 1)) {
$actions[] = ['action' => 'publish', 'title' => $this->getLanguageService()->sL($this->pathToLocallang . ':label_doaction_publish')];
}
if ($currentWorkspace !== WorkspaceService::LIVE_WORKSPACE_ID) {
$actions[] = ['action' => 'discard', 'title' => $this->getLanguageService()->sL($this->pathToLocallang . ':label_doaction_discard')];
}
}
$result = [
'total' => count($actions),
'data' => $actions
];
return $result;
}

/**
* Publishes the current workspace.
*
Expand Down Expand Up @@ -247,7 +213,7 @@ protected function validateLanguageParameter(\stdClass $parameters)
*
* @return int The current workspace ID
*/
protected function getCurrentWorkspace()
protected function getCurrentWorkspace(): int
{
return $this->workspaceService->getCurrentWorkspace();
}
Expand Down
Loading

0 comments on commit cc0c62e

Please sign in to comment.