Skip to content

Commit

Permalink
Merge pull request #6842 from getkirby/v5/batch-editing
Browse files Browse the repository at this point in the history
Batch editing
  • Loading branch information
bastianallgeier authored Feb 3, 2025
2 parents 6891eaf + b23dea4 commit 4385a76
Show file tree
Hide file tree
Showing 31 changed files with 1,190 additions and 72 deletions.
25 changes: 19 additions & 6 deletions config/sections/files.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

return [
'mixins' => [
'batch',
'details',
'empty',
'headline',
Expand Down Expand Up @@ -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;
Expand All @@ -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();

Expand All @@ -127,7 +129,8 @@
'mime' => $file->mime(),
'parent' => $file->parent()->panel()->path(),
'permissions' => [
'sort' => $permissions->can('sort'),
'delete' => $permissions->can('delete'),
'sort' => $permissions->can('sort'),
],
'template' => $file->template(),
'text' => $file->toSafeString($this->text),
Expand All @@ -144,7 +147,7 @@
return $data;
},
'total' => function () {
return $this->models->pagination()->total();
return $this->models()->count();
},
'errors' => function () {
$errors = [];
Expand Down Expand Up @@ -218,6 +221,15 @@

return true;
}
],
[
'pattern' => 'delete',
'method' => 'DELETE',
'action' => function () {
return $this->section()->deleteSelected(
ids: $this->requestBody('ids'),
);
}
]
];
},
Expand All @@ -229,6 +241,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,
Expand Down
45 changes: 45 additions & 0 deletions config/sections/mixins/batch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use Kirby\Exception\Exception;
use Kirby\Exception\PermissionException;
use Kirby\Toolkit\I18n;

return [
'props' => [
/**
* 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;
}
]
];
31 changes: 25 additions & 6 deletions config/sections/pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

return [
'mixins' => [
'batch',
'details',
'empty',
'headline',
Expand Down Expand Up @@ -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();

Expand All @@ -182,10 +184,11 @@
'link' => $panel->url(true),
'parent' => $page->parentId(),
'permissions' => [
'sort' => $permissions->can('sort'),
'delete' => $permissions->can('delete'),
'changeSlug' => $permissions->can('changeSlug'),
'changeStatus' => $permissions->can('changeStatus'),
'changeTitle' => $permissions->can('changeTitle'),
'sort' => $permissions->can('sort'),
],
'status' => $page->status(),
'template' => $page->intendedTemplate()->name(),
Expand Down Expand Up @@ -315,12 +318,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,
Expand Down
4 changes: 4 additions & 0 deletions i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,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. Try each remaining file individually to see the specific error that prevents deletion.",
"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}",
Expand Down Expand Up @@ -171,6 +172,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. Try each remaining page individually to see the specific error that prevents deletion.",
"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",
Expand Down Expand Up @@ -382,6 +384,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",
Expand Down Expand Up @@ -591,6 +594,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",
Expand Down
18 changes: 18 additions & 0 deletions panel/lab/components/item/1_list/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@
width="1/2"
/>
</k-lab-example>
<k-lab-example label="Selecting">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selecting="true"
:selectable="true"
info="With some info"
text="This is a nice item"
/>
</k-lab-example>
<k-lab-example label="Selecting & not selectable">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selecting="true"
:selectable="false"
info="With some info"
text="This is a nice item"
/>
</k-lab-example>
<k-lab-example label="Theme: disabled">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
Expand Down
27 changes: 26 additions & 1 deletion panel/lab/components/item/2_cards/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<k-lab-examples>
<k-lab-examples class="k-card-items-example">
<k-lab-example label="Card Item">
<k-item layout="cards" text="This is a nice item" />
</k-lab-example>
Expand Down Expand Up @@ -73,6 +73,25 @@
width="1/2"
/>
</k-lab-example>
<k-lab-example label="Selecting">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selecting="true"
layout="cards"
info="With some info"
text="This is a nice item"
/>
</k-lab-example>
<k-lab-example label="Selecting & no selectable">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selecting="true"
:selectable="false"
layout="cards"
info="With some info"
text="This is a nice item"
/>
</k-lab-example>
<k-lab-example label="Theme: disabled">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
Expand Down Expand Up @@ -100,3 +119,9 @@
</k-lab-example>
</k-lab-examples>
</template>

<style>
.k-card-items-example .k-item {
max-width: 30rem;
}
</style>
19 changes: 19 additions & 0 deletions panel/lab/components/item/3_cardlets/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@
width="1/2"
/>
</k-lab-example>
<k-lab-example label="Selecting">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selecting="true"
layout="cardlets"
info="With some info"
text="This is a nice item"
/>
</k-lab-example>
<k-lab-example label="Selecting & not selectable">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selecting="true"
:selectable="false"
layout="cardlets"
info="With some info"
text="This is a nice item"
/>
</k-lab-example>
<k-lab-example label="Theme: disabled">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
Expand Down
29 changes: 29 additions & 0 deletions panel/lab/components/items/1_list/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,42 @@
<k-lab-example label="List">
<k-items :items="items" />
</k-lab-example>
<k-lab-example label="Selectable">
<k-items :items="selectableItems" :selectable="true" @select="onSelect" />
<br />
<k-code>Selected: {{ selected.join(", ") }}</k-code>
</k-lab-example>
</k-lab-examples>
</template>

<script>
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);
}
}
}
};
</script>
Loading

0 comments on commit 4385a76

Please sign in to comment.