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
|
||||
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: |
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 …'),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 …'),
|
||||
|
||||
Reference in New Issue
Block a user