Skip to content
This repository was archived by the owner on May 3, 2022. It is now read-only.

Commit 9a5d3aa

Browse files
natanael89andrerom
authored andcommitted
Fix EZP-27750: Backport of EZP-25478: Restore items whose original location has been deleted (#890)
1 parent 533d12d commit 9a5d3aa

File tree

9 files changed

+281
-21
lines changed

9 files changed

+281
-21
lines changed

Resources/public/js/models/ez-trashitemmodel.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,22 @@ YUI.add('ez-trashitemmodel', function (Y) {
3939
},
4040

4141
/**
42-
* Restores the item to it's original location
42+
* Restores the item to it's original location or to a given one if specified
4343
*
4444
* @method restore
4545
* @param {Object} options the required for the update
4646
* @param {Object} options.api (required) the JS REST client instance
47+
* @param {Object} [options.destination] if provided, locationId under which the item will be restored.
4748
* @param {Function} callback a callback executed when the operation is finished
4849
*/
4950
restore: function (options, callback) {
5051
var contentService = options.api.getContentService();
5152

52-
contentService.recover(this.get('id'), callback);
53+
if (options.destination) {
54+
contentService.recover(this.get('id'), options.destination, callback);
55+
} else {
56+
contentService.recover(this.get('id'), callback);
57+
}
5358
},
5459

5560
}, {

Resources/public/js/views/ez-trashview.js

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ YUI.add('ez-trashview', function (Y) {
2626
'.ez-trashitem-box': {
2727
'change': '_updateTrashBarButtons'
2828
},
29+
'.ez-trashitem-restore': {
30+
'tap': '_restoreSingleTrashItem'
31+
},
2932
},
3033

3134
initializer: function () {
@@ -96,7 +99,48 @@ YUI.add('ez-trashview', function (Y) {
9699
},
97100

98101
/**
99-
* Restores the selected trash items
102+
* Finds a trash item in the item list and returns it
103+
*
104+
* @private
105+
* @method _findTrashItems
106+
* @param {String} trashItemId
107+
* @return {Array} of TrashItem
108+
*/
109+
_findTrashItems: function (trashItemId) {
110+
var resultArray = [];
111+
Y.Array.some(this.get('trashItems'), function (trashItem) {
112+
if (trashItemId === trashItem.item.get('id')) {
113+
resultArray.push(trashItem.item);
114+
return true;
115+
}
116+
});
117+
118+
return resultArray;
119+
},
120+
121+
/**
122+
* Fires the `restoreItems` event
123+
*
124+
* @private
125+
* @method _fireRestoreItems
126+
* @param {Array} trashItems Trash items to be restored
127+
* @param {String} [destination] if provided, Location Id where it will be restored
128+
*/
129+
_fireRestoreItems: function (trashItems, destination) {
130+
/**
131+
* Fired to restore the selected items
132+
* @event restoreItems
133+
* @param {Array} trashItems Trash items to be restored
134+
* @param {String} [destination] if provided, Location Id where it will be restored
135+
*/
136+
this.fire('restoreItems', {
137+
trashItems: trashItems,
138+
destination: destination,
139+
});
140+
},
141+
142+
/**
143+
* Restores selected trash items
100144
*
101145
* @private
102146
* @method _restoreTrashItems
@@ -106,19 +150,35 @@ YUI.add('ez-trashview', function (Y) {
106150
trashItems = [];
107151

108152
selectedTrashItems.each(function (selectedTrashItem) {
109-
Y.Array.some(this.get('trashItems'), function (trashItem) {
110-
if (selectedTrashItem.getAttribute('value') === trashItem.item.get('id')) {
111-
trashItems.push(trashItem.item);
112-
return true;
113-
}
114-
});
153+
trashItems = trashItems.concat(this._findTrashItems(selectedTrashItem.getAttribute('value')));
115154
}, this);
116155

117-
/**
118-
* Fired to restore the selected items
119-
* @event restoreItems
120-
*/
121-
this.fire('restoreItems', {trashItems: trashItems});
156+
this._fireRestoreItems(trashItems);
157+
},
158+
159+
/**
160+
* Restores the selected (using UDW) trash item
161+
*
162+
* @protected
163+
* @method _restoreSingleTrashItem
164+
* @param {EventFacade} e
165+
*/
166+
_restoreSingleTrashItem: function (e) {
167+
var trashItemId = e.target.getAttribute('data-trash-item-id');
168+
169+
e.preventDefault();
170+
this.fire('contentDiscover', {
171+
config: {
172+
title: Y.eZ.trans('trash.ancestor.to.select', {}, 'trash'),
173+
multiple: false,
174+
contentDiscoveredHandler: Y.bind(function (e) {
175+
this._fireRestoreItems(this._findTrashItems(trashItemId), e.selection.location.get('id'));
176+
}, this),
177+
isSelectable: function (contentStruct) {
178+
return contentStruct.contentType.get('isContainer');
179+
},
180+
},
181+
});
122182
},
123183

124184
/**

Resources/public/js/views/services/ez-trashviewservice.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,10 @@ YUI.add('ez-trashviewservice', function (Y) {
228228
* @protected
229229
* @param {Object} e restoreItems event facade
230230
* @param {Array} e.trashItems List of trashItems to be restored
231+
* @param {String} e.destination (Optional) locationId where items need to be restored
231232
*/
232233
_restoreItems: function (e) {
233-
var loadOptions = {api: this.get('capi')},
234+
var loadOptions = {api: this.get('capi'), destination: e.destination},
234235
tasks = new Y.Parallel(),
235236
service = this,
236237
notificationIdentifier = "restoreTrashItems",

Resources/public/templates/trash.hbt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
</ul>
4444
{{else}}
4545
<span class="ez-trashview-info-message">{{translate 'trash.ancestors' 'trash'}}</span>
46+
<button
47+
data-trash-item-id="{{item.id}}"
48+
class="ez-trashitem-restore pure-button ez-button">
49+
{{translate 'trash.ancestors.button' 'trash'}}
50+
</button>
4651
{{/if}}
4752
</td>
4853
</tr>

Resources/translations/trash.en.xlf

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:jms="urn:jms:translation" version="1.2">
3-
<file date="2016-11-29T11:49:06Z" source-language="en" target-language="en" datatype="plaintext" original="not.available">
3+
<file date="2017-03-07T14:24:01Z" source-language="en" target-language="en" datatype="plaintext" original="not.available">
44
<header>
55
<tool tool-id="JMSTranslationBundle" tool-name="JMSTranslationBundle" tool-version="1.1.0-DEV"/>
66
<note>The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.</note>
@@ -72,12 +72,24 @@
7272
<note>key: restoring.trash.items</note>
7373
<jms:reference-file>./Resources/public/js/views/services/ez-trashviewservice.js</jms:reference-file>
7474
</trans-unit>
75+
<trans-unit id="a97f5d20007194640d4a0f86e05bccd2463d9642" resname="trash.ancestor.to.select">
76+
<source>Select a new parent location for the item</source>
77+
<target>Select a new parent location for the item</target>
78+
<note>key: trash.ancestor.to.select</note>
79+
<jms:reference-file>Resources/public/js/views/ez-trashview.js</jms:reference-file>
80+
</trans-unit>
7581
<trans-unit id="bdaee6eee47e0c028566b056680e5f7488772428" resname="trash.ancestors">
7682
<source>Item's ancestors are in Trash</source>
7783
<target>Item's ancestors are in Trash</target>
7884
<note>key: trash.ancestors</note>
7985
<jms:reference-file>./Resources/public/templates/trash.hbt</jms:reference-file>
8086
</trans-unit>
87+
<trans-unit id="ef15335cc7bc4c4bb04a558b3e7346fb2be24795" resname="trash.ancestors.button">
88+
<source>Restore under a new parent</source>
89+
<target>Restore under a new parent</target>
90+
<note>key: trash.ancestors.button</note>
91+
<jms:reference-file>Resources/public/templates/trash.hbt</jms:reference-file>
92+
</trans-unit>
8193
<trans-unit id="7883128cdfb813826271abe90b3a1f3190833ce6" resname="trash.empty">
8294
<source>The trash is empty</source>
8395
<target>The trash is empty</target>

Tests/js/models/assets/ez-trashitemmodel-tests.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,6 @@ YUI.add('ez-trashitemmodel-tests', function (Y) {
220220
args: [],
221221
returns: this.contentServiceMock
222222
});
223-
224-
this.options = {api: this.apiMock};
225223
},
226224

227225
tearDown: function () {
@@ -230,7 +228,8 @@ YUI.add('ez-trashitemmodel-tests', function (Y) {
230228
},
231229

232230
"Should call the api on restore": function () {
233-
var restoreCallback = function () {};
231+
var restoreCallback = function () {},
232+
options = {api: this.apiMock};
234233

235234
Mock.expect(this.contentServiceMock, {
236235
method: "recover",
@@ -253,7 +252,38 @@ YUI.add('ez-trashitemmodel-tests', function (Y) {
253252
});
254253

255254
this.model.set('id', this.id);
256-
this.model.restore(this.options, restoreCallback);
255+
this.model.restore(options, restoreCallback);
256+
257+
Mock.verify(this.contentServiceMock);
258+
},
259+
260+
"Should call the api on restore with destination provided": function () {
261+
var restoreCallback = function () {},
262+
destination = {},
263+
options = {api: this.apiMock, destination: destination};
264+
265+
Mock.expect(this.contentServiceMock, {
266+
method: "recover",
267+
args: [this.id, destination, Mock.Value.Function],
268+
run: Y.bind(function (id, destination, callback) {
269+
Assert.areSame(
270+
this.id,
271+
id,
272+
"Id passed to the contentService should match the model's"
273+
);
274+
275+
Assert.areSame(
276+
restoreCallback,
277+
callback,
278+
"Callback passed to the contentService should match"
279+
);
280+
281+
callback();
282+
}, this),
283+
});
284+
285+
this.model.set('id', this.id);
286+
this.model.restore(options, restoreCallback);
257287

258288
Mock.verify(this.contentServiceMock);
259289
},

Tests/js/views/assets/ez-trashview-tests.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ YUI.add('ez-trashview-tests', function (Y) {
217217
this.view = new Y.eZ.TrashView({
218218
trashBar: new Y.View(),
219219
trashItems: this.trashItems,
220+
container: '.container',
220221
});
221222
},
222223

@@ -269,6 +270,125 @@ YUI.add('ez-trashview-tests', function (Y) {
269270

270271
this.view.fire('whatever:restoreTrashItemsAction');
271272
},
273+
274+
"Should run the UDW when clicking on the restore a single item button": function () {
275+
var itemId = '/trash/item/42',
276+
udwStarted = false;
277+
278+
this.trashItems.push(this._createItem(itemId));
279+
280+
this.view.on('contentDiscover', Y.bind(function (e) {
281+
this.resume(function () {
282+
udwStarted = true;
283+
});
284+
}, this));
285+
286+
this.view.render();
287+
288+
this.view.get('container').one('.ez-trashitem-restore').simulateGesture('tap');
289+
this.wait();
290+
291+
Assert.isTrue(
292+
udwStarted,
293+
"UDW should have been started"
294+
);
295+
},
296+
297+
_testIsSelectable: function (isContainer) {
298+
var itemId = '/trash/item/42';
299+
300+
this.trashItems.push(this._createItem(itemId));
301+
302+
this.view.on('contentDiscover', Y.bind(function (e) {
303+
this.resume(function () {
304+
var contentTypeMock = new Mock();
305+
306+
Mock.expect(contentTypeMock, {
307+
method: 'get',
308+
args: ['isContainer'],
309+
returns: isContainer
310+
});
311+
312+
Assert.areSame(
313+
isContainer,
314+
e.config.isSelectable.call(this, {contentType: contentTypeMock}),
315+
"The isSelectable method should the value of isContainer"
316+
);
317+
});
318+
}, this));
319+
320+
this.view.render();
321+
322+
this.view.get('container').one('.ez-trashitem-restore').simulateGesture('tap');
323+
this.wait();
324+
},
325+
326+
"Should allow to pick an element which is a container": function () {
327+
this._testIsSelectable(true);
328+
},
329+
330+
"Should not allow to pick an element which is not a container": function () {
331+
this._testIsSelectable(false);
332+
},
333+
334+
"Should fire `restoreItems` with destination when restoring a single item": function () {
335+
var locationMock = new Mock(),
336+
fakeEventFacade = {selection: {location: locationMock}},
337+
destinationId = '/my/destination/42',
338+
itemId = '/trash/item/42';
339+
340+
Mock.expect(locationMock, {
341+
method: 'get',
342+
args: ['id'],
343+
returns: destinationId
344+
});
345+
346+
this.trashItems.push(this._createItem(itemId));
347+
348+
this.view.on('*:restoreItems', Y.bind(function (e) {
349+
Assert.areSame(
350+
1,
351+
e.trashItems.length,
352+
"The clicked item should be provided"
353+
);
354+
355+
Assert.areSame(
356+
itemId,
357+
e.trashItems[0].get('id'),
358+
"Item 1 should be the same"
359+
);
360+
361+
Assert.areSame(
362+
destinationId,
363+
e.destination,
364+
"Destination should be provided"
365+
);
366+
}, this));
367+
368+
this.view.on('contentDiscover', Y.bind(function (e) {
369+
this.resume(function () {
370+
var contentTypeMock = new Mock();
371+
372+
Mock.expect(contentTypeMock, {
373+
method: 'get',
374+
args: ['isContainer'],
375+
returns: true
376+
});
377+
378+
Assert.isTrue(
379+
e.config.isSelectable.call(this, {contentType: contentTypeMock}),
380+
"The isSelectable method should the value of isContainer"
381+
);
382+
383+
e.config.contentDiscoveredHandler.call(this, fakeEventFacade);
384+
});
385+
}, this));
386+
387+
this.view.render();
388+
389+
this.view.get('container').one('.ez-trashitem-restore').simulateGesture('tap');
390+
this.wait();
391+
},
272392
});
273393

274394
attributesTest = new Y.Test.Case({

Tests/js/views/ez-trashview.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
<input type="checkbox" class="ez-trashitem-box" value="{{item.id}}"/>
1414
{{/each}}
1515
</div>
16+
<button
17+
data-trash-item-id="/trash/item/42"
18+
class="ez-trashitem-restore">
19+
Restore under a new parent
20+
</button>
1621
<div class="ez-trashbar-container"></div>
1722
</script>
1823

0 commit comments

Comments
 (0)