mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
Feature: More Shopping List Improvements (#2164)
* added color back to labels * improved mobile view refactored layout to use grid allowed text wrapping on item labels removed label overflow added completion date on checked items * sort checked items by last updated * made checking an item off more responsive * optimized moving checked items removed unnecessary updateAll call removed jitter when shopping list refreshes
This commit is contained in:
parent
2e6ad5da8e
commit
8ca0fe42de
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<v-chip v-bind="$attrs" label :color="label.color || undefined" :text-color="textColor">
|
||||
{{ label.name }}
|
||||
<span style="max-width: 100%; overflow: hidden; text-overflow: ellipsis;">
|
||||
{{ label.name }}
|
||||
</span>
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="d-flex justify-space-between align-center mx-2">
|
||||
<div class="handle">
|
||||
<span class="mr-2">
|
||||
<v-icon>
|
||||
<v-icon :color="labelColor">
|
||||
{{ $globals.icons.tags }}
|
||||
</v-icon>
|
||||
</span>
|
||||
@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/group";
|
||||
|
||||
interface actions {
|
||||
@ -42,9 +42,15 @@ export default defineComponent({
|
||||
type: Object as () => ShoppingListMultiPurposeLabelOut,
|
||||
required: true,
|
||||
},
|
||||
useColor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
const labelColor = ref<string | undefined>(props.useColor ? props.value.label.color : undefined);
|
||||
|
||||
const contextMenu: actions[] = [
|
||||
{
|
||||
text: i18n.t("general.transfer") as string,
|
||||
@ -59,6 +65,7 @@ export default defineComponent({
|
||||
return {
|
||||
contextHandler,
|
||||
contextMenu,
|
||||
labelColor,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -1,43 +1,59 @@
|
||||
<template>
|
||||
<div v-if="!edit" class="d-flex justify-space-between align-center">
|
||||
<v-checkbox
|
||||
v-model="listItem.checked"
|
||||
class="mt-0"
|
||||
color="null"
|
||||
hide-details
|
||||
dense
|
||||
:label="listItem.note"
|
||||
@change="$emit('checked', listItem)"
|
||||
>
|
||||
<template #label>
|
||||
<div :class="listItem.checked ? 'strike-through' : ''">
|
||||
{{ listItem.display }}
|
||||
</div>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
<MultiPurposeLabel v-if="label && showLabel" :label="label" class="ml-auto" small />
|
||||
<div style="min-width: 72px">
|
||||
<v-menu offset-x left min-width="125px">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn small class="ml-2 handle" icon v-bind="attrs" v-on="on">
|
||||
<v-container v-if="!edit" class="pa-0">
|
||||
<v-row no-gutters class="flex-nowrap align-center">
|
||||
<v-col :cols="itemLabelCols">
|
||||
<v-checkbox
|
||||
v-model="listItem.checked"
|
||||
class="mt-0"
|
||||
color="null"
|
||||
hide-details
|
||||
dense
|
||||
:label="listItem.note"
|
||||
@change="$emit('checked', listItem)"
|
||||
>
|
||||
<template #label>
|
||||
<div :class="listItem.checked ? 'strike-through' : ''">
|
||||
{{ listItem.display }}
|
||||
</div>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col v-if="label && showLabel" cols="3" class="text-right">
|
||||
<MultiPurposeLabel :label="label" small />
|
||||
</v-col>
|
||||
<v-col cols="auto" class="text-right">
|
||||
<div v-if="!listItem.checked" style="min-width: 72px">
|
||||
<v-menu offset-x left min-width="125px">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn small class="ml-2 handle" icon v-bind="attrs" v-on="on">
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="action in contextMenu" :key="action.event" dense @click="contextHandler(action.event)">
|
||||
<v-list-item-title>{{ action.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn small class="ml-2 handle" icon @click="toggleEdit(true)">
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="action in contextMenu" :key="action.event" dense @click="contextHandler(action.event)">
|
||||
<v-list-item-title>{{ action.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn small class="ml-2 handle" icon @click="toggleEdit(true)">
|
||||
<v-icon>
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="listItem.checked" no-gutters class="mb-2">
|
||||
<v-col cols="auto">
|
||||
<div class="text-caption font-weight-light font-italic">
|
||||
{{ $t("shopping-list.completed-on", {date: new Date(listItem.updateAt+"Z").toLocaleDateString($i18n.locale)}) }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<div v-else class="mb-1 mt-6">
|
||||
<ShoppingListItemEditor
|
||||
v-model="localListItem"
|
||||
@ -91,6 +107,7 @@ export default defineComponent({
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
const itemLabelCols = ref<string>(props.value.checked ? "auto" : props.showLabel ? "6" : "8");
|
||||
|
||||
const contextMenu: actions[] = [
|
||||
{
|
||||
@ -174,6 +191,7 @@ export default defineComponent({
|
||||
contextHandler,
|
||||
edit,
|
||||
contextMenu,
|
||||
itemLabelCols,
|
||||
listItem,
|
||||
localListItem,
|
||||
label,
|
||||
|
@ -608,7 +608,8 @@
|
||||
"check-all-items": "Check All Items",
|
||||
"linked-recipes-count": "No Linked Recipes|One Linked Recipe|{count} Linked Recipes",
|
||||
"items-checked-count": "No items checked|One item checked|{count} items checked",
|
||||
"no-label": "No Label"
|
||||
"no-label": "No Label",
|
||||
"completed-on": "Completed on {date}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "All Recipes",
|
||||
|
@ -32,7 +32,7 @@
|
||||
<div v-for="(value, key, idx) in itemsByLabel" :key="key" class="mb-6">
|
||||
<div @click="toggleShowChecked()">
|
||||
<span v-if="idx || key !== $tc('shopping-list.no-label')">
|
||||
<v-icon>
|
||||
<v-icon :color="value[0].label.color">
|
||||
{{ $globals.icons.tags }}
|
||||
</v-icon>
|
||||
</span>
|
||||
@ -60,7 +60,7 @@
|
||||
<v-card height="fit-content" max-height="70vh" style="overflow-y: auto;">
|
||||
<draggable :value="shoppingList.labelSettings" handle=".handle" class="my-2" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateLabelOrder">
|
||||
<div v-for="(labelSetting, index) in shoppingList.labelSettings" :key="labelSetting.id">
|
||||
<MultiPurposeLabelSection v-model="shoppingList.labelSettings[index]" />
|
||||
<MultiPurposeLabelSection v-model="shoppingList.labelSettings[index]" use-color />
|
||||
</div>
|
||||
</draggable>
|
||||
</v-card>
|
||||
@ -319,8 +319,11 @@ export default defineComponent({
|
||||
|
||||
const listItems = computed(() => {
|
||||
return {
|
||||
checked: shoppingList.value?.listItems?.filter((item) => item.checked) ?? [],
|
||||
unchecked: shoppingList.value?.listItems?.filter((item) => !item.checked) ?? [],
|
||||
checked: shoppingList.value?.listItems
|
||||
?.filter((item) => item.checked)
|
||||
.sort((a, b) => (a.updateAt < b.updateAt ? 1 : -1))
|
||||
?? [],
|
||||
};
|
||||
});
|
||||
|
||||
@ -467,9 +470,7 @@ export default defineComponent({
|
||||
|
||||
function updateItemsByLabel() {
|
||||
const items: { [prop: string]: ShoppingListItemOut[] } = {};
|
||||
|
||||
const noLabelText = i18n.tc("shopping-list.no-label");
|
||||
|
||||
const noLabel = [] as ShoppingListItemOut[];
|
||||
|
||||
shoppingList.value?.listItems?.forEach((item) => {
|
||||
@ -515,7 +516,7 @@ export default defineComponent({
|
||||
|
||||
watch(shoppingList, () => {
|
||||
updateItemsByLabel();
|
||||
});
|
||||
}, {deep: true});
|
||||
|
||||
async function refreshLabels() {
|
||||
const { data } = await userApi.multiPurposeLabels.getAll();
|
||||
@ -579,19 +580,25 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
loadingCounter.value += 1;
|
||||
if (item.checked && shoppingList.value.listItems) {
|
||||
const lst = shoppingList.value.listItems.filter((itm) => itm.id !== item.id);
|
||||
lst.push(item);
|
||||
updateListItems();
|
||||
|
||||
// make sure the item is at the end of the list with the other checked items
|
||||
item.position = shoppingList.value.listItems.length;
|
||||
|
||||
// set a temporary updatedAt timestamp so it appears at the top of the checked items in the UI
|
||||
item.updateAt = new Date().toISOString();
|
||||
item.updateAt = item.updateAt.substring(0, item.updateAt.length-1);
|
||||
}
|
||||
|
||||
loadingCounter.value += 1;
|
||||
const { data } = await userApi.shopping.items.updateOne(item.id, item);
|
||||
loadingCounter.value -= 1;
|
||||
|
||||
if (data) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteListItem(item: ShoppingListItemOut) {
|
||||
@ -694,7 +701,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// Set Position
|
||||
shoppingList.value.listItems = shoppingList.value.listItems.map((itm: ShoppingListItemOut, idx: number) => {
|
||||
shoppingList.value.listItems = listItems.value.unchecked.concat(listItems.value.checked).map((itm: ShoppingListItemOut, idx: number) => {
|
||||
itm.position = idx;
|
||||
return itm;
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user