10f44de0dd0da08c4dd04cffdf858647fee386fa
[Packages/TYPO3.CMS.git] / typo3 / sysext / workspaces / Resources / Public / JavaScript / Backend.js
1 /*
2 /*
3 * This file is part of the TYPO3 CMS project.
4 *
5 * It is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU General Public License, either version 2
7 * of the License, or any later version.
8 *
9 * For the full copyright and license information, please read the
10 * LICENSE.txt file that was distributed with this source code.
11 *
12 * The TYPO3 project - inspiring people to share!
13 */
14
15 /**
16 * RequireJS module for the workspace backend module
17 */
18 define([
19 'jquery',
20 'TYPO3/CMS/Workspaces/Workspaces',
21 'TYPO3/CMS/Backend/Tooltip',
22 'TYPO3/CMS/Backend/Severity',
23 'TYPO3/CMS/Backend/Modal',
24 'TYPO3/CMS/Backend/Wizard',
25 'nprogress',
26 'TYPO3/CMS/Backend/jquery.clearable'
27 ], function($, Workspaces, Tooltip, Severity, Modal, Wizard, NProgress) {
28 'use strict';
29
30 var Backend = {
31 workspaceTitle: '',
32 identifiers: {
33 searchForm: '#workspace-settings-form',
34 searchTextField: '#workspace-settings-form input[name="search-text"]',
35 searchSubmitBtn: '#workspace-settings-form button[type="submit"]',
36 depthSelector: '#workspace-settings-form [name="depth"]',
37 languageSelector: '#workspace-settings-form select[name="languages"]',
38 actionForm: '#workspace-actions-form',
39 chooseStageAction: '#workspace-actions-form [name="stage-action"]',
40 chooseSelectionAction: '#workspace-actions-form [name="selection-action"]',
41 chooseMassAction: '#workspace-actions-form [name="mass-action"]',
42 container: '#workspace-panel',
43 actionIcons: '#workspace-action-icons',
44 toggleAll: '.t3js-toggle-all',
45 previewLinksButton: '.t3js-preview-link',
46 pagination: '#workspace-pagination'
47 },
48 settings: {
49 depth: TYPO3.settings.Workspaces.depth,
50 dir: 'ASC',
51 id: TYPO3.settings.Workspaces.id,
52 language: TYPO3.settings.Workspaces.language,
53 limit: 30,
54 query: '',
55 sort: 'label_Live',
56 start: 0,
57 filterTxt: ''
58 },
59 paging: {
60 currentPage: 1,
61 totalPages: 1,
62 totalItems: 0
63 },
64 allToggled: false,
65 elements: {}, // filled in Backend.getElements()
66 latestPath: '',
67 markedRecordsForMassAction: [],
68 indentationPadding: 26
69 };
70
71 Backend.initialize = function() {
72 Backend.getElements();
73 Backend.registerEvents();
74
75 if (TYPO3.settings.Workspaces.depth > 0) {
76 Backend.elements.$depthSelector.val(TYPO3.settings.Workspaces.depth);
77 }
78
79 Backend.loadWorkspaceComponents();
80 };
81
82 Backend.getElements = function() {
83 Backend.elements.$searchForm = $(Backend.identifiers.searchForm);
84 Backend.elements.$searchTextField = $(Backend.identifiers.searchTextField);
85 Backend.elements.$searchSubmitBtn = $(Backend.identifiers.searchSubmitBtn);
86 Backend.elements.$depthSelector = $(Backend.identifiers.depthSelector);
87 Backend.elements.$languageSelector = $(Backend.identifiers.languageSelector);
88 Backend.elements.$container = $(Backend.identifiers.container);
89 Backend.elements.$tableBody = Backend.elements.$container.find('tbody');
90 Backend.elements.$actionIcons = $(Backend.identifiers.actionIcons);
91 Backend.elements.$toggleAll = $(Backend.identifiers.toggleAll);
92 Backend.elements.$chooseStageAction = $(Backend.identifiers.chooseStageAction);
93 Backend.elements.$chooseSelectionAction = $(Backend.identifiers.chooseSelectionAction);
94 Backend.elements.$chooseMassAction = $(Backend.identifiers.chooseMassAction);
95 Backend.elements.$previewLinksButton = $(Backend.identifiers.previewLinksButton);
96 Backend.elements.$pagination = $(Backend.identifiers.pagination);
97 };
98
99 Backend.registerEvents = function() {
100 $(document).on('click', '[data-action="swap"]', function(e) {
101 var $tr = $(e.target).closest('tr');
102 Workspaces.checkIntegrity(
103 {
104 selection: [
105 {
106 liveId: $tr.data('uid'),
107 versionId: $tr.data('t3ver_oid'),
108 table: $tr.data('table')
109 }
110 ],
111 type: 'selection'
112 }
113 ).done(function(response) {
114 if (response[0].result.result === 'warning') {
115 Backend.addIntegrityCheckWarningToWizard();
116 }
117
118 Wizard.setup.forceSelection = false;
119 Wizard.addSlide(
120 'swap-confirm',
121 'Swap',
122 TYPO3.lang['window.swap.message'],
123 Severity.info
124 );
125 Wizard.addFinalProcessingSlide(function() {
126 // We passed this slide, swap the record now
127 Workspaces.sendRemoteRequest(
128 Workspaces.generateRemoteActionsPayload('swapSingleRecord', [
129 $tr.data('table'),
130 $tr.data('t3ver_oid'),
131 $tr.data('uid')
132 ])
133 ).done(function() {
134 Wizard.dismiss();
135 Backend.getWorkspaceInfos();
136 Backend.refreshPageTree();
137 });
138 }).done(function() {
139 Wizard.show();
140 });
141 });
142 }).on('click', '[data-action="prevstage"]', function(e) {
143 Backend.sendToStage($(e.target).closest('tr'), 'prev');
144 }).on('click', '[data-action="nextstage"]', function(e) {
145 Backend.sendToStage($(e.target).closest('tr'), 'next');
146 }).on('click', '[data-action="changes"]', Backend.viewChanges
147 ).on('click', '[data-action="preview"]', Backend.openPreview
148 ).on('click', '[data-action="open"]', function(e) {
149 var $tr = $(e.target).closest('tr'),
150 newUrl = TYPO3.settings.FormEngine.moduleUrl + '&returnUrl=' + encodeURIComponent(document.location.href) + '&id=' + TYPO3.settings.Workspaces.id + '&edit[' + $tr.data('table') + '][' + $tr.data('uid') + ']=edit';
151
152 // Append workspace of record in all-workspaces view
153 if (TYPO3.settings.Workspaces.allView) {
154 newUrl += '&workspace=' + $tr.data('t3ver_wsid');
155 }
156 window.location.href = newUrl;
157 }).on('click', '[data-action="version"]', function(e) {
158 var $tr = $(e.target).closest('tr');
159 if ($tr.data('table') === 'pages') {
160 top.loadEditId($tr.data('t3ver_oid'));
161 } else {
162 top.loadEditId($tr.data('pid'));
163 }
164 }).on('click', '[data-action="remove"]', Backend.confirmDeleteRecordFromWorkspace
165 ).on('click', '[data-action="expand"]', function(e) {
166 var $me = $(this),
167 $target = Backend.elements.$tableBody.find($me.data('target')),
168 iconIdentifier;
169
170 if ($target.first().attr('aria-expanded') === 'true') {
171 iconIdentifier = 'apps-pagetree-expand';
172 } else {
173 iconIdentifier = 'apps-pagetree-collapse';
174 }
175
176 $me.html(Backend.getPreRenderedIcon(iconIdentifier));
177 });
178
179 Backend.elements.$searchForm.on('submit', function(e) {
180 e.preventDefault();
181 Backend.settings.filterTxt = Backend.elements.$searchTextField.val();
182 Backend.getWorkspaceInfos();
183 });
184
185 Backend.elements.$searchTextField.on('keyup', function() {
186 var $me = $(this);
187
188 if ($me.val() !== '') {
189 Backend.elements.$searchSubmitBtn.removeClass('disabled');
190 } else {
191 Backend.elements.$searchSubmitBtn.addClass('disabled');
192 Backend.getWorkspaceInfos();
193 }
194 }).clearable(
195 {
196 onClear: function() {
197 Backend.elements.$searchSubmitBtn.addClass('disabled');
198 Backend.settings.filterTxt = '';
199 Backend.getWorkspaceInfos();
200 }
201 }
202 );
203
204 // checkboxes in the table
205 Backend.elements.$toggleAll.on('click', function() {
206 Backend.allToggled = !Backend.allToggled;
207 Backend.elements.$tableBody.find('input[type="checkbox"]').prop('checked', Backend.allToggled).trigger('change');
208 });
209 Backend.elements.$tableBody.on('change', 'tr input[type=checkbox]', Backend.handleCheckboxChange);
210
211 // Listen for depth changes
212 Backend.elements.$depthSelector.on('change', function(e) {
213 var $me = $(this);
214 Backend.settings.depth = $me.val();
215
216 Backend.getWorkspaceInfos();
217 });
218
219 // Generate preview links
220 Backend.elements.$previewLinksButton.on('click', Backend.generatePreviewLinks);
221
222 // Listen for language changes
223 Backend.elements.$languageSelector.on('change', function(e) {
224 var $me = $(this);
225 Backend.settings.language = $me.val();
226
227 Workspaces.sendRemoteRequest([
228 Workspaces.generateRemoteActionsPayload('saveLanguageSelection', [$me.val()]),
229 Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings)
230 ]).done(function(response) {
231 Backend.elements.$languageSelector.prev().html($me.find(':selected').data('icon'));
232 Backend.renderWorkspaceInfos(response[1].result);
233 });
234 });
235
236 // Listen for actions
237 Backend.elements.$chooseStageAction.on('change', Backend.sendToSpecificStageAction);
238 Backend.elements.$chooseSelectionAction.on('change', Backend.runSelectionAction);
239 Backend.elements.$chooseMassAction.on('change', Backend.runMassAction);
240
241 // clicking an action in the paginator
242 Backend.elements.$pagination.on('click', 'a[data-action]', function(e) {
243 e.preventDefault();
244
245 var $el = $(this),
246 reload = false;
247
248 switch ($el.data('action')) {
249 case 'previous':
250 if (Backend.paging.currentPage > 1) {
251 Backend.paging.currentPage--;
252 reload = true;
253 }
254 break;
255 case 'next':
256 if (Backend.paging.currentPage < Backend.paging.totalPages) {
257 Backend.paging.currentPage++;
258 reload = true;
259 }
260 break;
261 case 'page':
262 Backend.paging.currentPage = parseInt($el.data('page'));
263 reload = true;
264 break;
265 }
266
267 if (reload) {
268 // Adjust settings
269 Backend.settings.start = Backend.settings.limit * (Backend.paging.currentPage - 1);
270 Backend.getWorkspaceInfos();
271 }
272 });
273 };
274
275 Backend.handleCheckboxChange = function(e) {
276 var $checkbox = $(this),
277 $tr = $checkbox.parents('tr'),
278 table = $tr.data('table'),
279 uid = $tr.data('uid'),
280 t3ver_oid = $tr.data('t3ver_oid'),
281 record = table + ':' + uid + ':' + t3ver_oid;
282
283 if ($checkbox.prop('checked')) {
284 Backend.markedRecordsForMassAction.push(record);
285 $tr.addClass('warning');
286 } else {
287 var index = Backend.markedRecordsForMassAction.indexOf(record);
288 if (index > -1) {
289 Backend.markedRecordsForMassAction.splice(index, 1);
290 }
291 $tr.removeClass('warning');
292 }
293
294 Backend.elements.$chooseStageAction.prop('disabled', Backend.markedRecordsForMassAction.length === 0);
295 Backend.elements.$chooseSelectionAction.prop('disabled', Backend.markedRecordsForMassAction.length === 0);
296 Backend.elements.$chooseMassAction.prop('disabled', Backend.markedRecordsForMassAction.length > 0);
297 };
298
299 /**
300 * Generates the diff view of a record
301 *
302 * @param {Object} diff
303 * @return {$}
304 */
305 Backend.generateDiffView = function(diff) {
306 var $diff = $('<div />', {class: 'diff'});
307
308 for (var i = 0; i < diff.length; ++i) {
309 $diff.append(
310 $('<div />', {class: 'diff-item'}).append(
311 $('<div />', {class: 'diff-item-title'}).text(diff[i].label),
312 $('<div />', {class: 'diff-item-result diff-item-result-inline'}).html(diff[i].content)
313 )
314 );
315 }
316 return $diff;
317 };
318
319 /**
320 * Generates the comments view of a record
321 *
322 * @param {Object} comments
323 * @return {$}
324 */
325 Backend.generateCommentView = function(comments) {
326 var $comments = $('<div />');
327
328 for (var i = 0; i < comments.length; ++i) {
329 var $panel = $('<div />', {class: 'panel panel-default'});
330
331 if (comments[i].user_comment.length > 0) {
332 $panel.append(
333 $('<div />', {class: 'panel-body'}).html(comments[i].user_comment)
334 );
335 }
336
337 $panel.append(
338 $('<div />', {class: 'panel-footer'}).append(
339 $('<span />', {class: 'label label-success'}).text(comments[i].stage_title),
340 $('<span />', {class: 'label label-info'}).text(comments[i].tstamp)
341 )
342 );
343
344 $comments.append(
345 $('<div />', {class: 'media'}).append(
346 $('<div />', {class: 'media-left text-center'}).text(comments[i].user_username).prepend(
347 $('<div />').html(comments[i].user_avatar)
348 ),
349 $('<div />', {class: 'media-body'}).append($panel)
350 )
351 );
352 }
353
354 return $comments;
355 };
356
357 /**
358 * Sends a record to a stage
359 *
360 * @param {Object} $row
361 * @param {String} direction
362 */
363 Backend.sendToStage = function($row, direction) {
364 var nextStage,
365 stageWindowAction,
366 stageExecuteAction;
367
368 if (direction === 'next') {
369 nextStage = $row.data('nextStage');
370 stageWindowAction = 'sendToNextStageWindow';
371 stageExecuteAction = 'sendToNextStageExecute';
372 } else if (direction === 'prev') {
373 nextStage = $row.data('prevStage');
374 stageWindowAction = 'sendToPrevStageWindow';
375 stageExecuteAction = 'sendToPrevStageExecute';
376 } else {
377 throw 'Invalid direction given.';
378 }
379
380 Workspaces.sendRemoteRequest(
381 Workspaces.generateRemoteActionsPayload(stageWindowAction, [
382 $row.data('uid'), $row.data('table'), $row.data('t3ver_oid')
383 ])
384 ).done(function(response) {
385 var $modal = Workspaces.renderSendToStageWindow(response);
386 $modal.on('button.clicked', function(e) {
387 if (e.target.name === 'ok') {
388 var $form = $(e.currentTarget).find('form'),
389 serializedForm = $form.serializeObject();
390
391 serializedForm.affects = {
392 table: $row.data('table'),
393 nextStage: nextStage,
394 t3ver_oid: $row.data('t3ver_oid'),
395 uid: $row.data('uid'),
396 elements: []
397 };
398
399 Workspaces.sendRemoteRequest([
400 Workspaces.generateRemoteActionsPayload(stageExecuteAction, [serializedForm]),
401 Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings)
402 ]).done(function(response) {
403 $modal.modal('hide');
404 Backend.renderWorkspaceInfos(response[1].result);
405 Backend.refreshPageTree();
406 });
407 }
408 });
409 });
410 };
411
412 /**
413 * Loads the workspace components, like available stage actions and items of the workspace
414 */
415 Backend.loadWorkspaceComponents = function() {
416 Workspaces.sendRemoteRequest([
417 Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings),
418 Workspaces.generateRemotePayload('getStageActions', {}),
419 Workspaces.generateRemoteMassActionsPayload('getMassStageActions', {}),
420 Workspaces.generateRemotePayload('getSystemLanguages', {})
421 ]).done(function(response) {
422 Backend.elements.$depthSelector.prop('disabled', false);
423
424 // Records
425 Backend.renderWorkspaceInfos(response[0].result);
426
427 // Stage actions
428 var stageActions = response[1].result.data,
429 i;
430 for (i = 0; i < stageActions.length; ++i) {
431 Backend.elements.$chooseStageAction.append(
432 $('<option />').val(stageActions[i].uid).text(stageActions[i].title)
433 );
434 }
435
436 // Mass actions
437 var massActions = response[2].result.data;
438 for (i = 0; i < massActions.length; ++i) {
439 Backend.elements.$chooseSelectionAction.append(
440 $('<option />').val(massActions[i].action).text(massActions[i].title)
441 );
442
443 Backend.elements.$chooseMassAction.append(
444 $('<option />').val(massActions[i].action).text(massActions[i].title)
445 );
446 }
447
448 // Languages
449 var languages = response[3].result.data;
450 for (i = 0; i < languages.length; ++i) {
451 var $option = $('<option />').val(languages[i].uid).text(languages[i].title).data('icon', languages[i].icon);
452 if (String(languages[i].uid) === String(TYPO3.settings.Workspaces.language)) {
453 $option.prop('selected', true);
454 Backend.elements.$languageSelector.prev().html(languages[i].icon);
455 }
456 Backend.elements.$languageSelector.append($option);
457 }
458 Backend.elements.$languageSelector.prop('disabled', false);
459 });
460 };
461
462 /**
463 * Gets the workspace infos
464 *
465 * @return {Promise}
466 * @protected
467 */
468 Backend.getWorkspaceInfos = function() {
469 Workspaces.sendRemoteRequest(
470 Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings)
471 ).done(function(response) {
472 Backend.renderWorkspaceInfos(response[0].result);
473 });
474 };
475
476 /**
477 * Renders fetched workspace informations
478 *
479 * @param {Object} result
480 */
481 Backend.renderWorkspaceInfos = function(result) {
482 Backend.elements.$tableBody.children().remove();
483 Backend.allToggled = false;
484 Backend.elements.$chooseStageAction.prop('disabled', true);
485 Backend.elements.$chooseSelectionAction.prop('disabled', true);
486 Backend.elements.$chooseMassAction.prop('disabled', result.data.length === 0);
487
488 Backend.buildPagination(result.total);
489
490 for (var i = 0; i < result.data.length; ++i) {
491 var item = result.data[i],
492 $actions = $('<div />', {class: 'btn-group'}),
493 $integrityIcon = '';
494
495 $actions.append(
496 Backend.getAction(item.Workspaces_CollectionChildren > 0 && item.Workspaces_CollectionCurrent !== '', 'expand', 'apps-pagetree-collapse').attr('title', TYPO3.lang['tooltip.swap']).attr('data-target', '[data-collection="' + item.Workspaces_CollectionCurrent + '"]').attr('data-toggle', 'collapse'),
497 $('<button />', {class: 'btn btn-default', 'data-action': 'changes', 'data-toggle': 'tooltip', title: TYPO3.lang['tooltip.showChanges']}).append(Backend.getPreRenderedIcon('actions-document-info')),
498 Backend.getAction(item.allowedAction_swap && item.Workspaces_CollectionParent === '', 'swap', 'actions-version-swap-version').attr('title', TYPO3.lang['tooltip.swap']),
499 Backend.getAction(item.allowedAction_view, 'preview', 'actions-version-workspace-preview').attr('title', TYPO3.lang['tooltip.viewElementAction']),
500 $('<button />', {class: 'btn btn-default', 'data-action': 'open', 'data-toggle': 'tooltip', title: TYPO3.lang['tooltip.editElementAction']}).append(Backend.getPreRenderedIcon('actions-open')),
501 $('<button />', {class: 'btn btn-default', 'data-action': 'version', 'data-toggle': 'tooltip', title: TYPO3.lang['tooltip.openPage']}).append(Backend.getPreRenderedIcon('actions-version-page-open')),
502 Backend.getAction(item.allowedAction_delete, 'remove', 'actions-version-document-remove').attr('title', TYPO3.lang['tooltip.discardVersion']),
503 $('<label />', {class: 'btn btn-default btn-checkbox'}).append(
504 $('<input />', {type: 'checkbox'}),
505 $('<span />', {class: 't3-icon fa'})
506 )
507 );
508
509 if (item.integrity.messages !== '') {
510 $integrityIcon = $(TYPO3.settings.Workspaces.icons[item.integrity.status]);
511 $integrityIcon
512 .attr('data-toggle', 'tooltip')
513 .attr('data-placement', 'top')
514 .attr('data-html', true)
515 .attr('title', item.integrity.messages);
516 }
517
518 if (Backend.latestPath !== item.path_Workspace) {
519 Backend.latestPath = item.path_Workspace;
520 Backend.elements.$tableBody.append(
521 $('<tr />').append(
522 $('<th />', {colspan: 6}).text(Backend.latestPath)
523 )
524 );
525 }
526
527 var rowConfiguration = {
528 'data-uid': item.uid,
529 'data-pid': item.livepid,
530 'data-t3ver_oid': item.t3ver_oid,
531 'data-t3ver_wsid': item.t3ver_wsid,
532 'data-table': item.table,
533 'data-next-stage': item.value_nextStage,
534 'data-prev-stage': item.value_prevStage,
535 'data-stage': item.stage
536 };
537
538 if (item.Workspaces_CollectionParent !== '') {
539 rowConfiguration['data-collection'] = item.Workspaces_CollectionParent;
540 rowConfiguration['class'] = 'collapse';
541 }
542
543 Backend.elements.$tableBody.append(
544 $('<tr />', rowConfiguration).append(
545 $('<td />', {class: 't3js-title-workspace', style: item.Workspaces_CollectionLevel > 0 ? 'padding-left: ' + Backend.indentationPadding * item.Workspaces_CollectionLevel + 'px' : ''}).html(item.icon_Workspace + '&nbsp;' + '<a href="#" data-action="changes"><span class="item-state-' + item.state_Workspace + '">' + item.label_Workspace + '</span></a>'),
546 $('<td />', {class: 't3js-title-live'}).html(item.icon_Live + '&nbsp;' + item.label_Live),
547 $('<td />').text(item.label_Stage),
548 $('<td />').html($integrityIcon),
549 $('<td />').html(item.language.icon),
550 $('<td />', {class: 'text-right', nowrap: 'nowrap'}).append($actions)
551 )
552 );
553
554 Tooltip.initialize('[data-toggle="tooltip"]', {
555 delay: {
556 show: 500,
557 hide: 100
558 },
559 trigger: 'hover',
560 container: 'body'
561 });
562 }
563 };
564
565 /**
566 * Renders the pagination
567 *
568 * @param {Number} totalItems
569 */
570 Backend.buildPagination = function(totalItems) {
571 if (totalItems === 0) {
572 Backend.elements.$pagination.contents().remove();
573 return;
574 }
575
576 Backend.paging.totalItems = totalItems;
577 Backend.paging.totalPages = Math.ceil(totalItems / Backend.settings.limit);
578
579 if (Backend.paging.totalPages === 1) {
580 // early abort if only one page is available
581 Backend.elements.$pagination.contents().remove();
582 return;
583 }
584
585 var $ul = $('<ul />', {class: 'pagination pagination-block'}),
586 liElements = [],
587 $controlFirstPage = $('<li />').append(
588 $('<a />', {'data-action': 'previous'}).append(
589 $('<span />', {class: 't3-icon fa fa-arrow-left'})
590 )
591 ),
592 $controlLastPage = $('<li />').append(
593 $('<a />', {'data-action': 'next'}).append(
594 $('<span />', {class: 't3-icon fa fa-arrow-right'})
595 )
596 );
597
598 if (Backend.paging.currentPage === 1) {
599 $controlFirstPage.disablePagingAction();
600 }
601
602 if (Backend.paging.currentPage === Backend.paging.totalPages) {
603 $controlLastPage.disablePagingAction();
604 }
605
606 for (var i = 1; i <= Backend.paging.totalPages; i++) {
607 var $li = $('<li />', {class: Backend.paging.currentPage === i ? 'active' : ''});
608 $li.append(
609 $('<a />', {'data-action': 'page', 'data-page': i}).append(
610 $('<span />').text(i)
611 )
612 );
613 liElements.push($li);
614 }
615
616 $ul.append($controlFirstPage, liElements, $controlLastPage);
617 Backend.elements.$pagination.html($ul);
618 };
619
620 /**
621 * View changes of a record
622 *
623 * @param {Event} e
624 */
625 Backend.viewChanges = function(e) {
626 e.preventDefault();
627
628 var $tr = $(e.target).closest('tr');
629
630 Workspaces.sendRemoteRequest(
631 Workspaces.generateRemotePayload('getRowDetails', {
632 stage: $tr.data('stage'),
633 t3ver_oid: $tr.data('t3ver_oid'),
634 table: $tr.data('table'),
635 uid: $tr.data('uid')
636 })
637 ).done(function(response) {
638 var item = response[0].result.data[0],
639 $content = $('<div />'),
640 $tabsNav = $('<ul />', {class: 'nav nav-tabs', role: 'tablist'}),
641 $tabsContent = $('<div />', {class: 'tab-content'}),
642 modalButtons = [];
643
644 $content.append(
645 $('<p />').html(TYPO3.lang['path'].replace('{0}', item.path_Live)),
646 $('<p />').html(TYPO3.lang['current_step'].replace('{0}', item.label_Stage).replace('{1}', item.stage_position).replace('{2}', item.stage_count))
647 );
648
649 if (item.diff.length > 0) {
650 $tabsNav.append(
651 $('<li />', {role: 'presentation'}).append(
652 $('<a />', {href: '#workspace-changes', 'aria-controls': 'workspace-changes', role: 'tab', 'data-toggle': 'tab'}).text(TYPO3.lang['window.recordChanges.tabs.changeSummary'])
653 )
654 );
655 $tabsContent.append(
656 $('<div />', {role: 'tabpanel', class: 'tab-pane', id: 'workspace-changes'}).append(
657 $('<div />', {class: 'form-section'}).append(
658 Backend.generateDiffView(item.diff)
659 )
660 )
661 );
662 }
663
664 if (item.comments.length > 0) {
665 $tabsNav.append(
666 $('<li />', {role: 'presentation'}).append(
667 $('<a />', {href: '#workspace-comments', 'aria-controls': 'workspace-comments', role: 'tab', 'data-toggle': 'tab'}).html(TYPO3.lang['window.recordChanges.tabs.comments'] + '&nbsp;').append(
668 $('<span />', {class: 'badge'}).text(item.comments.length)
669 )
670 )
671 );
672 $tabsContent.append(
673 $('<div />', {role: 'tabpanel', class: 'tab-pane', id: 'workspace-comments'}).append(
674 $('<div />', {class: 'form-section'}).append(
675 Backend.generateCommentView(item.comments)
676 )
677 )
678 );
679 }
680
681 if (item.history.total > 0) {
682 $tabsNav.append(
683 $('<li />', {role: 'presentation'}).append(
684 $('<a />', {href: '#workspace-history', 'aria-controls': 'workspace-history', role: 'tab', 'data-toggle': 'tab'}).text(TYPO3.lang['window.recordChanges.tabs.history'])
685 )
686 );
687
688 $tabsContent.append(
689 $('<div />', {role: 'tabpanel', class: 'tab-pane', id: 'workspace-history'}).append(
690 $('<div />', {class: 'form-section'}).append(
691 Backend.generateHistoryView(item.history.data)
692 )
693 )
694 );
695 }
696
697 // Mark the first tab and pane as active
698 $tabsNav.find('li').first().addClass('active');
699 $tabsContent.find('.tab-pane').first().addClass('active');
700
701 // Attach tabs
702 $content.append(
703 $('<div />').append(
704 $tabsNav,
705 $tabsContent
706 )
707 );
708
709 if ($tr.data('stage') !== $tr.data('prevStage')) {
710 modalButtons.push({
711 text: item.label_PrevStage.title,
712 active: true,
713 btnClass: 'btn-default',
714 name: 'prevstage',
715 trigger: function () {
716 Modal.currentModal.trigger('modal-dismiss');
717 Backend.sendToStage($(e.target).closest('tr'), 'prev');
718 }
719 });
720 }
721
722 modalButtons.push({
723 text: item.label_NextStage.title,
724 active: true,
725 btnClass: 'btn-default',
726 name: 'nextstage',
727 trigger: function () {
728 Modal.currentModal.trigger('modal-dismiss');
729 Backend.sendToStage($(e.target).closest('tr'), 'next');
730 }
731 });
732 modalButtons.push({
733 text: TYPO3.lang['close'],
734 active: true,
735 btnClass: 'btn-info',
736 name: 'cancel',
737 trigger: function () {
738 Modal.currentModal.trigger('modal-dismiss');
739 }
740 });
741
742 Modal.show(
743 TYPO3.lang['window.recordInformation'].replace('{0}', $.trim($tr.find('.t3js-title-live').text())),
744 $content,
745 Severity.info,
746 modalButtons
747 );
748 });
749 };
750
751 /**
752 * Opens a record in a preview window
753 *
754 * @param {Event} e
755 */
756 Backend.openPreview = function(e) {
757 var $tr = $(e.target).closest('tr');
758
759 Workspaces.sendRemoteRequest(
760 Workspaces.generateRemoteActionsPayload('viewSingleRecord', [
761 $tr.data('table'), $tr.data('uid')
762 ])
763 ).done(function(response) {
764 eval(response[0].result);
765 });
766 };
767
768 /**
769 * Renders the record's history
770 *
771 * @param {Object} data
772 */
773 Backend.generateHistoryView = function(data) {
774 var $history = $('<div />');
775
776 for (var i = 0; i < data.length; ++i) {
777 var $panel = $('<div />', {class: 'panel panel-default'}),
778 $diff;
779
780 if (typeof data[i].differences === 'object') {
781 if (data[i].differences.length === 0) {
782 // Somehow here are no differences. What a pity, skip that record
783 continue;
784 }
785 $diff = $('<div />', {class: 'diff'});
786
787 for (var j = 0; j < data[i].differences.length; ++j) {
788 $diff.append(
789 $('<div />', {class: 'diff-item'}).append(
790 $('<div />', {class: 'diff-item-title'}).text(data[i].differences[j].label),
791 $('<div />', {class: 'diff-item-result diff-item-result-inline'}).html(data[i].differences[j].html)
792 )
793 );
794 }
795
796 $panel.append(
797 $('<div />').append($diff)
798 );
799 } else {
800 $panel.append(
801 $('<div />', {class: 'panel-body'}).text(data[i].differences)
802 );
803 }
804 $panel.append(
805 $('<div />', {class: 'panel-footer'}).append(
806 $('<span />', {class: 'label label-info'}).text(data[i].datetime)
807 )
808 );
809
810 $history.append(
811 $('<div />', {class: 'media'}).append(
812 $('<div />', {class: 'media-left text-center'}).text(data[i].user).prepend(
813 $('<div />').html(data[i].user_avatar)
814 ),
815 $('<div />', {class: 'media-body'}).append($panel)
816 )
817 );
818 }
819
820 return $history;
821 };
822
823 /**
824 * Shows a confirmation modal and deletes the selected record from workspace.
825 *
826 * @param {Event} e
827 */
828 Backend.confirmDeleteRecordFromWorkspace = function(e) {
829 var $tr = $(e.target).closest('tr');
830 var $modal = Modal.confirm(
831 TYPO3.lang['window.discard.title'],
832 TYPO3.lang['window.discard.message'],
833 Severity.warning,
834 [
835 {
836 text: TYPO3.lang['cancel'],
837 active: true,
838 btnClass: 'btn-default',
839 name: 'cancel',
840 trigger: function() {
841 $modal.modal('hide');
842 }
843 }, {
844 text: TYPO3.lang['ok'],
845 btnClass: 'btn-warning',
846 name: 'ok'
847 }
848 ]
849 );
850 $modal.on('button.clicked', function(e) {
851 if (e.target.name === 'ok') {
852 Workspaces.sendRemoteRequest([
853 Workspaces.generateRemoteActionsPayload('deleteSingleRecord', [
854 $tr.data('table'),
855 $tr.data('uid')
856 ])
857 ]).done(function() {
858 $modal.modal('hide');
859 Backend.getWorkspaceInfos();
860 Backend.refreshPageTree();
861 });
862 }
863 });
864 };
865
866 /**
867 * Runs a mass action
868 */
869 Backend.runSelectionAction = function() {
870 var selectedAction = Backend.elements.$chooseSelectionAction.val(),
871 integrityCheckRequired = selectedAction !== 'discard';
872
873 if (selectedAction.length === 0) {
874 // Don't do anything if that value is empty
875 return;
876 }
877
878 var affectedRecords = [];
879 for (var i = 0; i < Backend.markedRecordsForMassAction.length; ++i) {
880 var affected = Backend.markedRecordsForMassAction[i].split(':');
881 affectedRecords.push({
882 table: affected[0],
883 liveId: affected[2],
884 versionId: affected[1]
885 });
886 }
887
888 if (!integrityCheckRequired) {
889 Wizard.setup.forceSelection = false;
890 Backend.renderSelectionActionWizard(selectedAction, affectedRecords);
891 } else {
892 Workspaces.checkIntegrity(
893 {
894 selection: affectedRecords,
895 type: 'selection'
896 }
897 ).done(function(response) {
898 Wizard.setup.forceSelection = false;
899 if (response[0].result.result === 'warning') {
900 Backend.addIntegrityCheckWarningToWizard();
901 }
902 Backend.renderSelectionActionWizard(selectedAction, affectedRecords);
903 });
904 }
905 };
906
907 /**
908 * Adds a slide to the wizard concerning an integrity check warning.
909 */
910 Backend.addIntegrityCheckWarningToWizard = function() {
911 Wizard.addSlide(
912 'integrity-warning',
913 'Warning',
914 TYPO3.lang['integrity.hasIssuesDescription'] + '<br>' + TYPO3.lang['integrity.hasIssuesQuestion'],
915 Severity.warning
916 );
917 };
918
919 /**
920 * Renders the wizard for selection actions
921 *
922 * @param {String} selectedAction
923 * @param {Object} affectedRecords
924 */
925 Backend.renderSelectionActionWizard = function(selectedAction, affectedRecords) {
926 Wizard.addSlide(
927 'mass-action-confirmation',
928 TYPO3.lang['window.selectionAction.title'],
929 $('<p />').text(TYPO3.lang['tooltip.' + selectedAction + 'Selected']),
930 Severity.warning
931 );
932 Wizard.addFinalProcessingSlide(function() {
933 Workspaces.sendRemoteRequest(
934 Workspaces.generateRemoteActionsPayload('executeSelectionAction', {
935 action: selectedAction,
936 selection: affectedRecords
937 })
938 ).done(function() {
939 Backend.getWorkspaceInfos();
940 Wizard.dismiss();
941 Backend.refreshPageTree();
942 });
943 }).done(function() {
944 Wizard.show();
945
946 Wizard.getComponent().on('wizard-dismissed', function() {
947 Backend.elements.$chooseSelectionAction.val('');
948 });
949 });
950 };
951
952 /**
953 * Runs a mass action
954 */
955 Backend.runMassAction = function() {
956 var selectedAction = Backend.elements.$chooseMassAction.val(),
957 integrityCheckRequired = selectedAction !== 'discard';
958
959 if (selectedAction.length === 0) {
960 // Don't do anything if that value is empty
961 return;
962 }
963
964 if (!integrityCheckRequired) {
965 Wizard.setup.forceSelection = false;
966 Backend.renderMassActionWizard(selectedAction);
967 } else {
968 Workspaces.checkIntegrity(
969 {
970 language: Backend.settings.language,
971 type: selectedAction
972 }
973 ).done(function(response) {
974 Wizard.setup.forceSelection = false;
975 if (response[0].result.result === 'warning') {
976 Backend.addIntegrityCheckWarningToWizard();
977 }
978 Backend.renderMassActionWizard(selectedAction);
979 });
980 }
981 };
982
983 /**
984 * Renders the wizard for mass actions
985 *
986 * @param {String} selectedAction
987 */
988 Backend.renderMassActionWizard = function(selectedAction) {
989 var massAction,
990 doSwap = false;
991
992 switch (selectedAction) {
993 case 'publish':
994 massAction = 'publishWorkspace';
995 break;
996 case 'swap':
997 massAction = 'publishWorkspace';
998 doSwap = true;
999 break;
1000 case 'discard':
1001 massAction = 'flushWorkspace';
1002 break;
1003 }
1004
1005 if (massAction === null) {
1006 throw 'Invalid mass action ' + selectedAction + ' called.';
1007 }
1008
1009 Wizard.setup.forceSelection = false;
1010 Wizard.addSlide(
1011 'mass-action-confirmation',
1012 TYPO3.lang['window.massAction.title'],
1013 $('<p />').html(TYPO3.lang['tooltip.' + selectedAction + 'All'] + '<br><br>' + TYPO3.lang['tooltip.affectWholeWorkspace']),
1014 Severity.warning
1015 );
1016 Wizard.addFinalProcessingSlide(function() {
1017 Workspaces.sendRemoteRequest(
1018 Workspaces.generateRemoteMassActionsPayload(massAction, {
1019 init: true,
1020 total: 0,
1021 processed: 0,
1022 language: Backend.settings.language,
1023 swap: doSwap
1024 })
1025 ).done(function(response) {
1026 var payload = response[0].result;
1027 Workspaces.sendRemoteRequest(
1028 Workspaces.generateRemoteMassActionsPayload(massAction, payload)
1029 ).done(function() {
1030 Backend.getWorkspaceInfos();
1031 Wizard.dismiss();
1032 });
1033 });
1034 }).done(function() {
1035 Wizard.show();
1036
1037 Wizard.getComponent().on('wizard-dismissed', function() {
1038 Backend.elements.$chooseMassAction.val('');
1039 });
1040 });
1041 };
1042
1043 /**
1044 * Sends marked records to a stage
1045 *
1046 * @param {Event} e
1047 */
1048 Backend.sendToSpecificStageAction = function(e) {
1049 var affectedRecords = [],
1050 stage = $(e.currentTarget).val();
1051 for (var i = 0; i < Backend.markedRecordsForMassAction.length; ++i) {
1052 var affected = Backend.markedRecordsForMassAction[i].split(':');
1053 affectedRecords.push({
1054 table: affected[0],
1055 uid: affected[1],
1056 t3ver_oid: affected[2]
1057 });
1058 }
1059 Workspaces.sendRemoteRequest(
1060 Workspaces.generateRemoteActionsPayload('sendToSpecificStageWindow', [
1061 stage, affectedRecords
1062 ])
1063 ).done(function(response) {
1064 var $modal = Workspaces.renderSendToStageWindow(response);
1065 $modal.on('button.clicked', function(e) {
1066 if (e.target.name === 'ok') {
1067 var $form = $(e.currentTarget).find('form'),
1068 serializedForm = $form.serializeObject();
1069
1070 serializedForm.affects = {
1071 elements: affectedRecords,
1072 nextStage: stage
1073 };
1074
1075 Workspaces.sendRemoteRequest([
1076 Workspaces.generateRemoteActionsPayload('sendToSpecificStageExecute', [serializedForm]),
1077 Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings)
1078 ]).done(function(response) {
1079 $modal.modal('hide');
1080 Backend.renderWorkspaceInfos(response[1].result);
1081 Backend.refreshPageTree();
1082 });
1083 }
1084 }).on('modal-destroyed', function() {
1085 Backend.elements.$chooseStageAction.val('');
1086 });
1087 });
1088 };
1089
1090 /**
1091 * Reloads the page tree
1092 */
1093 Backend.refreshPageTree = function() {
1094 if (top.TYPO3 && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer && top.TYPO3.Backend.NavigationContainer.PageTree) {
1095 top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree();
1096 }
1097 };
1098
1099 /**
1100 * Renders the action button based on the user's permission.
1101 *
1102 * @returns {$}
1103 * @private
1104 */
1105 Backend.getAction = function(condition, action, iconIdentifier) {
1106 if (condition) {
1107 return $('<button />', {class: 'btn btn-default', 'data-action': action, 'data-toggle': 'tooltip'}).append(Backend.getPreRenderedIcon(iconIdentifier))
1108 }
1109 return $('<span />', {class: 'btn btn-default disabled'}).append(Backend.getPreRenderedIcon('empty-empty'));
1110 };
1111
1112 /**
1113 * Fetches and renders available preview links
1114 */
1115 Backend.generatePreviewLinks = function() {
1116 Workspaces.sendRemoteRequest(
1117 Workspaces.generateRemoteActionsPayload('generateWorkspacePreviewLinksForAllLanguages', [
1118 Backend.settings.id
1119 ])
1120 ).done(function(response) {
1121 var result = response[0].result,
1122 $list = $('<dl />');
1123
1124 $.each(result, function(language, url) {
1125 $list.append(
1126 $('<dt />').text(language),
1127 $('<dd />').append(
1128 $('<a />', {href: url, target: '_blank'}).text(url)
1129 )
1130 );
1131 });
1132
1133 Modal.show(
1134 TYPO3.lang['previewLink'],
1135 $list,
1136 Severity.info,
1137 [{
1138 text: TYPO3.lang['ok'],
1139 active: true,
1140 btnClass: 'btn-info',
1141 name: 'ok',
1142 trigger: function() {
1143 Modal.currentModal.trigger('modal-dismiss');
1144 }
1145 }],
1146 ['modal-inner-scroll']
1147 );
1148 });
1149 };
1150
1151 /**
1152 * Gets the pre-rendered icon
1153 * This method is intended to be dropped once we use Fluid's StandaloneView.
1154 *
1155 * @param {String} identifier
1156 * @returns {$}
1157 */
1158 Backend.getPreRenderedIcon = function(identifier) {
1159 return Backend.elements.$actionIcons.find('[data-identifier="' + identifier + '"]').clone();
1160 };
1161
1162 /**
1163 * Serialize a form to a JavaScript object
1164 *
1165 * @see http://stackoverflow.com/a/1186309/4828813
1166 * @return {Object}
1167 */
1168 $.fn.serializeObject = function() {
1169 var o = {};
1170 var a = this.serializeArray();
1171 $.each(a, function() {
1172 if (typeof o[this.name] !== 'undefined') {
1173 if (!o[this.name].push) {
1174 o[this.name] = [o[this.name]];
1175 }
1176 o[this.name].push(this.value || '');
1177 } else {
1178 o[this.name] = this.value || '';
1179 }
1180 });
1181 return o;
1182 };
1183
1184 /**
1185 * Changes the markup of a pagination action being disabled
1186 */
1187 $.fn.disablePagingAction = function() {
1188 $(this).addClass('disabled').find('.t3-icon').unwrap().wrap($('<span />'));
1189 };
1190
1191 $(Backend.initialize);
1192 });