feat: bulk deletion on "Manage Data" page (#3056)

* labels bulk delete

* add foods

* bulk delete units

* add categories

* add tags

* add tools

* update translations

* fix types for text

* fix reactivity for stores

---------

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
Kuchenpirat 2024-02-04 19:55:14 +01:00 committed by GitHub
parent 67b7fb007b
commit 52c58e1dc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 310 additions and 22 deletions

View File

@ -57,12 +57,12 @@
:buttons="[ :buttons="[
{ {
icon: $globals.icons.edit, icon: $globals.icons.edit,
text: $t('general.edit'), text: $tc('general.edit'),
event: 'edit', event: 'edit',
}, },
{ {
icon: $globals.icons.delete, icon: $globals.icons.delete,
text: $t('general.delete'), text: $tc('general.delete'),
event: 'delete', event: 'delete',
}, },
]" ]"
@ -160,6 +160,8 @@ export default defineComponent({
props.bulkActions.forEach((action) => { props.bulkActions.forEach((action) => {
handlers[action.event] = () => { handlers[action.event] = () => {
context.emit(action.event, selected.value); context.emit(action.event, selected.value);
// clear selection
selected.value = [];
}; };
}); });

View File

@ -4,7 +4,7 @@ import { usePublicExploreApi } from "../api/api-client";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { IngredientFood } from "~/lib/api/types/recipe"; import { IngredientFood } from "~/lib/api/types/recipe";
let foodStore: Ref<IngredientFood[] | null> | null = null; let foodStore: Ref<IngredientFood[] | null> = ref([]);
/** /**
* useFoodData returns a template reactive object * useFoodData returns a template reactive object
@ -39,11 +39,11 @@ export const usePublicFoodStore = function (groupSlug: string) {
const actions = { const actions = {
...usePublicStoreActions(api.foods, foodStore, loading), ...usePublicStoreActions(api.foods, foodStore, loading),
flushStore() { flushStore() {
foodStore = null; foodStore = ref([]);
}, },
}; };
if (!foodStore) { if (!foodStore.value) {
foodStore = actions.getAll(); foodStore = actions.getAll();
} }
@ -57,7 +57,7 @@ export const useFoodStore = function () {
const actions = { const actions = {
...useStoreActions(api.foods, foodStore, loading), ...useStoreActions(api.foods, foodStore, loading),
flushStore() { flushStore() {
foodStore = null; foodStore.value = [];
}, },
}; };

View File

@ -3,7 +3,7 @@ import { useStoreActions } from "../partials/use-actions-factory";
import { MultiPurposeLabelOut } from "~/lib/api/types/labels"; import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
let labelStore: Ref<MultiPurposeLabelOut[] | null> | null = null; let labelStore: Ref<MultiPurposeLabelOut[] | null> = ref([]);
export function useLabelData() { export function useLabelData() {
const data = reactive({ const data = reactive({
@ -33,11 +33,11 @@ export function useLabelStore() {
const actions = { const actions = {
...useStoreActions<MultiPurposeLabelOut>(api.multiPurposeLabels, labelStore, loading), ...useStoreActions<MultiPurposeLabelOut>(api.multiPurposeLabels, labelStore, loading),
flushStore() { flushStore() {
labelStore = null; labelStore.value =[];
}, },
}; };
if (!labelStore) { if (!labelStore.value) {
labelStore = actions.getAll(); labelStore = actions.getAll();
} }

View File

@ -3,7 +3,7 @@ import { useStoreActions } from "../partials/use-actions-factory";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { IngredientUnit } from "~/lib/api/types/recipe"; import { IngredientUnit } from "~/lib/api/types/recipe";
let unitStore: Ref<IngredientUnit[] | null> | null = null; let unitStore: Ref<IngredientUnit[] | null> = ref([]);
/** /**
* useUnitData returns a template reactive object * useUnitData returns a template reactive object
@ -40,11 +40,11 @@ export const useUnitStore = function () {
const actions = { const actions = {
...useStoreActions<IngredientUnit>(api.units, unitStore, loading), ...useStoreActions<IngredientUnit>(api.units, unitStore, loading),
flushStore() { flushStore() {
unitStore = null; unitStore.value = [];
}, },
}; };
if (!unitStore) { if (!unitStore.value) {
unitStore = actions.getAll(); unitStore = actions.getAll();
} }

View File

@ -199,7 +199,8 @@
"upload-file": "Upload File", "upload-file": "Upload File",
"created-on-date": "Created on: {0}", "created-on-date": "Created on: {0}",
"unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.", "unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.",
"clipboard-copy-failure": "Failed to copy to the clipboard." "clipboard-copy-failure": "Failed to copy to the clipboard.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?"
}, },
"group": { "group": {
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?", "are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",

View File

@ -49,15 +49,41 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">{{ $t('general.confirm-delete-generic-items') }}</p>
<v-card outlined>
<v-virtual-scroll height="400" item-height="25" :items="bulkDeleteTarget">
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Data Table --> <!-- Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.categories" section :title="$tc('data-pages.categories.category-data')"> </BaseCardSectionTitle> <BaseCardSectionTitle :icon="$globals.icons.categories" section :title="$tc('data-pages.categories.category-data')"> </BaseCardSectionTitle>
<CrudTable <CrudTable
:table-config="tableConfig" :table-config="tableConfig"
:headers.sync="tableHeaders" :headers.sync="tableHeaders"
:data="categories || []" :data="categories || []"
:bulk-actions="[]" :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
@delete-one="deleteEventHandler" @delete-one="deleteEventHandler"
@edit-one="editEventHandler" @edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
> >
<template #button-row> <template #button-row>
<BaseButton create @click="state.createDialog = true">{{ $t("general.create") }}</BaseButton> <BaseButton create @click="state.createDialog = true">{{ $t("general.create") }}</BaseButton>
@ -96,6 +122,7 @@ export default defineComponent({
createDialog: false, createDialog: false,
editDialog: false, editDialog: false,
deleteDialog: false, deleteDialog: false,
bulkDeleteDialog: false,
}); });
const categoryData = useCategoryData(); const categoryData = useCategoryData();
const categoryStore = useCategoryStore(); const categoryStore = useCategoryStore();
@ -149,6 +176,24 @@ export default defineComponent({
state.deleteDialog = false; state.deleteDialog = false;
} }
// ============================================================
// Bulk Delete Category
const bulkDeleteTarget = ref<RecipeCategory[]>([]);
function bulkDeleteEventHandler(selection: RecipeCategory[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
for (const item of bulkDeleteTarget.value) {
if (!item.id) {
continue;
}
await categoryStore.actions.deleteOne(item.id);
}
bulkDeleteTarget.value = [];
}
return { return {
state, state,
tableConfig, tableConfig,
@ -168,7 +213,12 @@ export default defineComponent({
// delete // delete
deleteTarget, deleteTarget,
deleteEventHandler, deleteEventHandler,
deleteCategory deleteCategory,
// bulk delete
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
}; };
}, },
}); });

View File

@ -155,16 +155,42 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="bulkDeleteDialog"
width="650px"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">{{ $t('general.confirm-delete-generic-items') }}</p>
<v-card outlined>
<v-virtual-scroll height="400" item-height="25" :items="bulkDeleteTarget">
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Data Table --> <!-- Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.foods" section :title="$tc('data-pages.foods.food-data')"> </BaseCardSectionTitle> <BaseCardSectionTitle :icon="$globals.icons.foods" section :title="$tc('data-pages.foods.food-data')"> </BaseCardSectionTitle>
<CrudTable <CrudTable
:table-config="tableConfig" :table-config="tableConfig"
:headers.sync="tableHeaders" :headers.sync="tableHeaders"
:data="foods || []" :data="foods || []"
:bulk-actions="[]" :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
@delete-one="deleteEventHandler" @delete-one="deleteEventHandler"
@edit-one="editEventHandler" @edit-one="editEventHandler"
@create-one="createEventHandler" @create-one="createEventHandler"
@delete-selected="bulkDeleteEventHandler"
> >
<template #button-row> <template #button-row>
<BaseButton create @click="createDialog = true" /> <BaseButton create @click="createDialog = true" />
@ -306,6 +332,21 @@ export default defineComponent({
deleteDialog.value = false; deleteDialog.value = false;
} }
const bulkDeleteDialog = ref(false);
const bulkDeleteTarget = ref<IngredientFood[]>([]);
function bulkDeleteEventHandler(selection: IngredientFood[]) {
bulkDeleteTarget.value = selection;
bulkDeleteDialog.value = true;
}
async function deleteSelected() {
for (const item of bulkDeleteTarget.value) {
await foodStore.actions.deleteOne(item.id);
}
bulkDeleteTarget.value = [];
}
// ============================================================ // ============================================================
// Alias Manager // Alias Manager
@ -396,6 +437,10 @@ export default defineComponent({
deleteDialog, deleteDialog,
deleteFood, deleteFood,
deleteTarget, deleteTarget,
bulkDeleteDialog,
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
// Alias Manager // Alias Manager
aliasManagerDialog, aliasManagerDialog,
aliasManagerEventHandler, aliasManagerEventHandler,

View File

@ -45,6 +45,31 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">{{ $t('general.confirm-delete-generic-items') }}</p>
<v-card outlined>
<v-virtual-scroll height="400" item-height="25" :items="bulkDeleteTarget">
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Seed Dialog--> <!-- Seed Dialog-->
<BaseDialog <BaseDialog
v-model="seedDialog" v-model="seedDialog"
@ -88,9 +113,10 @@
:table-config="tableConfig" :table-config="tableConfig"
:headers.sync="tableHeaders" :headers.sync="tableHeaders"
:data="labels || []" :data="labels || []"
:bulk-actions="[]" :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
@delete-one="deleteEventHandler" @delete-one="deleteEventHandler"
@edit-one="editEventHandler" @edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
> >
<template #button-row> <template #button-row>
<BaseButton create @click="state.createDialog = true">{{ $t("general.create") }}</BaseButton> <BaseButton create @click="state.createDialog = true">{{ $t("general.create") }}</BaseButton>
@ -146,6 +172,7 @@ export default defineComponent({
createDialog: false, createDialog: false,
editDialog: false, editDialog: false,
deleteDialog: false, deleteDialog: false,
bulkDeleteDialog: false,
}); });
// ============================================================ // ============================================================
@ -179,6 +206,21 @@ export default defineComponent({
state.deleteDialog = false; state.deleteDialog = false;
} }
// Bulk Delete
const bulkDeleteTarget = ref<MultiPurposeLabelSummary[]>([]);
function bulkDeleteEventHandler(selection: MultiPurposeLabelSummary[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
for (const item of bulkDeleteTarget.value) {
await labelStore.actions.deleteOne(item.id);
}
bulkDeleteTarget.value = [];
}
// Edit // Edit
const editLabel = ref<MultiPurposeLabelSummary | null>(null); const editLabel = ref<MultiPurposeLabelSummary | null>(null);
@ -244,6 +286,9 @@ export default defineComponent({
deleteEventHandler, deleteEventHandler,
deleteLabel, deleteLabel,
deleteTarget, deleteTarget,
bulkDeleteEventHandler,
deleteSelected,
bulkDeleteTarget,
// Seed // Seed
seedDatabase, seedDatabase,

View File

@ -49,15 +49,41 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">{{ $t('general.confirm-delete-generic-items') }}</p>
<v-card outlined>
<v-virtual-scroll height="400" item-height="25" :items="bulkDeleteTarget">
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Data Table --> <!-- Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.tags" section :title="$tc('data-pages.tags.tag-data')"> </BaseCardSectionTitle> <BaseCardSectionTitle :icon="$globals.icons.tags" section :title="$tc('data-pages.tags.tag-data')"> </BaseCardSectionTitle>
<CrudTable <CrudTable
:table-config="tableConfig" :table-config="tableConfig"
:headers.sync="tableHeaders" :headers.sync="tableHeaders"
:data="tags || []" :data="tags || []"
:bulk-actions="[]" :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
@delete-one="deleteEventHandler" @delete-one="deleteEventHandler"
@edit-one="editEventHandler" @edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
> >
<template #button-row> <template #button-row>
<BaseButton create @click="state.createDialog = true">{{ $t("general.create") }}</BaseButton> <BaseButton create @click="state.createDialog = true">{{ $t("general.create") }}</BaseButton>
@ -96,6 +122,7 @@ export default defineComponent({
createDialog: false, createDialog: false,
editDialog: false, editDialog: false,
deleteDialog: false, deleteDialog: false,
bulkDeleteDialog: false,
}); });
const tagData = useTagData(); const tagData = useTagData();
@ -150,6 +177,24 @@ export default defineComponent({
state.deleteDialog = false; state.deleteDialog = false;
} }
// ============================================================
// Bulk Delete Tag
const bulkDeleteTarget = ref<RecipeTag[]>([]);
function bulkDeleteEventHandler(selection: RecipeTag[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
for (const item of bulkDeleteTarget.value) {
if (!item.id) {
continue;
}
await tagStore.actions.deleteOne(item.id);
}
bulkDeleteTarget.value = [];
}
return { return {
state, state,
tableConfig, tableConfig,
@ -169,7 +214,12 @@ export default defineComponent({
// delete // delete
deleteTarget, deleteTarget,
deleteEventHandler, deleteEventHandler,
deleteTag deleteTag,
// bulk delete
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
}; };
}, },
}); });

View File

@ -51,15 +51,41 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">{{ $t('general.confirm-delete-generic-items') }}</p>
<v-card outlined>
<v-virtual-scroll height="400" item-height="25" :items="bulkDeleteTarget">
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Data Table --> <!-- Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.potSteam" section :title="$tc('data-pages.tools.tool-data')"> </BaseCardSectionTitle> <BaseCardSectionTitle :icon="$globals.icons.potSteam" section :title="$tc('data-pages.tools.tool-data')"> </BaseCardSectionTitle>
<CrudTable <CrudTable
:table-config="tableConfig" :table-config="tableConfig"
:headers.sync="tableHeaders" :headers.sync="tableHeaders"
:data="tools || []" :data="tools || []"
:bulk-actions="[]" :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
@delete-one="deleteEventHandler" @delete-one="deleteEventHandler"
@edit-one="editEventHandler" @edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
> >
<template #button-row> <template #button-row>
<BaseButton create @click="state.createDialog = true">{{ $t("general.create") }}</BaseButton> <BaseButton create @click="state.createDialog = true">{{ $t("general.create") }}</BaseButton>
@ -108,6 +134,7 @@ export default defineComponent({
createDialog: false, createDialog: false,
editDialog: false, editDialog: false,
deleteDialog: false, deleteDialog: false,
bulkDeleteDialog: false,
}); });
const toolData = useToolData(); const toolData = useToolData();
@ -162,6 +189,22 @@ export default defineComponent({
state.deleteDialog = false; state.deleteDialog = false;
} }
// ============================================================
// Bulk Delete Tag
const bulkDeleteTarget = ref<RecipeTool[]>([]);
function bulkDeleteEventHandler(selection: RecipeTool[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
for (const item of bulkDeleteTarget.value) {
await toolStore.actions.deleteOne(item.id);
}
bulkDeleteTarget.value = [];
}
return { return {
state, state,
tableConfig, tableConfig,
@ -181,7 +224,12 @@ export default defineComponent({
// delete // delete
deleteTarget, deleteTarget,
deleteEventHandler, deleteEventHandler,
deleteTool deleteTool,
// bulk delete
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
}; };
}, },
}); });

View File

@ -129,6 +129,31 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="bulkDeleteDialog"
width="650px"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">{{ $t('general.confirm-delete-generic-items') }}</p>
<v-card outlined>
<v-virtual-scroll height="400" item-height="25" :items="bulkDeleteTarget">
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Seed Dialog--> <!-- Seed Dialog-->
<BaseDialog <BaseDialog
v-model="seedDialog" v-model="seedDialog"
@ -172,10 +197,11 @@
:table-config="tableConfig" :table-config="tableConfig"
:headers.sync="tableHeaders" :headers.sync="tableHeaders"
:data="units || []" :data="units || []"
:bulk-actions="[]" :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
@delete-one="deleteEventHandler" @delete-one="deleteEventHandler"
@edit-one="editEventHandler" @edit-one="editEventHandler"
@create-one="createEventHandler" @create-one="createEventHandler"
@delete-selected="bulkDeleteEventHandler"
> >
<template #button-row> <template #button-row>
<BaseButton create @click="createDialog = true" /> <BaseButton create @click="createDialog = true" />
@ -339,6 +365,22 @@ export default defineComponent({
deleteDialog.value = false; deleteDialog.value = false;
} }
// ============================================================
// Bulk Delete Units
const bulkDeleteDialog = ref(false);
const bulkDeleteTarget = ref<IngredientUnit[]>([]);
function bulkDeleteEventHandler(selection: IngredientUnit[]) {
bulkDeleteTarget.value = selection;
bulkDeleteDialog.value = true;
}
async function deleteSelected() {
for (const item of bulkDeleteTarget.value) {
await unitActions.deleteOne(item.id);
}
bulkDeleteTarget.value = [];
}
// ============================================================ // ============================================================
// Alias Manager // Alias Manager
@ -423,6 +465,11 @@ export default defineComponent({
deleteDialog, deleteDialog,
deleteUnit, deleteUnit,
deleteTarget, deleteTarget,
// Bulk Delete
bulkDeleteDialog,
bulkDeleteEventHandler,
bulkDeleteTarget,
deleteSelected,
// Alias Manager // Alias Manager
aliasManagerDialog, aliasManagerDialog,
aliasManagerEventHandler, aliasManagerEventHandler,