mirror of
https://github.com/chenasraf/nextcloud-pantry.git
synced 2026-05-17 17:28:01 +00:00
feat: edit/delete lists, label updates, workflow fix
This commit is contained in:
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -101,6 +101,7 @@ jobs:
|
|||||||
name: Release to Nextcloud Apps
|
name: Release to Nextcloud Apps
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build, release, upload]
|
needs: [build, release, upload]
|
||||||
|
if: ${{ needs.release.outputs.release_created }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -116,7 +117,6 @@ jobs:
|
|||||||
echo -n "${{ secrets.NEXTCLOUD_APP_PRIVATE_KEY }}" > ~/.nextcloud/certificates/pantry.key
|
echo -n "${{ secrets.NEXTCLOUD_APP_PRIVATE_KEY }}" > ~/.nextcloud/certificates/pantry.key
|
||||||
|
|
||||||
- name: Release to Nextcloud Apps
|
- name: Release to Nextcloud Apps
|
||||||
if: ${{ needs.release.outputs.release_created }}
|
|
||||||
env:
|
env:
|
||||||
NEXTCLOUD_API_TOKEN: ${{ secrets.NEXTCLOUD_API_TOKEN }}
|
NEXTCLOUD_API_TOKEN: ${{ secrets.NEXTCLOUD_API_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ class ShoppingListItem extends Entity implements \JsonSerializable {
|
|||||||
$this->addType('sortOrder', 'integer');
|
$this->addType('sortOrder', 'integer');
|
||||||
$this->addType('createdAt', 'integer');
|
$this->addType('createdAt', 'integer');
|
||||||
$this->addType('updatedAt', '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 {
|
public function jsonSerialize(): array {
|
||||||
|
|||||||
@@ -231,10 +231,10 @@ function iconFor(key: string) {
|
|||||||
const strings = {
|
const strings = {
|
||||||
placeholder: t('pantry', 'Pick a category'),
|
placeholder: t('pantry', 'Pick a category'),
|
||||||
createTitle: t('pantry', 'New category'),
|
createTitle: t('pantry', 'New category'),
|
||||||
nameLabel: t('pantry', 'Name:'),
|
nameLabel: t('pantry', 'Name'),
|
||||||
namePlaceholder: t('pantry', 'e.g. Produce, Dairy'),
|
namePlaceholder: t('pantry', 'e.g. Produce, Dairy'),
|
||||||
iconLabel: t('pantry', 'Icon:'),
|
iconLabel: t('pantry', 'Icon'),
|
||||||
colorLabel: t('pantry', 'Color:'),
|
colorLabel: t('pantry', 'Color'),
|
||||||
create: t('pantry', 'Create'),
|
create: t('pantry', 'Create'),
|
||||||
saving: t('pantry', 'Saving …'),
|
saving: t('pantry', 'Saving …'),
|
||||||
cancel: t('pantry', 'Cancel'),
|
cancel: t('pantry', 'Cancel'),
|
||||||
|
|||||||
@@ -458,19 +458,19 @@ function clear(): void {
|
|||||||
|
|
||||||
const strings = {
|
const strings = {
|
||||||
title: t('pantry', 'Recurrence'),
|
title: t('pantry', 'Recurrence'),
|
||||||
presetsLabel: t('pantry', 'Presets:'),
|
presetsLabel: t('pantry', 'Presets'),
|
||||||
frequencyLabel: t('pantry', 'Unit:'),
|
frequencyLabel: t('pantry', 'Unit'),
|
||||||
everyLabel: t('pantry', 'Every:'),
|
everyLabel: t('pantry', 'Every'),
|
||||||
weekdaysLabel: t('pantry', 'Repeat on:'),
|
weekdaysLabel: t('pantry', 'Repeat on'),
|
||||||
monthDaysLabel: t('pantry', 'Days of the month:'),
|
monthDaysLabel: t('pantry', 'Days of the month'),
|
||||||
monthDaysHint: t('pantry', 'Leave empty to repeat on the same day each 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'),
|
endNever: t('pantry', 'Never'),
|
||||||
endAfter: t('pantry', 'After'),
|
endAfter: t('pantry', 'After'),
|
||||||
endAfterSuffix: t('pantry', 'occurrences'),
|
endAfterSuffix: t('pantry', 'occurrences'),
|
||||||
endOn: t('pantry', 'On date'),
|
endOn: t('pantry', 'On date'),
|
||||||
fromCompletionLabel: t('pantry', 'Count interval from when the item is ticked off'),
|
fromCompletionLabel: t('pantry', 'Count interval from when the item is ticked off'),
|
||||||
summaryLabel: t('pantry', 'Summary:'),
|
summaryLabel: t('pantry', 'Summary'),
|
||||||
cancel: t('pantry', 'Cancel'),
|
cancel: t('pantry', 'Cancel'),
|
||||||
save: t('pantry', 'Save'),
|
save: t('pantry', 'Save'),
|
||||||
clearButton: t('pantry', 'Remove recurrence'),
|
clearButton: t('pantry', 'Remove recurrence'),
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ export function useShoppingLists(houseId: number) {
|
|||||||
return created
|
return created
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rename(listId: number, name: string): Promise<void> {
|
async function update(
|
||||||
const updated = await api.updateList(houseId, listId, { name })
|
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))
|
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)
|
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) {
|
export function useShoppingListItems(houseId: number, listId: number) {
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ async function deleteHouse() {
|
|||||||
|
|
||||||
const strings = {
|
const strings = {
|
||||||
title: t('pantry', 'House settings'),
|
title: t('pantry', 'House settings'),
|
||||||
nameLabel: t('pantry', 'Name:'),
|
nameLabel: t('pantry', 'Name'),
|
||||||
namePlaceholder: t('pantry', 'House name'),
|
namePlaceholder: t('pantry', 'House name'),
|
||||||
descriptionLabel: t('pantry', 'Description:'),
|
descriptionLabel: t('pantry', 'Description'),
|
||||||
descriptionPlaceholder: t('pantry', 'A short description'),
|
descriptionPlaceholder: t('pantry', 'A short description'),
|
||||||
save: t('pantry', 'Save changes'),
|
save: t('pantry', 'Save changes'),
|
||||||
saving: t('pantry', 'Saving …'),
|
saving: t('pantry', 'Saving …'),
|
||||||
|
|||||||
@@ -186,9 +186,9 @@ const strings = {
|
|||||||
colRole: t('pantry', 'Role'),
|
colRole: t('pantry', 'Role'),
|
||||||
colJoined: t('pantry', 'Joined'),
|
colJoined: t('pantry', 'Joined'),
|
||||||
addDialogTitle: t('pantry', 'Add a member'),
|
addDialogTitle: t('pantry', 'Add a member'),
|
||||||
userIdLabel: t('pantry', 'Account ID:'),
|
userIdLabel: t('pantry', 'Account ID'),
|
||||||
userIdPlaceholder: t('pantry', 'The Nextcloud username'),
|
userIdPlaceholder: t('pantry', 'The Nextcloud username'),
|
||||||
roleLabel: t('pantry', 'Role:'),
|
roleLabel: t('pantry', 'Role'),
|
||||||
cancel: t('pantry', 'Cancel'),
|
cancel: t('pantry', 'Cancel'),
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</NcEmptyContent>
|
</NcEmptyContent>
|
||||||
|
|
||||||
<ul v-else class="pantry-lists__grid">
|
<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
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'list-detail',
|
name: 'list-detail',
|
||||||
@@ -44,6 +44,16 @@
|
|||||||
<p v-if="list.description">{{ list.description }}</p>
|
<p v-if="list.description">{{ list.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -53,7 +63,7 @@
|
|||||||
:open="showCreate"
|
:open="showCreate"
|
||||||
@update:open="showCreate = $event"
|
@update:open="showCreate = $event"
|
||||||
>
|
>
|
||||||
<form class="pantry-form" @submit.prevent="submitCreate">
|
<form id="pantry-create-list-form" class="pantry-form" @submit.prevent="submitCreate">
|
||||||
<NcTextField
|
<NcTextField
|
||||||
v-model="newName"
|
v-model="newName"
|
||||||
:label="strings.nameLabel"
|
:label="strings.nameLabel"
|
||||||
@@ -67,11 +77,56 @@
|
|||||||
</form>
|
</form>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<NcButton @click="showCreate = false">{{ strings.cancel }}</NcButton>
|
<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 }}
|
{{ strings.create }}
|
||||||
</NcButton>
|
</NcButton>
|
||||||
</template>
|
</template>
|
||||||
</NcDialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -84,15 +139,20 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
|||||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
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 PlusIcon from '@icons/Plus.vue'
|
||||||
import CartIcon from '@icons/Cart.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'
|
import { useShoppingLists } from '@/composables/useShoppingList'
|
||||||
|
|
||||||
const props = defineProps<{ houseId: string }>()
|
const props = defineProps<{ houseId: string }>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const houseIdNum = computed(() => Number(props.houseId))
|
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)
|
onMounted(load)
|
||||||
watch(
|
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 = {
|
const strings = {
|
||||||
title: t('pantry', 'Shopping lists'),
|
title: t('pantry', 'Shopping lists'),
|
||||||
newList: t('pantry', 'New list'),
|
newList: t('pantry', 'New list'),
|
||||||
create: t('pantry', 'Create'),
|
create: t('pantry', 'Create'),
|
||||||
|
save: t('pantry', 'Save'),
|
||||||
cancel: t('pantry', 'Cancel'),
|
cancel: t('pantry', 'Cancel'),
|
||||||
|
edit: t('pantry', 'Edit'),
|
||||||
|
delete: t('pantry', 'Delete'),
|
||||||
|
listMenu: t('pantry', 'List actions'),
|
||||||
createDialogTitle: t('pantry', 'Create a shopping list'),
|
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'),
|
namePlaceholder: t('pantry', 'e.g. Weekly groceries'),
|
||||||
descriptionLabel: t('pantry', 'Description (optional):'),
|
descriptionLabel: t('pantry', 'Description (optional)'),
|
||||||
descriptionPlaceholder: t('pantry', 'A short description'),
|
descriptionPlaceholder: t('pantry', 'A short description'),
|
||||||
emptyTitle: t('pantry', 'No lists yet'),
|
emptyTitle: t('pantry', 'No lists yet'),
|
||||||
emptyBody: t('pantry', 'Create your first shopping list to start adding items.'),
|
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 {
|
.pantry-list-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
padding-inline-end: 3rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-large, 12px);
|
border-radius: var(--border-radius-large, 12px);
|
||||||
background: var(--color-main-background);
|
background: var(--color-main-background);
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
:open="showCreate"
|
:open="showCreate"
|
||||||
@update:open="showCreate = $event"
|
@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
|
<NcTextField
|
||||||
v-model="newName"
|
v-model="newName"
|
||||||
:label="strings.nameLabel"
|
:label="strings.nameLabel"
|
||||||
@@ -131,7 +131,12 @@
|
|||||||
</form>
|
</form>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<NcButton @click="showCreate = false">{{ strings.cancel }}</NcButton>
|
<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 }}
|
{{ creating ? strings.creating : strings.create }}
|
||||||
</NcButton>
|
</NcButton>
|
||||||
</template>
|
</template>
|
||||||
@@ -264,9 +269,9 @@ const strings = {
|
|||||||
createHouse: t('pantry', 'New house …'),
|
createHouse: t('pantry', 'New house …'),
|
||||||
welcomeHint: t('pantry', 'Pick or create a house to get started.'),
|
welcomeHint: t('pantry', 'Pick or create a house to get started.'),
|
||||||
createDialogTitle: t('pantry', 'Create a house'),
|
createDialogTitle: t('pantry', 'Create a house'),
|
||||||
nameLabel: t('pantry', 'Name:'),
|
nameLabel: t('pantry', 'Name'),
|
||||||
namePlaceholder: t('pantry', 'e.g. Home, Beach house'),
|
namePlaceholder: t('pantry', 'e.g. Home, Beach house'),
|
||||||
descriptionLabel: t('pantry', 'Description (optional):'),
|
descriptionLabel: t('pantry', 'Description (optional)'),
|
||||||
descriptionPlaceholder: t('pantry', 'A short description'),
|
descriptionPlaceholder: t('pantry', 'A short description'),
|
||||||
create: t('pantry', 'Create'),
|
create: t('pantry', 'Create'),
|
||||||
creating: t('pantry', 'Creating …'),
|
creating: t('pantry', 'Creating …'),
|
||||||
|
|||||||
Reference in New Issue
Block a user