Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions app/assets/stylesheets/pageflow/editor/filtered_files.scss
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,17 @@
flex: 1;
}

// On a line of its own, so that the number of checked files is not
// squeezed next to it in languages with longer words than English.
// On a line of their own, so that the number of checked files is not
// squeezed next to them in languages with longer words than English.
&-selection_bar_actions {
display: flex;
align-self: flex-end;
gap: space(1);
}

&-selection_bar_action {
@include simple-button;

align-self: flex-end;
padding: space(1) space(4);

&[disabled] {
Expand All @@ -126,6 +131,16 @@
}
}

// The icon only mixin brings its own vertical padding, which would
// make this button taller than the one next to it.
&-selection_bar_destroy {
@include icon-only-button("destructive");
@include trash-icon;

padding-top: space(1);
padding-bottom: space(1);
}

&-selection_bar_dismiss {
@include cancel-icon;

Expand Down
8 changes: 8 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,8 @@ de:
chapter_item_view:
chapter: Kapitel
unnamed: "(Unbenannt)"
confirmable_file_item_view:
confirm_destroy: Datei wirklich löschen?
edit_chapter_view:
confirm_destroy: |-
Kapitel einschließlich ALLER enthaltener Seiten wirklich löschen?
Expand Down Expand Up @@ -1565,6 +1567,8 @@ de:
show_editor: Editorleiste einblenden
entry_preview_view:
scroll_hint: Scrollen, um weiterzulesen
file_item_view:
confirm_destroy: Datei wirklich löschen?
file_meta_data_item_value_view:
blank: "-"
file_preview_progress_bar_view:
Expand All @@ -1590,6 +1594,10 @@ de:
actions: 'Aktionen für die Dateiliste'
any_file_type: 'Datei'
cancel_selection: 'Auswahl abbrechen'
confirm_destroy_selection:
one: '1 Element wirklich löschen?'
other: '%{count} Elemente wirklich löschen?'
destroy_selection: 'Auswahl löschen'
end_selection: 'Auswahl beenden'
move_selection: 'Verschieben...'
reset_filter: 'Filter zurücksetzen'
Expand Down
8 changes: 8 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,8 @@ en:
chapter_item_view:
chapter: Chapter
unnamed: "(New chapter)"
confirmable_file_item_view:
confirm_destroy: Really delete this file?
edit_chapter_view:
confirm_destroy: |-
Really delete this chapter including ALL its pages?
Expand Down Expand Up @@ -1557,6 +1559,8 @@ en:
show_editor: Show editor panel
entry_preview_view:
scroll_hint: Scroll down to continue
file_item_view:
confirm_destroy: Really delete this file?
file_meta_data_item_value_view:
blank: "-"
file_preview_progress_bar_view:
Expand All @@ -1582,6 +1586,10 @@ en:
actions: 'File list actions'
any_file_type: 'a file'
cancel_selection: 'Cancel selection'
confirm_destroy_selection:
one: 'Really delete 1 item?'
other: 'Really delete %{count} items?'
destroy_selection: 'Delete selection'
end_selection: 'End selection'
move_selection: 'Move...'
reset_filter: 'Reset filter'
Expand Down
28 changes: 28 additions & 0 deletions package/spec/editor/collections/FileFoldersCollection-spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Backbone from 'backbone';

import {FileFoldersCollection} from 'pageflow/editor';

import * as support from '$support';
Expand Down Expand Up @@ -159,6 +161,32 @@ describe('FileFoldersCollection', () => {
});
});

describe('#isEmptyFolder', () => {
it('returns true for folder without files or subfolders', () => {
const fileFolders = collection([{perma_id: 1, name: 'Interviews'}]);
const files = new Backbone.Collection([{folder_perma_id: 2}]);

expect(fileFolders.isEmptyFolder(fileFolders.byPermaId(1), files)).toBe(true);
});

it('returns false for folder which holds a file', () => {
const fileFolders = collection([{perma_id: 1, name: 'Interviews'}]);
const files = new Backbone.Collection([{folder_perma_id: 1}]);

expect(fileFolders.isEmptyFolder(fileFolders.byPermaId(1), files)).toBe(false);
});

it('returns false for folder which holds an empty subfolder', () => {
const fileFolders = collection([
{perma_id: 1, name: 'Interviews'},
{perma_id: 2, name: 'Raw', parent_folder_perma_id: 1}
]);
const files = new Backbone.Collection([]);

expect(fileFolders.isEmptyFolder(fileFolders.byPermaId(1), files)).toBe(false);
});
});

