From cb2cb731aa8ac64d700c3af8d3ee633a311d0903 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Sun, 12 Apr 2026 11:06:23 +0300 Subject: [PATCH] feat: move items between lists --- lib/Controller/ChecklistController.php | 9 +- lib/Service/ChecklistService.php | 6 + openapi.json | 7 ++ src/api/lists.ts | 1 + .../ChecklistFormDialog.vue | 109 ++++++++++++++++++ src/components/ChecklistIconPicker/index.ts | 1 + .../ChecklistItemRow/ChecklistItemRow.vue | 9 ++ src/views/ChecklistDetail.vue | 74 +++++++++++- 8 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 src/components/ChecklistIconPicker/ChecklistFormDialog.vue diff --git a/lib/Controller/ChecklistController.php b/lib/Controller/ChecklistController.php index 575d58d..df34843 100644 --- a/lib/Controller/ChecklistController.php +++ b/lib/Controller/ChecklistController.php @@ -265,6 +265,7 @@ final class ChecklistController extends OCSController { * @param bool|null $repeatFromCompletion New recurrence anchor mode. * @param int|null $imageFileId File id of attached image (0 or negative clears). * @param int|null $sortOrder New sort order. + * @param int|null $targetListId Move item to a different list (must belong to the same house). * * @return DataResponse * @@ -284,8 +285,9 @@ final class ChecklistController extends OCSController { ?bool $repeatFromCompletion = null, ?int $imageFileId = null, ?int $sortOrder = null, + ?int $targetListId = null, ): DataResponse { - return $this->runAction(function () use ($houseId, $listId, $itemId, $name, $description, $categoryId, $quantity, $rrule, $repeatFromCompletion, $imageFileId, $sortOrder): DataResponse { + return $this->runAction(function () use ($houseId, $listId, $itemId, $name, $description, $categoryId, $quantity, $rrule, $repeatFromCompletion, $imageFileId, $sortOrder, $targetListId): DataResponse { $this->auth->requireMember($houseId, $this->requireUid()); $item = $this->lists->getItem($itemId); $list = $this->lists->getList($item->getListId()); @@ -323,6 +325,11 @@ final class ChecklistController extends OCSController { if ($sortOrder !== null) { $patch['sortOrder'] = $sortOrder; } + if ($targetListId !== null) { + $targetList = $this->lists->getList($targetListId); + $this->assertListInHouse($targetList->getHouseId(), $houseId); + $patch['listId'] = $targetListId; + } $updated = $this->lists->updateItem($itemId, $patch); return new DataResponse($updated->jsonSerialize()); }); diff --git a/lib/Service/ChecklistService.php b/lib/Service/ChecklistService.php index 5718b42..f880689 100644 --- a/lib/Service/ChecklistService.php +++ b/lib/Service/ChecklistService.php @@ -206,6 +206,12 @@ class ChecklistService { && (array_key_exists('rrule', $patch) || array_key_exists('repeatFromCompletion', $patch))) { $item->setNextDueAt($this->computeNextDueAt($item, time())?->getTimestamp()); } + if (isset($patch['listId'])) { + $targetListId = (int)$patch['listId']; + // Ensure the target list exists. + $this->getList($targetListId); + $item->setListId($targetListId); + } if (isset($patch['sortOrder'])) { $item->setSortOrder((int)$patch['sortOrder']); } diff --git a/openapi.json b/openapi.json index 1958af5..a0794c8 100644 --- a/openapi.json +++ b/openapi.json @@ -2123,6 +2123,13 @@ "nullable": true, "default": null, "description": "New sort order." + }, + "targetListId": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": null, + "description": "Move item to a different list (must belong to the same house)." } } } diff --git a/src/api/lists.ts b/src/api/lists.ts index d0bf8d6..8603745 100644 --- a/src/api/lists.ts +++ b/src/api/lists.ts @@ -57,6 +57,7 @@ export interface ItemInput { rrule?: string | null repeatFromCompletion?: boolean sortOrder?: number + targetListId?: number } export async function addItem( diff --git a/src/components/ChecklistIconPicker/ChecklistFormDialog.vue b/src/components/ChecklistIconPicker/ChecklistFormDialog.vue new file mode 100644 index 0000000..605b849 --- /dev/null +++ b/src/components/ChecklistIconPicker/ChecklistFormDialog.vue @@ -0,0 +1,109 @@ + + + diff --git a/src/components/ChecklistIconPicker/index.ts b/src/components/ChecklistIconPicker/index.ts index 7f31ae1..2d21b38 100644 --- a/src/components/ChecklistIconPicker/index.ts +++ b/src/components/ChecklistIconPicker/index.ts @@ -4,3 +4,4 @@ export { checklistIconComponent, type ChecklistIconOption, } from './checklistIcons' +export { default as ChecklistFormDialog } from './ChecklistFormDialog.vue' diff --git a/src/components/ChecklistItemRow/ChecklistItemRow.vue b/src/components/ChecklistItemRow/ChecklistItemRow.vue index a8a8bf5..a85723c 100644 --- a/src/components/ChecklistItemRow/ChecklistItemRow.vue +++ b/src/components/ChecklistItemRow/ChecklistItemRow.vue @@ -46,6 +46,12 @@ {{ strings.editItem }} + + + {{ strings.moveItem }} + @@ -152,11 +183,13 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { t } from '@nextcloud/l10n' import NcButton from '@nextcloud/vue/components/NcButton' +import NcDialog from '@nextcloud/vue/components/NcDialog' import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' import NcActions from '@nextcloud/vue/components/NcActions' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import ArrowLeftIcon from '@icons/ArrowLeft.vue' +import PlusIcon from '@icons/Plus.vue' import SortIcon from '@icons/Sort.vue' import RadioboxBlankIcon from '@icons/RadioboxBlank.vue' import RadioboxMarkedIcon from '@icons/RadioboxMarked.vue' @@ -168,8 +201,8 @@ import { ChecklistItemEditDialog } from '@/components/ChecklistItemEditDialog' import { ChecklistItemViewDialog } from '@/components/ChecklistItemViewDialog' import { ChecklistImagePreview } from '@/components/ChecklistImagePreview' import { CategoryManagerDialog } from '@/components/CategoryManager' -import { checklistIconComponent } from '@/components/ChecklistIconPicker' -import { useChecklistItems } from '@/composables/useChecklist' +import { checklistIconComponent, ChecklistFormDialog } from '@/components/ChecklistIconPicker' +import { useChecklists, useChecklistItems } from '@/composables/useChecklist' import { useCategories } from '@/composables/useCategories' import { useTouchReorder } from '@/composables/useTouchReorder' import { getList } from '@/api/lists' @@ -517,6 +550,34 @@ function openPreview(item: ChecklistItem) { const showCategoryManager = ref(false) +// ----- Move item to another list ----- + +const { lists: allLists, create: createList } = useChecklists(houseIdNum.value) +const otherLists = computed(() => allLists.value.filter((l) => l.id !== listIdNum.value)) +const movingItem = ref(null) +const showCreateForMove = ref(false) + +function startMoveItem(item: ChecklistItem) { + movingItem.value = item +} + +async function submitMoveItem(targetListId: number) { + if (!movingItem.value) return + await update(movingItem.value.id, { targetListId }) + items.value = items.value.filter((i) => i.id !== movingItem.value!.id) + movingItem.value = null +} + +function createListForMove() { + showCreateForMove.value = true +} + +async function submitCreateListAndMove(data: { name: string; description: string; icon: string }) { + const newList = await createList(data.name, data.description || null, data.icon || null) + showCreateForMove.value = false + await submitMoveItem(newList.id) +} + const strings = { back: t('pantry', 'Back to lists'), emptyTitle: t('pantry', 'No items yet'), @@ -524,6 +585,8 @@ const strings = { sortLabel: t('pantry', 'Sort order'), doneTitle: t('pantry', 'Done'), manageCategories: t('pantry', 'Manage categories'), + moveToList: t('pantry', 'Move to list'), + newList: t('pantry', 'New list'), } @@ -568,6 +631,13 @@ const strings = { } } +.pantry-move-list { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.5rem 0; +} + .pantry-sort-active { font-weight: 600; }