feat: edit/delete lists, label updates, workflow fix

This commit is contained in:
2026-04-06 01:40:22 +03:00
parent cac1159588
commit fdf4b006c0
9 changed files with 163 additions and 28 deletions

View File

@@ -101,6 +101,7 @@ jobs:
name: Release to Nextcloud Apps
runs-on: ubuntu-latest
needs: [build, release, upload]
if: ${{ needs.release.outputs.release_created }}
steps:
- name: Checkout
@@ -116,7 +117,6 @@ jobs:
echo -n "${{ secrets.NEXTCLOUD_APP_PRIVATE_KEY }}" > ~/.nextcloud/certificates/pantry.key
- name: Release to Nextcloud Apps
if: ${{ needs.release.outputs.release_created }}
env:
NEXTCLOUD_API_TOKEN: ${{ secrets.NEXTCLOUD_API_TOKEN }}
run: |

View File

@@ -62,6 +62,12 @@ class ShoppingListItem extends Entity implements \JsonSerializable {
$this->addType('sortOrder', 'integer');
$this->addType('createdAt', 'integer');
$this->addType('updatedAt', 'integer');
// Force these bool fields to be included in INSERTs. Their PHP defaults
// match the initial value, so the magic setter would otherwise never
// mark them dirty and the column would be omitted from the INSERT.
// fromRow() resets updated fields after hydration, so reads are unaffected.
$this->markFieldUpdated('bought');
$this->markFieldUpdated('repeatFromCompletion');
}
public function jsonSerialize(): array {

View File

@@ -231,10 +231,10 @@ function iconFor(key: string) {
const strings = {
placeholder: t('pantry', 'Pick a category'),
createTitle: t('pantry', 'New category'),
nameLabel: t('pantry', 'Name:'),
nameLabel: t('pantry', 'Name'),
namePlaceholder: t('pantry', 'e.g. Produce, Dairy'),
iconLabel: t('pantry', 'Icon:'),
colorLabel: t('pantry', 'Color:'),
iconLabel: t('pantry', 'Icon'),
colorLabel: t('pantry', 'Color'),
create: t('pantry', 'Create'),
saving: t('pantry', 'Saving …'),
cancel: t('pantry', 'Cancel'),

View File

@@ -458,19 +458,19 @@ function clear(): void {
const strings = {
title: t('pantry', 'Recurrence'),
presetsLabel: t('pantry', 'Presets:'),
frequencyLabel: t('pantry', 'Unit:'),
everyLabel: t('pantry', 'Every:'),
weekdaysLabel: t('pantry', 'Repeat on:'),
monthDaysLabel: t('pantry', 'Days of the month:'),
presetsLabel: t('pantry', 'Presets'),
frequencyLabel: t('pantry', 'Unit'),
everyLabel: t('pantry', 'Every'),
weekdaysLabel: t('pantry', 'Repeat on'),
monthDaysLabel: t('pantry', 'Days of the month'),
monthDaysHint: t('pantry', 'Leave empty to repeat on the same day each month.'),
endsLabel: t('pantry', 'Ends:'),
endsLabel: t('pantry', 'Ends'),
endNever: t('pantry', 'Never'),
endAfter: t('pantry', 'After'),
endAfterSuffix: t('pantry', 'occurrences'),
endOn: t('pantry', 'On date'),
fromCompletionLabel: t('pantry', 'Count interval from when the item is ticked off'),
summaryLabel: t('pantry', 'Summary:'),
summaryLabel: t('pantry', 'Summary'),
cancel: t('pantry', 'Cancel'),
save: t('pantry', 'Save'),
clearButton: t('pantry', 'Remove recurrence'),

View File

@@ -25,8 +25,11 @@ export function useShoppingLists(houseId: number) {
return created
}
async function rename(listId: number, name: string): Promise<void> {
const updated = await api.updateList(houseId, listId, { name })
async function update(
listId: number,
patch: { name?: string; description?: string | null },
): Promise<void> {
const updated = await api.updateList(houseId, listId, patch)
lists.value = lists.value.map((l) => (l.id === listId ? updated : l))
}
@@ -35,7 +38,7 @@ export function useShoppingLists(houseId: number) {
lists.value = lists.value.filter((l) => l.id !== listId)
}
return { lists, loading, error, load, create, rename, remove }
return { lists, loading, error, load, create, update, remove }
}
export function useShoppingListItems(houseId: number, listId: number) {

View File

@@ -98,9 +98,9 @@ async function deleteHouse() {
const strings = {
title: t('pantry', 'House settings'),
nameLabel: t('pantry', 'Name:'),
nameLabel: t('pantry', 'Name'),
namePlaceholder: t('pantry', 'House name'),
descriptionLabel: t('pantry', 'Description:'),
descriptionLabel: t('pantry', 'Description'),
descriptionPlaceholder: t('pantry', 'A short description'),
save: t('pantry', 'Save changes'),
saving: t('pantry', 'Saving …'),

View File

@@ -186,9 +186,9 @@ const strings = {
colRole: t('pantry', 'Role'),
colJoined: t('pantry', 'Joined'),
addDialogTitle: t('pantry', 'Add a member'),
userIdLabel: t('pantry', 'Account ID:'),
userIdLabel: t('pantry', 'Account ID'),
userIdPlaceholder: t('pantry', 'The Nextcloud username'),
roleLabel: t('pantry', 'Role:'),
roleLabel: t('pantry', 'Role'),
cancel: t('pantry', 'Cancel'),
}
</script>

View File

@@ -30,7 +30,7 @@
</NcEmptyContent>
<ul v-else class="pantry-lists__grid">
<li v-for="list in lists" :key="list.id">
<li v-for="list in lists" :key="list.id" class="pantry-list-card-wrap">
<router-link
:to="{
name: 'list-detail',
@@ -44,6 +44,16 @@
<p v-if="list.description">{{ list.description }}</p>
</div>
</router-link>
<NcActions class="pantry-list-card__actions" :aria-label="strings.listMenu">
<NcActionButton @click="startEdit(list)">
<template #icon><PencilIcon :size="20" /></template>
{{ strings.edit }}
</NcActionButton>
<NcActionButton @click="confirmDelete(list)">
<template #icon><DeleteIcon :size="20" /></template>
{{ strings.delete }}
</NcActionButton>
</NcActions>
</li>
</ul>
@@ -53,7 +63,7 @@
:open="showCreate"
@update:open="showCreate = $event"
>
<form class="pantry-form" @submit.prevent="submitCreate">
<form id="pantry-create-list-form" class="pantry-form" @submit.prevent="submitCreate">
<NcTextField
v-model="newName"
:label="strings.nameLabel"
@@ -67,11 +77,56 @@
</form>
<template #actions>
<NcButton @click="showCreate = false">{{ strings.cancel }}</NcButton>
<NcButton variant="primary" :disabled="!newName.trim()" @click="submitCreate">
<NcButton
form="pantry-create-list-form"
type="submit"
variant="primary"
:disabled="!newName.trim()"
>
{{ strings.create }}
</NcButton>
</template>
</NcDialog>
<NcDialog
v-if="editing"
:name="strings.editDialogTitle"
:open="!!editing"
@update:open="(v) => !v && (editing = null)"
>
<form class="pantry-form" @submit.prevent="submitEdit">
<NcTextField
v-model="editName"
:label="strings.nameLabel"
:placeholder="strings.namePlaceholder"
/>
<NcTextField
v-model="editDescription"
:label="strings.descriptionLabel"
:placeholder="strings.descriptionPlaceholder"
/>
<button type="submit" class="pantry-hidden-submit" aria-hidden="true" tabindex="-1" />
</form>
<template #actions>
<NcButton @click="editing = null">{{ strings.cancel }}</NcButton>
<NcButton variant="primary" :disabled="!editName.trim()" @click="submitEdit">
{{ strings.save }}
</NcButton>
</template>
</NcDialog>
<NcDialog
v-if="deleting"
:name="strings.deleteDialogTitle"
:open="!!deleting"
@update:open="(v) => !v && (deleting = null)"
>
<p>{{ deleteConfirmBody }}</p>
<template #actions>
<NcButton @click="deleting = null">{{ strings.cancel }}</NcButton>
<NcButton variant="error" @click="submitDelete">{{ strings.delete }}</NcButton>
</template>
</NcDialog>
</div>
</template>
@@ -84,15 +139,20 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import PlusIcon from '@icons/Plus.vue'
import CartIcon from '@icons/Cart.vue'
import PencilIcon from '@icons/Pencil.vue'
import DeleteIcon from '@icons/Delete.vue'
import type { ShoppingList } from '@/api/types'
import { useShoppingLists } from '@/composables/useShoppingList'
const props = defineProps<{ houseId: string }>()
const router = useRouter()
const houseIdNum = computed(() => Number(props.houseId))
const { lists, loading, load, create } = useShoppingLists(houseIdNum.value)
const { lists, loading, load, create, update, remove } = useShoppingLists(houseIdNum.value)
onMounted(load)
watch(
@@ -117,15 +177,63 @@ async function submitCreate() {
})
}
const editing = ref<ShoppingList | null>(null)
const editName = ref('')
const editDescription = ref('')
function startEdit(list: ShoppingList) {
editing.value = list
editName.value = list.name
editDescription.value = list.description ?? ''
}
async function submitEdit() {
const target = editing.value
if (!target) return
const name = editName.value.trim()
if (!name) return
await update(target.id, {
name,
description: editDescription.value.trim() || null,
})
editing.value = null
}
const deleting = ref<ShoppingList | null>(null)
const deleteConfirmBody = computed(() =>
t(
'pantry',
'Are you sure you want to delete {name}? All items in this list will also be removed.',
{ name: deleting.value?.name ?? '' },
),
)
function confirmDelete(list: ShoppingList) {
deleting.value = list
}
async function submitDelete() {
const target = deleting.value
if (!target) return
await remove(target.id)
deleting.value = null
}
const strings = {
title: t('pantry', 'Shopping lists'),
newList: t('pantry', 'New list'),
create: t('pantry', 'Create'),
save: t('pantry', 'Save'),
cancel: t('pantry', 'Cancel'),
edit: t('pantry', 'Edit'),
delete: t('pantry', 'Delete'),
listMenu: t('pantry', 'List actions'),
createDialogTitle: t('pantry', 'Create a shopping list'),
nameLabel: t('pantry', 'Name:'),
editDialogTitle: t('pantry', 'Edit shopping list'),
deleteDialogTitle: t('pantry', 'Delete shopping list'),
nameLabel: t('pantry', 'Name'),
namePlaceholder: t('pantry', 'e.g. Weekly groceries'),
descriptionLabel: t('pantry', 'Description (optional):'),
descriptionLabel: t('pantry', 'Description (optional)'),
descriptionPlaceholder: t('pantry', 'A short description'),
emptyTitle: t('pantry', 'No lists yet'),
emptyBody: t('pantry', 'Create your first shopping list to start adding items.'),
@@ -158,10 +266,23 @@ const strings = {
}
}
.pantry-list-card-wrap {
position: relative;
&__actions,
.pantry-list-card__actions {
position: absolute;
top: 0.5rem;
inset-inline-end: 0.5rem;
z-index: 1;
}
}
.pantry-list-card {
display: flex;
gap: 0.75rem;
padding: 1rem;
padding-inline-end: 3rem;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-large, 12px);
background: var(--color-main-background);

View File

@@ -116,7 +116,7 @@
:open="showCreate"
@update:open="showCreate = $event"
>
<form class="pantry-create-form" @submit.prevent="submitCreate">
<form id="pantry-create-house-form" class="pantry-create-form" @submit.prevent="submitCreate">
<NcTextField
v-model="newName"
:label="strings.nameLabel"
@@ -131,7 +131,12 @@
</form>
<template #actions>
<NcButton @click="showCreate = false">{{ strings.cancel }}</NcButton>
<NcButton variant="primary" :disabled="creating || !newName.trim()" @click="submitCreate">
<NcButton
form="pantry-create-house-form"
type="submit"
variant="primary"
:disabled="creating || !newName.trim()"
>
{{ creating ? strings.creating : strings.create }}
</NcButton>
</template>
@@ -264,9 +269,9 @@ const strings = {
createHouse: t('pantry', 'New house …'),
welcomeHint: t('pantry', 'Pick or create a house to get started.'),
createDialogTitle: t('pantry', 'Create a house'),
nameLabel: t('pantry', 'Name:'),
nameLabel: t('pantry', 'Name'),
namePlaceholder: t('pantry', 'e.g. Home, Beach house'),
descriptionLabel: t('pantry', 'Description (optional):'),
descriptionLabel: t('pantry', 'Description (optional)'),
descriptionPlaceholder: t('pantry', 'A short description'),
create: t('pantry', 'Create'),
creating: t('pantry', 'Creating …'),