feat(checklist): trash mode toggle to view deleted items

This commit is contained in:
2026-05-15 01:00:22 +03:00
parent d5765be160
commit ec9c1767bb
8 changed files with 813 additions and 11 deletions

View File

@@ -200,6 +200,34 @@ final class ChecklistController extends OCSController {
});
}
/**
* List soft-deleted items in a checklist (trash)
*
* Returns items whose deleted_at is set, most recently deleted first.
*
* @param int $houseId House id.
* @param int $listId List id.
* @param int<1, 1000> $limit Maximum number of items to return.
* @param int<0, max> $offset Number of items to skip.
*
* @return DataResponse<Http::STATUS_OK, list<PantryListItem>, array{}>
*
* 200: Deleted items returned
*/
#[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/lists/{listId}/items/trash')]
#[NoAdminRequired]
public function indexDeletedItems(int $houseId, int $listId, int $limit = 200, int $offset = 0): DataResponse {
return $this->runAction(function () use ($houseId, $listId, $limit, $offset): DataResponse {
$this->auth->requireMember($houseId, $this->requireUid());
$list = $this->lists->getList($listId);
$this->assertListInHouse($list->getHouseId(), $houseId);
$all = $this->lists->listDeletedItems($listId);
$sliced = array_slice($all, max(0, $offset), max(0, $limit));
$items = array_map(fn ($i) => $i->jsonSerialize(), $sliced);
return new DataResponse($items);
});
}
/**
* Add an item to a list
*
@@ -401,6 +429,84 @@ final class ChecklistController extends OCSController {
});
}
/**
* Restore a soft-deleted item back into the active list
*
* @param int $houseId House id.
* @param int $listId List id.
* @param int $itemId Item id.
*
* @return DataResponse<Http::STATUS_OK, PantryListItem, array{}>
*
* 200: Item restored
*/
#[ApiRoute(verb: 'POST', url: '/api/houses/{houseId}/lists/{listId}/items/{itemId}/restore')]
#[NoAdminRequired]
public function restoreItem(int $houseId, int $listId, int $itemId): DataResponse {
return $this->runAction(function () use ($houseId, $listId, $itemId): DataResponse {
$this->auth->requireMember($houseId, $this->requireUid());
$item = $this->lists->getItem($itemId, includeDeleted: true);
$list = $this->lists->getList($item->getListId());
$this->assertListInHouse($list->getHouseId(), $houseId);
if ($item->getListId() !== $listId) {
throw new NotFoundException('Item does not belong to this list');
}
$restored = $this->lists->restoreItem($itemId);
return new DataResponse($restored->jsonSerialize());
});
}
/**
* Permanently delete an item, bypassing the trash
*
* Works on both live items and items already in trash.
*
* @param int $houseId House id.
* @param int $listId List id.
* @param int $itemId Item id.
*
* @return DataResponse<Http::STATUS_OK, PantrySuccess, array{}>
*
* 200: Item permanently deleted
*/
#[ApiRoute(verb: 'DELETE', url: '/api/houses/{houseId}/lists/{listId}/items/{itemId}/permanent')]
#[NoAdminRequired]
public function permanentlyDeleteItem(int $houseId, int $listId, int $itemId): DataResponse {
return $this->runAction(function () use ($houseId, $listId, $itemId): DataResponse {
$this->auth->requireMember($houseId, $this->requireUid());
$item = $this->lists->getItem($itemId, includeDeleted: true);
$list = $this->lists->getList($item->getListId());
$this->assertListInHouse($list->getHouseId(), $houseId);
if ($item->getListId() !== $listId) {
throw new NotFoundException('Item does not belong to this list');
}
$this->lists->permanentlyDeleteItem($itemId);
return new DataResponse(['success' => true]);
});
}
/**
* Empty a list's trash, permanently deleting every soft-deleted item
*
* @param int $houseId House id.
* @param int $listId List id.
*
* @return DataResponse<Http::STATUS_OK, PantrySuccess, array{}>
*
* 200: Trash emptied
*/
#[ApiRoute(verb: 'DELETE', url: '/api/houses/{houseId}/lists/{listId}/items/trash')]
#[NoAdminRequired]
public function emptyTrash(int $houseId, int $listId): DataResponse {
return $this->runAction(function () use ($houseId, $listId): DataResponse {
$this->auth->requireMember($houseId, $this->requireUid());
$list = $this->lists->getList($listId);
$this->assertListInHouse($list->getHouseId(), $houseId);
$this->lists->emptyTrash($listId);
return new DataResponse(['success' => true]);
});
}
/**
* Batch reorder items in a list
*

View File

@@ -135,10 +135,37 @@ class ChecklistItemMapper extends QBMapper {
return $this->findEntities($qb);
}
/**
* Find soft-deleted items in a list, most recently deleted first.
*
* @return ChecklistItem[]
*/
public function findDeletedByList(int $listId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('list_id', $qb->createNamedParameter($listId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->isNotNull('deleted_at'))
->orderBy('deleted_at', 'DESC');
return $this->findEntities($qb);
}
public function deleteByList(int $listId): void {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName())
->where($qb->expr()->eq('list_id', $qb->createNamedParameter($listId, IQueryBuilder::PARAM_INT)));
$qb->executeStatement();
}
/**
* Hard-delete every soft-deleted item in the list.
*/
public function emptyTrashForList(int $listId): void {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName())
->where($qb->expr()->eq('list_id', $qb->createNamedParameter($listId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->isNotNull('deleted_at'));
$qb->executeStatement();
}
}

