diff --git a/config/sections/files.php b/config/sections/files.php index 169a3edc5f..e2d48a8034 100644 --- a/config/sections/files.php +++ b/config/sections/files.php @@ -6,6 +6,7 @@ return [ 'mixins' => [ + 'batch', 'details', 'empty', 'headline', @@ -90,14 +91,15 @@ $files = $files->flip(); } + return $files; + }, + 'modelsPaginated' => function () { // apply the default pagination - $files = $files->paginate([ + return $this->models()->paginate([ 'page' => $this->page, 'limit' => $this->limit, 'method' => 'none' // the page is manually provided ]); - - return $files; }, 'files' => function () { return $this->models; @@ -109,7 +111,7 @@ // a different parent model $dragTextAbsolute = $this->model->is($this->parent) === false; - foreach ($this->models as $file) { + foreach ($this->modelsPaginated() as $file) { $panel = $file->panel(); $permissions = $file->permissions(); @@ -144,7 +146,7 @@ return $data; }, 'total' => function () { - return $this->models->pagination()->total(); + return $this->models()->count(); }, 'errors' => function () { $errors = []; @@ -218,6 +220,15 @@ return true; } + ], + [ + 'pattern' => 'delete', + 'method' => 'DELETE', + 'action' => function () { + return $this->section()->deleteSelected( + ids: $this->requestBody('ids'), + ); + } ] ]; }, @@ -229,6 +240,7 @@ 'options' => [ 'accept' => $this->accept, 'apiUrl' => $this->parent->apiUrl(true) . '/sections/' . $this->name, + 'batch' => $this->batch, 'columns' => $this->columnsWithTypes(), 'empty' => $this->empty, 'headline' => $this->headline, diff --git a/config/sections/mixins/batch.php b/config/sections/mixins/batch.php new file mode 100644 index 0000000000..25128f253a --- /dev/null +++ b/config/sections/mixins/batch.php @@ -0,0 +1,45 @@ + [ + /** + * Activates the batch delete option for the section + */ + 'batch' => function (bool $batch = false) { + return $batch; + }, + ], + 'methods' => [ + 'deleteSelected' => function (array $ids): bool { + if ($ids === []) { + return true; + } + + // check if batch deletion is allowed + if ($this->batch() === false) { + throw new PermissionException( + message: 'The section does not support batch actions' + ); + } + + $min = $this->min(); + + // check if the section has enough items after the deletion + if ($this->total() - count($ids) < $min) { + throw new Exception( + message: I18n::template('error.section.' . $this->type() . '.min.' . I18n::form($min), [ + 'min' => $min, + 'section' => $this->headline() + ]) + ); + } + + $this->models()->delete($ids); + return true; + } + ] +]; diff --git a/config/sections/pages.php b/config/sections/pages.php index 6f296c15c3..bdbd61346a 100644 --- a/config/sections/pages.php +++ b/config/sections/pages.php @@ -10,6 +10,7 @@ return [ 'mixins' => [ + 'batch', 'details', 'empty', 'headline', @@ -149,25 +150,26 @@ $pages = $pages->flip(); } + return $pages; + }, + 'modelsPaginated' => function () { // pagination - $pages = $pages->paginate([ + return $this->models()->paginate([ 'page' => $this->page, 'limit' => $this->limit, 'method' => 'none' // the page is manually provided ]); - - return $pages; }, 'pages' => function () { return $this->models; }, 'total' => function () { - return $this->models->pagination()->total(); + return $this->models()->count(); }, 'data' => function () { $data = []; - foreach ($this->models as $page) { + foreach ($this->modelsPaginated() as $page) { $panel = $page->panel(); $permissions = $page->permissions(); @@ -315,12 +317,28 @@ return $blueprints; }, ], + // @codeCoverageIgnoreStart + 'api' => function () { + return [ + [ + 'pattern' => 'delete', + 'method' => 'DELETE', + 'action' => function () { + return $this->section()->deleteSelected( + ids: $this->requestBody('ids'), + ); + } + ] + ]; + }, + // @codeCoverageIgnoreEnd 'toArray' => function () { return [ 'data' => $this->data, 'errors' => $this->errors, 'options' => [ 'add' => $this->add, + 'batch' => $this->batch, 'columns' => $this->columnsWithTypes(), 'empty' => $this->empty, 'headline' => $this->headline, diff --git a/i18n/translations/en.json b/i18n/translations/en.json index 70bece0b76..0ec1dbea57 100644 --- a/i18n/translations/en.json +++ b/i18n/translations/en.json @@ -108,6 +108,7 @@ "error.file.changeTemplate.invalid": "The template for the file \"{id}\" cannot be changed to \"{template}\" (valid: \"{blueprints}\")", "error.file.changeTemplate.permission": "You are not allowed to change the template for the file \"{id}\"", + "error.file.delete.multiple": "Not all files could be deleted", "error.file.duplicate": "A file with the name \"{filename}\" already exists", "error.file.extension.forbidden": "The extension \"{extension}\" is not allowed", "error.file.extension.invalid": "Invalid extension: {extension}", @@ -170,6 +171,7 @@ "error.page.delete": "The page \"{slug}\" cannot be deleted", "error.page.delete.confirm": "Please enter the page title to confirm", "error.page.delete.hasChildren": "The page has subpages and cannot be deleted", + "error.page.delete.multiple": "Not all pages could be deleted", "error.page.delete.permission": "You are not allowed to delete \"{slug}\"", "error.page.draft.duplicate": "A page draft with the URL appendix \"{slug}\" already exists", "error.page.duplicate": "A page with the URL appendix \"{slug}\" already exists", @@ -381,6 +383,7 @@ "file.sort": "Change position", "files": "Files", + "files.delete.confirm.selected": "Do you really want to delete the selected files? This action cannot be undone.", "files.empty": "No files yet", "filter": "Filter", @@ -590,6 +593,7 @@ "page.status.unlisted.description": "The page is only accessible via URL", "pages": "Pages", + "pages.delete.confirm.selected": "Do you really want to delete the selected pages? This action cannot be undone.", "pages.empty": "No pages yet", "pages.status.draft": "Drafts", "pages.status.listed": "Published", diff --git a/panel/lab/components/item/1_list/index.vue b/panel/lab/components/item/1_list/index.vue index fc62ac1528..9bedcc3b86 100644 --- a/panel/lab/components/item/1_list/index.vue +++ b/panel/lab/components/item/1_list/index.vue @@ -69,6 +69,14 @@ width="1/2" /> + + + + + + + + + + + +
+ Selected: {{ selected.join(", ") }} +
@@ -10,6 +15,30 @@ export default { props: { items: Array + }, + data() { + return { + selected: [] + }; + }, + computed: { + selectableItems() { + return this.items.map((item) => { + return { + ...item, + selectable: true + }; + }); + } + }, + methods: { + onSelect(item, index) { + if (this.selected.includes(index)) { + this.selected = this.selected.filter((i) => i !== index); + } else { + this.selected.push(index); + } + } } }; diff --git a/panel/lab/components/items/2_cards/index.vue b/panel/lab/components/items/2_cards/index.vue index e9ae98d1d1..02d8735100 100644 --- a/panel/lab/components/items/2_cards/index.vue +++ b/panel/lab/components/items/2_cards/index.vue @@ -3,6 +3,16 @@ + + +
+ Selected: {{ selected.join(", ") }} +
@@ -10,6 +20,30 @@ export default { props: { items: Array + }, + data() { + return { + selected: [] + }; + }, + computed: { + selectableItems() { + return this.items.map((item) => { + return { + ...item, + selectable: true + }; + }); + } + }, + methods: { + onSelect(item, index) { + if (this.selected.includes(index)) { + this.selected = this.selected.filter((i) => i !== index); + } else { + this.selected.push(index); + } + } } }; diff --git a/panel/lab/components/items/3_cardlets/index.vue b/panel/lab/components/items/3_cardlets/index.vue index aead0089f0..ca6f85b0b9 100644 --- a/panel/lab/components/items/3_cardlets/index.vue +++ b/panel/lab/components/items/3_cardlets/index.vue @@ -3,6 +3,16 @@ + + +
+ Selected: {{ selected.join(", ") }} +
@@ -10,6 +20,30 @@ export default { props: { items: Array + }, + data() { + return { + selected: [] + }; + }, + computed: { + selectableItems() { + return this.items.map((item) => { + return { + ...item, + selectable: true + }; + }); + } + }, + methods: { + onSelect(item, index) { + if (this.selected.includes(index)) { + this.selected = this.selected.filter((i) => i !== index); + } else { + this.selected.push(index); + } + } } }; diff --git a/panel/lab/components/items/4_table/index.vue b/panel/lab/components/items/4_table/index.vue index a760189118..ecc6464580 100644 --- a/panel/lab/components/items/4_table/index.vue +++ b/panel/lab/components/items/4_table/index.vue @@ -2,26 +2,23 @@ + + +
+ Selected: {{ selected.join(", ") }} +
@@ -29,6 +26,47 @@ export default { props: { items: Array + }, + data() { + return { + selected: [] + }; + }, + computed: { + columns() { + return { + image: { + label: "", + type: "image", + width: "var(--table-row-height)" + }, + text: { + label: "Text", + type: "text" + }, + info: { + label: "Info", + type: "text" + } + }; + }, + selectableItems() { + return this.items.map((item) => { + return { + ...item, + selectable: true + }; + }); + } + }, + methods: { + onSelect(item, index) { + if (this.selected.includes(index)) { + this.selected = this.selected.filter((i) => i !== index); + } else { + this.selected.push(index); + } + } } }; diff --git a/panel/lab/components/items/index.php b/panel/lab/components/items/index.php index 75b3ab7e5b..3ff50bece9 100644 --- a/panel/lab/components/items/index.php +++ b/panel/lab/components/items/index.php @@ -5,7 +5,7 @@ return [ 'docs' => 'k-items', - 'items' => A::map(range(0, 20), function ($item) { + 'items' => A::map(range(0, 10), function ($item) { return [ 'text' => 'This is item ' . $item, 'info' => 'Some info text', diff --git a/panel/lab/components/tables/1_horizontal/index.vue b/panel/lab/components/tables/1_horizontal/index.vue index 2007c8bed9..0a3589445e 100644 --- a/panel/lab/components/tables/1_horizontal/index.vue +++ b/panel/lab/components/tables/1_horizontal/index.vue @@ -129,10 +129,10 @@ }" element="tbody" > - + {{ i }} - + Kirby mail@getkirby.com diff --git a/panel/lab/components/tables/4_component/index.vue b/panel/lab/components/tables/4_component/index.vue new file mode 100644 index 0000000000..73f24c2706 --- /dev/null +++ b/panel/lab/components/tables/4_component/index.vue @@ -0,0 +1,73 @@ + + + diff --git a/panel/src/components/Collection/Collection.vue b/panel/src/components/Collection/Collection.vue index 3042b57f67..0851a2e756 100644 --- a/panel/src/components/Collection/Collection.vue +++ b/panel/src/components/Collection/Collection.vue @@ -15,6 +15,7 @@ items, layout, link, + selectable, size, sortable, theme @@ -22,6 +23,7 @@ @change="$emit('change', $event)" @item="$emit('item', $event)" @option="onOption" + @select="onSelect" @sort="$emit('sort', $event)" >