describe('persistence', () => {
support.useFakeXhr(() => testContext);

Expand Down
27 changes: 27 additions & 0 deletions package/spec/editor/collections/ListSelection-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,31 @@ describe('ListSelection', () => {

expect(selection.models).toEqual([file]);
});

describe('#destroyAll', () => {
it('destroys every selected item', () => {
const file = f.file({id: 1});
const otherFile = f.file({id: 2});
jest.spyOn(file, 'destroy').mockImplementation(() => {});
jest.spyOn(otherFile, 'destroy').mockImplementation(() => {});
const selection = new ListSelection([file, otherFile]);

selection.destroyAll();

expect(file.destroy).toHaveBeenCalled();
expect(otherFile.destroy).toHaveBeenCalled();
});

it('destroys items which leave the selection along the way', () => {
const file = f.file({id: 1});
const otherFile = f.file({id: 2});
const selection = new ListSelection([file, otherFile]);
jest.spyOn(file, 'destroy').mockImplementation(() => selection.remove(file));
jest.spyOn(otherFile, 'destroy').mockImplementation(() => selection.remove(otherFile));

selection.destroyAll();

expect(otherFile.destroy).toHaveBeenCalled();
});
});
});
4 changes: 2 additions & 2 deletions package/spec/editor/models/DestroyMenuItem-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {useFakeTranslations} from 'pageflow/testHelpers';