View File

@@ -106,9 +106,18 @@ class ChecklistService {
return $this->itemMapper->findByList($listId, $sortBy);
}
public function getItem(int $itemId): ChecklistItem {
/**
* List soft-deleted items for a list. Most recently deleted first.
*
* @return ChecklistItem[]
*/
public function listDeletedItems(int $listId): array {
return $this->itemMapper->findDeletedByList($listId);
}
public function getItem(int $itemId, bool $includeDeleted = false): ChecklistItem {
try {
return $this->itemMapper->findById($itemId);
return $this->itemMapper->findById($itemId, $includeDeleted);
} catch (DoesNotExistException) {
throw new NotFoundException('Item not found');
}
@@ -360,6 +369,33 @@ class ChecklistService {
$this->itemMapper->update($item);
}
/**
* Permanently remove an item, regardless of whether it is currently in
* trash. Bypasses the soft-delete row and erases it from the table.
*/
public function permanentlyDeleteItem(int $itemId): void {
$item = $this->getItem($itemId, includeDeleted: true);
$this->itemMapper->delete($item);
}
/**
* Restore a soft-deleted item by clearing its deleted_at marker.
*/
public function restoreItem(int $itemId): ChecklistItem {
$item = $this->getItem($itemId, includeDeleted: true);
$item->setDeletedAt(null);
$item->setUpdatedAt(time());
$this->itemMapper->update($item);
return $item;
}
/**
* Hard-delete every soft-deleted item in the list.
*/
public function emptyTrash(int $listId): void {
$this->itemMapper->emptyTrashForList($listId);
}
private function strOrNull(mixed $v): ?string {
if (!is_string($v)) {
return null;

View File

@@ -336,7 +336,8 @@
"imageUploadedBy",
"sortOrder",
"createdAt",
"updatedAt"
"updatedAt",
"deletedAt"
],
"properties": {
"id": {
@@ -410,6 +411,11 @@
"updatedAt": {
"type": "integer",
"format": "int64"
},
"deletedAt": {
"type": "integer",
"format": "int64",
"nullable": true
}
}
},
@@ -2100,6 +2106,249 @@
}
}
},
"/ocs/v2.php/apps/pantry/api/houses/{houseId}/lists/{listId}/items/trash": {
"get": {
"operationId": "checklist-index-deleted-items",
"summary": "List soft-deleted items in a checklist (trash)",
"description": "Returns items whose deleted_at is set, most recently deleted first.",
"tags": [
"checklist"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "houseId",
"in": "path",
"description": "House id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "listId",
"in": "path",
"description": "List id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "limit",
"in": "query",
"description": "Maximum number of items to return.",
"schema": {
"type": "integer",
"format": "int64",
"default": 200,
"minimum": 1,
"maximum": 1000
}
},
{
"name": "offset",
"in": "query",
"description": "Number of items to skip.",
"schema": {
"type": "integer",
"format": "int64",
"default": 0,
"minimum": 0
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Deleted items returned",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ListItem"
}
}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
},
"delete": {
"operationId": "checklist-empty-trash",
"summary": "Empty a list's trash, permanently deleting every soft-deleted item",
"tags": [
"checklist"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "houseId",
"in": "path",
"description": "House id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "listId",
"in": "path",
"description": "List id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Trash emptied",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Success"
}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/pantry/api/houses/{houseId}/lists/{listId}/items/{itemId}": {
"patch": {
"operationId": "checklist-update-item",
@@ -2531,6 +2780,245 @@
}
}
},
"/ocs/v2.php/apps/pantry/api/houses/{houseId}/lists/{listId}/items/{itemId}/restore": {
"post": {
"operationId": "checklist-restore-item",
"summary": "Restore a soft-deleted item back into the active list",
"tags": [
"checklist"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "houseId",
"in": "path",
"description": "House id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "listId",
"in": "path",
"description": "List id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "itemId",
"in": "path",
"description": "Item id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Item restored",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/ListItem"
}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/pantry/api/houses/{houseId}/lists/{listId}/items/{itemId}/permanent": {
"delete": {
"operationId": "checklist-permanently-delete-item",
"summary": "Permanently delete an item, bypassing the trash",
"description": "Works on both live items and items already in trash.",
"tags": [
"checklist"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "houseId",
"in": "path",
"description": "House id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "listId",
"in": "path",
"description": "List id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "itemId",
"in": "path",
"description": "Item id.",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Item permanently deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Success"
}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/pantry/api/houses/{houseId}/lists/{listId}/items/reorder": {
"post": {
"operationId": "checklist-reorder-items",

View File

@@ -49,6 +49,11 @@ export async function listItems(
return resp.data ?? []
}
export async function listDeletedItems(houseId: number, listId: number): Promise<ChecklistItem[]> {
const resp = await ocs.get<ChecklistItem[]>(`/houses/${houseId}/lists/${listId}/items/trash`)
return resp.data ?? []
}
export interface ItemInput {
name: string
description?: string | null
@@ -98,6 +103,29 @@ export async function deleteItem(houseId: number, listId: number, itemId: number
await ocs.delete(`/houses/${houseId}/lists/${listId}/items/${itemId}`)
}
export async function permanentlyDeleteItem(
houseId: number,
listId: number,
itemId: number,
): Promise<void> {
await ocs.delete(`/houses/${houseId}/lists/${listId}/items/${itemId}/permanent`)
}
export async function restoreItem(
houseId: number,
listId: number,
itemId: number,
): Promise<ChecklistItem> {
const resp = await ocs.post<ChecklistItem>(
`/houses/${houseId}/lists/${listId}/items/${itemId}/restore`,
)
return resp.data
}
export async function emptyTrash(houseId: number, listId: number): Promise<void> {
await ocs.delete(`/houses/${houseId}/lists/${listId}/items/trash`)
}
export async function reorderItems(
houseId: number,
listId: number,

View File

@@ -52,11 +52,17 @@
</template>
{{ strings.moveItem }}
</NcActionButton>
<NcActionButton v-if="trashMode" close-after-click @click="$emit('restore', item.id)">
<template #icon>
<DeleteRestoreIcon :size="20" />
</template>
{{ strings.restoreItem }}
</NcActionButton>
<NcActionButton close-after-click @click="$emit('remove', item.id)">
<template #icon>
<DeleteIcon :size="20" />
</template>
{{ strings.removeItem }}
{{ trashMode ? strings.deletePermanently : strings.removeItem }}
</NcActionButton>
</NcActions>
</div>
@@ -74,6 +80,7 @@ import RepeatIcon from '@icons/Repeat.vue'
import PencilIcon from '@icons/Pencil.vue'
import EyeIcon from '@icons/Eye.vue'
import DeleteIcon from '@icons/Delete.vue'
import DeleteRestoreIcon from '@icons/DeleteRestore.vue'
import ArrowRightIcon from '@icons/ArrowRight.vue'
import { categoryIconComponent } from '@/components/CategoryPicker'
import { itemImagePreviewUrl } from '@/api/images'
@@ -86,8 +93,9 @@ const props = withDefaults(
category: Category | null
houseId: number
reorderEnabled?: boolean
trashMode?: boolean
}>(),
{ reorderEnabled: false },
{ reorderEnabled: false, trashMode: false },
)
const emit = defineEmits<{
@@ -96,6 +104,7 @@ const emit = defineEmits<{
edit: [item: ChecklistItem]
move: [item: ChecklistItem]
remove: [id: number]
restore: [id: number]
preview: [item: ChecklistItem]
'drag-start': [itemId: number]
'reorder-over': [itemId: number, event: MouseEvent]
@@ -143,6 +152,8 @@ const strings = {
editItem: t('pantry', 'Edit item'),
moveItem: t('pantry', 'Move to list'),
removeItem: t('pantry', 'Remove item'),
deletePermanently: t('pantry', 'Delete permanently'),
restoreItem: t('pantry', 'Restore'),
}
</script>

View File

@@ -82,13 +82,16 @@ export function useChecklistItems(houseId: number, listId: number) {
const loading = ref(false)
const error = ref<string | null>(null)
const sortBy = ref<ChecklistItemSort>('custom')
const trashMode = ref(false)
async function load(sort?: ChecklistItemSort): Promise<void> {
loading.value = true
error.value = null
const s = sort ?? sortBy.value
try {
items.value = await api.listItems(houseId, listId, s)
items.value = trashMode.value
? await api.listDeletedItems(houseId, listId)
: await api.listItems(houseId, listId, s)
} catch (e) {
error.value = (e as Error).message
} finally {
@@ -150,6 +153,25 @@ export function useChecklistItems(houseId: number, listId: number) {
items.value = items.value.filter((i) => i.id !== itemId)
}
async function removePermanently(itemId: number): Promise<void> {
await api.permanentlyDeleteItem(houseId, listId, itemId)
items.value = items.value.filter((i) => i.id !== itemId)
}
async function restore(itemId: number): Promise<void> {
await api.restoreItem(houseId, listId, itemId)
// The item leaves the current view: in trash mode it returns to the active
// list (and stays hidden here); in active mode it was never visible.
items.value = items.value.filter((i) => i.id !== itemId)
}
async function emptyTrash(): Promise<void> {
await api.emptyTrash(houseId, listId)
if (trashMode.value) {
items.value = []
}
}
async function uploadImage(itemId: number, file: File): Promise<void> {
const updated = await api.uploadItemImage(houseId, listId, itemId, file)
items.value = items.value.map((i) => (i.id === itemId ? updated : i))
@@ -165,12 +187,16 @@ export function useChecklistItems(houseId: number, listId: number) {
loading,
error,
sortBy,
trashMode,
load,
add,
update,
toggle,
reorderItems,
remove,
removePermanently,
restore,
emptyTrash,
uploadImage,
clearImage,
}

View File

@@ -13,7 +13,7 @@
</NcButton>
</template>
<template #actions>
<NcActions :aria-label="strings.sortLabel" type="tertiary">
<NcActions :aria-label="strings.sortLabel" :title="strings.sortLabel" type="tertiary">
<template #icon>
<SortIcon :size="20" />
</template>
@@ -30,6 +30,18 @@
{{ opt.label }}
</NcActionButton>
</NcActions>
<NcButton
:variant="trashMode ? 'primary' : 'tertiary'"
:aria-label="strings.trashLabel"
:title="strings.trashLabel"
:aria-pressed="trashMode"
@click="toggleTrash"
>
<template #icon>
<TrashCanIcon :size="20" />
</template>
{{ strings.trashLabel }}
</NcButton>
<NcButton variant="primary" @click="showCategoryManager = true">
<template #icon>
<TagIcon :size="20" />
@@ -57,15 +69,24 @@
<NcEmptyContent
v-else-if="items.length === 0"
:name="strings.emptyTitle"
:description="strings.emptyBody"
:name="trashMode ? strings.trashEmptyTitle : strings.emptyTitle"
:description="trashMode ? strings.trashEmptyBody : strings.emptyBody"
>
<template #icon>
<component :is="checklistIconComponent(list?.icon)" />
<TrashCanIcon v-if="trashMode" />
<component :is="checklistIconComponent(list?.icon)" v-else />
</template>
</NcEmptyContent>
<template v-else>
<div v-if="trashMode" class="pantry-detail__trash-bar">
<NcButton variant="error" @click="confirmingEmptyTrash = true">
<template #icon>
<TrashCanIcon :size="20" />
</template>
{{ strings.emptyTrashAction }}
</NcButton>
</div>
<ul v-if="uncheckedItems.length > 0" ref="uncheckedListRef" class="pantry-detail__items">
<template v-for="gi in uncheckedGridItems" :key="gi.key">
<li
@@ -80,11 +101,13 @@
:category="categoryFor(gi.item.categoryId)"
:house-id="houseIdNum"
:reorder-enabled="isCustomSort"
:trash-mode="trashMode"
@toggle="handleToggle"
@view="openView"
@edit="startEdit"
@move="startMoveItem"
@remove="handleRemove"
@restore="handleRestore"
@preview="openPreview"
@drag-start="onItemDragStart"
@reorder-over="onReorderOver"
@@ -186,6 +209,22 @@
@update:open="showCreateForMove = $event"
@save="submitCreateListAndMove"
/>
<NcDialog
v-if="confirmingEmptyTrash"
:name="strings.emptyTrashTitle"
:open="confirmingEmptyTrash"
close-on-click-outside
@update:open="(v) => !v && (confirmingEmptyTrash = false)"
>
<p>{{ strings.emptyTrashConfirm }}</p>
<template #actions>
<NcButton @click="confirmingEmptyTrash = false">{{ strings.cancel }}</NcButton>
<NcButton variant="error" @click="submitEmptyTrash">
{{ strings.emptyTrashAction }}
</NcButton>
</template>
</NcDialog>
</div>
</template>
@@ -204,6 +243,7 @@ import SortIcon from '@icons/Sort.vue'
import RadioboxBlankIcon from '@icons/RadioboxBlank.vue'
import RadioboxMarkedIcon from '@icons/RadioboxMarked.vue'
import TagIcon from '@icons/Tag.vue'
import TrashCanIcon from '@icons/TrashCan.vue'
import PageToolbar from '@/components/PageToolbar'
import { ChecklistAddForm } from '@/components/ChecklistAddForm'
import { ChecklistFilter } from '@/components/ChecklistFilter'
@@ -237,10 +277,26 @@ const {
toggle,
reorderItems,
remove,
removePermanently,
restore,
emptyTrash,
uploadImage,
clearImage,
sortBy,
trashMode,
} = useChecklistItems(houseIdNum.value, listIdNum.value)
async function toggleTrash() {
trashMode.value = !trashMode.value
await load()
}
const confirmingEmptyTrash = ref(false)
async function submitEmptyTrash() {
confirmingEmptyTrash.value = false
await emptyTrash()
}
const categories = useCategories(houseIdNum.value)
function categoryFor(id: number | null) {
@@ -531,7 +587,15 @@ async function handleToggle(itemId: number) {
}
async function handleRemove(itemId: number) {
await remove(itemId)
if (trashMode.value) {
await removePermanently(itemId)
} else {
await remove(itemId)
}
}
async function handleRestore(itemId: number) {
await restore(itemId)
}
// ----- Edit -----
@@ -617,11 +681,21 @@ const strings = {
back: t('pantry', 'Back to lists'),
emptyTitle: t('pantry', 'No items yet'),
emptyBody: t('pantry', 'Add items using the form above.'),
trashEmptyTitle: t('pantry', 'Trash is empty'),
trashEmptyBody: t('pantry', 'Deleted items will appear here.'),
sortLabel: t('pantry', 'Sort order'),
trashLabel: t('pantry', 'Trash'),
doneTitle: t('pantry', 'Done'),
manageCategories: t('pantry', 'Manage categories'),
moveToList: t('pantry', 'Move to list'),
newList: t('pantry', 'New list'),
emptyTrashAction: t('pantry', 'Empty trash'),
emptyTrashTitle: t('pantry', 'Empty trash?'),
emptyTrashConfirm: t(
'pantry',
'All deleted items in this list will be permanently removed. This cannot be undone.',
),
cancel: t('pantry', 'Cancel'),
}
</script>
@@ -652,6 +726,12 @@ const strings = {
gap: 0.25rem;
}
&__trash-bar {
display: flex;
justify-content: flex-end;
margin-bottom: 0.75rem;
}
&__placeholder {
min-height: 48px;
border: 3px dashed var(--color-primary-element);