mirror of
https://github.com/chenasraf/nextcloud-pantry.git
synced 2026-05-17 17:28:01 +00:00
feat(checklist): trash mode toggle to view deleted items
This commit is contained in:
@@ -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
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
490
openapi.json
490
openapi.json
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user