describe('DestroyMenuItem', () => {
useFakeTranslations({
'pageflow.editor.destroy_menu_item.destroy': 'Delete',
'pageflow.editor.destroy_menu_item.confirm_destroy': 'Really delete?'
'pageflow.editor.views.destroy_menu_item.destroy': 'Delete',
'pageflow.editor.views.destroy_menu_item.confirm_destroy': 'Really delete?'
});

it('has name destroy by default', () => {
Expand Down
38 changes: 38 additions & 0 deletions package/spec/editor/views/ConfirmableFileItemView-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import * as support from '$support';
import {renderBackboneView as render} from 'pageflow/testHelpers';

describe('ConfirmableFileItemView', () => {
support.useFakeTranslations({
'pageflow.editor.templates.confirmable_file_item.remove': 'Delete',
'pageflow.editor.views.confirmable_file_item_view.confirm_destroy': 'Really delete this file?'
});

it('displays file title', () => {
const file = support.factories.file({file_name: 'original.mp4'});

Expand All @@ -16,4 +21,37 @@ describe('ConfirmableFileItemView', () => {

expect(getByText('original.mp4')).not.toBeNull();
});

it('destroys file when delete is clicked', () => {
window.confirm = jest.fn(() => true);
const file = support.factories.file({file_name: 'original.mp4'});
jest.spyOn(file, 'destroy').mockImplementation(() => {});

const view = new ConfirmableFileItemView({
model: file,
selectedFiles: new Backbone.Collection()
});

const {getByTitle} = render(view);
getByTitle('Delete').click();

expect(window.confirm).toHaveBeenCalledWith('Really delete this file?');
expect(file.destroy).toHaveBeenCalled();
});

it('keeps file when the confirmation is dismissed', () => {
window.confirm = jest.fn(() => false);
const file = support.factories.file({file_name: 'original.mp4'});
jest.spyOn(file, 'destroy').mockImplementation(() => {});

const view = new ConfirmableFileItemView({
model: file,
selectedFiles: new Backbone.Collection()
});

const {getByTitle} = render(view);
getByTitle('Delete').click();

expect(file.destroy).not.toHaveBeenCalled();
});
});
17 changes: 16 additions & 1 deletion package/spec/editor/views/FileItemView-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ describe('FileItemView', () => {
'pageflow.editor.templates.file_item.move': 'Move...',
'pageflow.editor.templates.file_item.settings': 'Settings',
'pageflow.editor.templates.file_item.select': 'Select',
'pageflow.editor.templates.files.in_folder': 'In folder:'
'pageflow.editor.templates.files.in_folder': 'In folder:',
'pageflow.editor.views.file_item_view.confirm_destroy': 'Really delete this file?'
});

it('displays file title', () => {
Expand Down Expand Up @@ -406,9 +407,23 @@ describe('FileItemView', () => {
const {getByRole} = render(view);
getByRole('link', {name: 'Delete'}).click();

expect(window.confirm).toHaveBeenCalledWith('Really delete this file?');
expect(file.destroy).toHaveBeenCalled();
});

it('keeps file when the confirmation is dismissed', () => {
window.confirm = jest.fn(() => false);
const file = support.factories.file({id: 123});
jest.spyOn(file, 'destroy').mockImplementation(() => {});

const view = new FileItemView({model: file});

const {getByRole} = render(view);
getByRole('link', {name: 'Delete'}).click();

expect(file.destroy).not.toHaveBeenCalled();
});

it('cancels upload when cancel upload is selected', () => {
const file = support.factories.file({id: 123, state: 'uploading'});
jest.spyOn(file, 'cancelUpload').mockImplementation(() => {});
Expand Down
65 changes: 65 additions & 0 deletions package/spec/editor/views/FilesView-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ describe('FilesView', () => {
'pageflow.editor.views.filtered_files_view.actions': 'File list actions',
'pageflow.editor.views.filtered_files_view.select_items': 'Select files and folders',
'pageflow.editor.views.filtered_files_view.end_selection': 'End selection',
'pageflow.editor.views.filtered_files_view.destroy_selection': 'Delete selection',
'pageflow.editor.views.filtered_files_view.confirm_destroy_selection': {
one: 'Really delete 1 item?',
other: 'Really delete %{count} items?'
},
'pageflow.editor.views.filtered_files_view.selected_items': {
zero: 'No items selected',
one: '1 item selected',
Expand Down Expand Up @@ -1322,6 +1327,10 @@ describe('FilesView', () => {
return view.el.querySelector('.filtered_files-selection_bar');
}

function destroyButton(queries) {
return queries.getByRole('button', {name: 'Delete selection'});
}

it('does not show check boxes before selecting has started', () => {
const view = new FilesView({model: entryWithFolders()});

Expand Down Expand Up @@ -1447,6 +1456,62 @@ describe('FilesView', () => {

expect(queryByRole('checkbox', {name: /^Interviews/})).toBeNull();
});

it('deletes checked folders and files at once', async () => {
const entry = entryWithFolders();
const view = new FilesView({model: entry});
const user = userEvent.setup();
window.confirm = jest.fn(() => true);

const queries = render(view);
await startSelecting(user, queries);
await check(user, queries, /^Landscapes/);
await check(user, queries, 'unfiled.png');
await user.click(destroyButton(queries));

expect(window.confirm).toHaveBeenCalledWith('Really delete 2 items?');
expect(testContext.requests.map(request => request.url))
.toEqual(['/editor/entries/1/file_folders/12',
'/editor/entries/1/files/image_files/1']);
});

it('is not offered for a folder which still holds files', async () => {
const view = new FilesView({model: entryWithFolders(), folderPermaId: '1'});
const user = userEvent.setup();

const queries = render(view);
await startSelecting(user, queries);
await check(user, queries, /^Raw/);

expect(destroyButton(queries)).toBeDisabled();
});

it('is not offered for a folder which still holds subfolders', async () => {
const view = new FilesView({model: entryWithFolders()});
const user = userEvent.setup();

const queries = render(view);
await startSelecting(user, queries);
await check(user, queries, /^Interviews/);

expect(destroyButton(queries)).toBeDisabled();
});

it('is offered again once the non-empty folder is unchecked', async () => {
const view = new FilesView({model: entryWithFolders()});
const user = userEvent.setup();

const queries = render(view);
await startSelecting(user, queries);
await check(user, queries, /^Landscapes/);
await check(user, queries, /^Interviews/);

expect(destroyButton(queries)).toBeDisabled();

await check(user, queries, /^Interviews/);

expect(destroyButton(queries)).toBeEnabled();
});
});

describe('filtering by file type', () => {
Expand Down
Loading
Loading