Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch editing #6842

Open
wants to merge 37 commits into
base: v5/develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9d816ce
Selectable items
bastianallgeier Oct 17, 2024
4a6fd47
Batch editing mode for model sections
bastianallgeier Oct 17, 2024
eca5c59
Backend requests for batch deleting files and pages
bastianallgeier Oct 17, 2024
d814492
Catch errors and send them to the error dialog
bastianallgeier Oct 22, 2024
54fcf50
Validate min validation
bastianallgeier Oct 22, 2024
7199e96
New batch mixin
bastianallgeier Dec 7, 2024
658c432
Only activate the batch mode by option
bastianallgeier Dec 7, 2024
89e76eb
Check for the batch option in the ApI calls
bastianallgeier Dec 7, 2024
4306d9a
Add unit tests for the batch option
bastianallgeier Dec 7, 2024
eb7bded
Move batch delete logic into the pages and files classes
bastianallgeier Dec 7, 2024
5233d0c
Add unit tests for new collection methods
bastianallgeier Dec 7, 2024
18d9a64
Fix covers annotations
bastianallgeier Dec 7, 2024
534f55b
Translate the bulk exceptions for Files::delete and Pages::delete
bastianallgeier Dec 7, 2024
08b6c53
Reuse the validation error messages
bastianallgeier Dec 7, 2024
66bbbd7
Selectable item: click anywhere to select
distantnative Dec 7, 2024
93ae836
Stop click event on input
bastianallgeier Dec 7, 2024
fe210c5
Already show a disabled delete button if nothing is selected
bastianallgeier Dec 7, 2024
59ff084
New lab examples
bastianallgeier Dec 10, 2024
2717415
Add selectable prop to collection, items and table
bastianallgeier Dec 10, 2024
392066a
Remove the layout check
bastianallgeier Dec 10, 2024
97076f3
Enable selectable mode
bastianallgeier Dec 10, 2024
e9dabe5
Reload the section on error as well
bastianallgeier Dec 10, 2024
696af87
Move logic into batch mixin
bastianallgeier Dec 10, 2024
d5003c8
Simplify code
bastianallgeier Dec 10, 2024
749e611
Close selection mode if another section opens theirs
bastianallgeier Dec 10, 2024
83da2d7
Use new models & modelsPaginated distinction to get access to both
bastianallgeier Dec 13, 2024
48eddf9
No longer send the checkbox event
bastianallgeier Dec 13, 2024
20d1260
Set the outline for selected items
bastianallgeier Dec 13, 2024
ebb5b27
Add sortable example
bastianallgeier Dec 13, 2024
6f0c8db
Better visual selected state for table rows
bastianallgeier Dec 13, 2024
965cced
Fix selectable examples
bastianallgeier Dec 13, 2024
af940f7
Improve selected state for items
bastianallgeier Dec 13, 2024
8060524
Add translations for the confirmation dialog
bastianallgeier Dec 13, 2024
45a84fa
Add unit tests
bastianallgeier Dec 13, 2024
5307032
Improve the error messages for batch processes
bastianallgeier Dec 13, 2024
dab3a6d
Test the new getDetails behavior
bastianallgeier Dec 13, 2024
febbef5
Add lab examples for selectable item
distantnative Dec 17, 2024
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
22 changes: 17 additions & 5 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 Down Expand Up @@ -144,7 +146,7 @@
return $data;
},
'total' => function () {
return $this->models->pagination()->total();
return $this->models()->count();
},
'errors' => function () {
$errors = [];
Expand Down Expand Up @@ -218,6 +220,15 @@

return true;
}
],
[
'pattern' => 'delete',
'method' => 'DELETE',
'action' => function () {
return $this->section()->deleteSelected(
ids: $this->requestBody('ids'),
);
}
]
];
},
Expand All @@ -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,
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;
}
]
];
28 changes: 23 additions & 5 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 Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 8 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,14 @@
width="1/2"
/>
</k-lab-example>
<k-lab-example label="Selectable">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selectable="true"
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
9 changes: 9 additions & 0 deletions panel/lab/components/item/2_cards/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@
width="1/2"
/>
</k-lab-example>
<k-lab-example label="Selectable">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selectable="true"
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
9 changes: 9 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,15 @@
width="1/2"
/>
</k-lab-example>
<k-lab-example label="Selectable">
<k-item
:image="{ src: 'https://picsum.photos/800/600' }"
:selectable="true"
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>
34 changes: 34 additions & 0 deletions panel/lab/components/items/2_cards/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,47 @@
<k-lab-example label="cards">
<k-items :items="items" layout="cards" />
</k-lab-example>
<k-lab-example label="Selectable">
<k-items
:items="selectableItems"
:selectable="true"
layout="cards"
@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