mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-24 07:15:24 -07:00
bulk assign frontend
This commit is contained in:
parent
10c86773ea
commit
f4485827d8
8 changed files with 467 additions and 39 deletions
|
@ -6,6 +6,7 @@ const prefix = baseURL + "categories";
|
||||||
|
|
||||||
const categoryURLs = {
|
const categoryURLs = {
|
||||||
getAll: `${prefix}`,
|
getAll: `${prefix}`,
|
||||||
|
getEmpty: `${prefix}/empty`,
|
||||||
getCategory: category => `${prefix}/${category}`,
|
getCategory: category => `${prefix}/${category}`,
|
||||||
deleteCategory: category => `${prefix}/${category}`,
|
deleteCategory: category => `${prefix}/${category}`,
|
||||||
updateCategory: category => `${prefix}/${category}`,
|
updateCategory: category => `${prefix}/${category}`,
|
||||||
|
@ -16,6 +17,10 @@ export const categoryAPI = {
|
||||||
let response = await apiReq.get(categoryURLs.getAll);
|
let response = await apiReq.get(categoryURLs.getAll);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
async getEmpty() {
|
||||||
|
let response = await apiReq.get(categoryURLs.getEmpty);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
async create(name) {
|
async create(name) {
|
||||||
let response = await apiReq.post(categoryURLs.getAll, { name: name });
|
let response = await apiReq.post(categoryURLs.getAll, { name: name });
|
||||||
store.dispatch("requestCategories");
|
store.dispatch("requestCategories");
|
||||||
|
@ -34,9 +39,11 @@ export const categoryAPI = {
|
||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
async delete(category) {
|
async delete(category, overrideRequest = false) {
|
||||||
let response = await apiReq.delete(categoryURLs.deleteCategory(category));
|
let response = await apiReq.delete(categoryURLs.deleteCategory(category));
|
||||||
store.dispatch("requestCategories");
|
if (!overrideRequest) {
|
||||||
|
store.dispatch("requestCategories");
|
||||||
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -45,6 +52,7 @@ const tagPrefix = baseURL + "tags";
|
||||||
|
|
||||||
const tagURLs = {
|
const tagURLs = {
|
||||||
getAll: `${tagPrefix}`,
|
getAll: `${tagPrefix}`,
|
||||||
|
getEmpty: `${tagPrefix}/empty`,
|
||||||
getTag: tag => `${tagPrefix}/${tag}`,
|
getTag: tag => `${tagPrefix}/${tag}`,
|
||||||
deleteTag: tag => `${tagPrefix}/${tag}`,
|
deleteTag: tag => `${tagPrefix}/${tag}`,
|
||||||
updateTag: tag => `${tagPrefix}/${tag}`,
|
updateTag: tag => `${tagPrefix}/${tag}`,
|
||||||
|
@ -55,6 +63,10 @@ export const tagAPI = {
|
||||||
let response = await apiReq.get(tagURLs.getAll);
|
let response = await apiReq.get(tagURLs.getAll);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
async getEmpty() {
|
||||||
|
let response = await apiReq.get(tagURLs.getEmpty);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
async create(name) {
|
async create(name) {
|
||||||
let response = await apiReq.post(tagURLs.getAll, { name: name });
|
let response = await apiReq.post(tagURLs.getAll, { name: name });
|
||||||
store.dispatch("requestTags");
|
store.dispatch("requestTags");
|
||||||
|
@ -73,9 +85,11 @@ export const tagAPI = {
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
async delete(tag) {
|
async delete(tag, overrideRequest = false) {
|
||||||
let response = await apiReq.delete(tagURLs.deleteTag(tag));
|
let response = await apiReq.delete(tagURLs.deleteTag(tag));
|
||||||
store.dispatch("requestTags");
|
if (!overrideRequest) {
|
||||||
|
store.dispatch("requestTags");
|
||||||
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,10 +60,10 @@
|
||||||
<v-row v-else dense>
|
<v-row v-else dense>
|
||||||
<v-col
|
<v-col
|
||||||
cols="12"
|
cols="12"
|
||||||
sm="12"
|
:sm="singleColumn ? '12' : '12'"
|
||||||
md="6"
|
:md="singleColumn ? '12' : '6'"
|
||||||
lg="4"
|
:lg="singleColumn ? '12' : '4'"
|
||||||
xl="3"
|
:xl="singleColumn ? '12' : '3'"
|
||||||
v-for="recipe in recipes.slice(0, cardLimit)"
|
v-for="recipe in recipes.slice(0, cardLimit)"
|
||||||
:key="recipe.name"
|
:key="recipe.name"
|
||||||
>
|
>
|
||||||
|
@ -79,14 +79,16 @@
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
<div v-intersect="bumpList" class="d-flex">
|
<div v-intersect="bumpList" class="d-flex">
|
||||||
<v-progress-circular
|
<v-expand-x-transition>
|
||||||
v-if="loading"
|
<v-progress-circular
|
||||||
class="mx-auto mt-1"
|
v-if="loading"
|
||||||
:size="50"
|
class="mx-auto mt-1"
|
||||||
:width="7"
|
:size="50"
|
||||||
color="primary"
|
:width="7"
|
||||||
indeterminate
|
color="primary"
|
||||||
></v-progress-circular>
|
indeterminate
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-expand-x-transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -109,6 +111,12 @@ export default {
|
||||||
hardLimit: {
|
hardLimit: {
|
||||||
default: 99999,
|
default: 99999,
|
||||||
},
|
},
|
||||||
|
mobileCards: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
singleColumn: {
|
||||||
|
defualt: false,
|
||||||
|
},
|
||||||
recipes: Array,
|
recipes: Array,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -117,8 +125,14 @@ export default {
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
recipes() {
|
||||||
|
this.bumpList();
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
viewScale() {
|
viewScale() {
|
||||||
|
if (this.mobileCards) return true;
|
||||||
switch (this.$vuetify.breakpoint.name) {
|
switch (this.$vuetify.breakpoint.name) {
|
||||||
case "xs":
|
case "xs":
|
||||||
return true;
|
return true;
|
||||||
|
@ -128,10 +142,16 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
effectiveHardLimit() {
|
||||||
|
return Math.min(this.hardLimit, this.recipes.length);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
bumpList() {
|
bumpList() {
|
||||||
const newCardLimit = Math.min(this.cardLimit + 20, this.hardLimit);
|
const newCardLimit = Math.min(
|
||||||
|
this.cardLimit + 20,
|
||||||
|
this.effectiveHardLimit
|
||||||
|
);
|
||||||
|
|
||||||
if (this.loading === false && newCardLimit > this.cardLimit) {
|
if (this.loading === false && newCardLimit > this.cardLimit) {
|
||||||
this.setLoader();
|
this.setLoader();
|
||||||
|
@ -141,7 +161,7 @@ export default {
|
||||||
},
|
},
|
||||||
async setLoader() {
|
async setLoader() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
await new Promise(r => setTimeout(r, 3000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-dialog v-model="dialog" :width="modalWidth + 'px'">
|
<v-dialog
|
||||||
<v-card class="pb-2">
|
v-model="dialog"
|
||||||
|
:width="modalWidth + 'px'"
|
||||||
|
:content-class="top ? 'top-dialog' : undefined"
|
||||||
|
>
|
||||||
|
<v-card class="pb-2" :loading="loading">
|
||||||
<v-app-bar dark :color="color" class="mt-n1 mb-2">
|
<v-app-bar dark :color="color" class="mt-n1 mb-2">
|
||||||
<v-icon large left v-if="!loading">
|
<v-icon large left v-if="!loading">
|
||||||
{{ titleIcon }}
|
{{ titleIcon }}
|
||||||
|
@ -19,13 +23,15 @@
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<slot> </slot>
|
<slot> </slot>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-btn text color="grey" @click="dialog = false">
|
<slot name="card-actions">
|
||||||
Cancel
|
<v-btn text color="grey" @click="dialog = false">
|
||||||
</v-btn>
|
Cancel
|
||||||
<v-spacer></v-spacer>
|
</v-btn>
|
||||||
<v-btn color="success" @click="$emit('submit')">
|
<v-spacer></v-spacer>
|
||||||
Submit
|
<v-btn color="success" @click="$emit('submit')">
|
||||||
</v-btn>
|
Submit
|
||||||
|
</v-btn>
|
||||||
|
</slot>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
<slot name="below-actions"> </slot>
|
<slot name="below-actions"> </slot>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
@ -48,20 +54,31 @@ export default {
|
||||||
modalWidth: {
|
modalWidth: {
|
||||||
default: "500",
|
default: "500",
|
||||||
},
|
},
|
||||||
|
loading: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
loading: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
open() {
|
open() {
|
||||||
this.dialog = true;
|
this.dialog = true;
|
||||||
},
|
},
|
||||||
|
close() {
|
||||||
|
this.dialog = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.top-dialog {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,8 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-btn icon @click="dialog = true" class="mt-n1">
|
<slot>
|
||||||
<v-icon :color="color">mdi-plus</v-icon>
|
<v-btn icon @click="dialog = true" class="mt-n1">
|
||||||
</v-btn>
|
<v-icon :color="color">mdi-plus</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</slot>
|
||||||
<v-dialog v-model="dialog" width="500">
|
<v-dialog v-model="dialog" width="500">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-app-bar dense dark color="primary mb-2">
|
<v-app-bar dense dark color="primary mb-2">
|
||||||
|
@ -80,6 +82,9 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
open() {
|
||||||
|
this.dialog = true;
|
||||||
|
},
|
||||||
async select() {
|
async select() {
|
||||||
const newItem = await (async () => {
|
const newItem = await (async () => {
|
||||||
if (this.tagDialog) {
|
if (this.tagDialog) {
|
||||||
|
|
79
frontend/src/components/UI/Search/FuseSearchBar.vue
Normal file
79
frontend/src/components/UI/Search/FuseSearchBar.vue
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot> </slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const RESULTS_EVENT = "results";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
search: {
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
rawData: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
/** Defaults to Show All Results */
|
||||||
|
showAll: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
keys: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ["name"],
|
||||||
|
},
|
||||||
|
defaultOptions: {
|
||||||
|
default: () => ({
|
||||||
|
shouldSort: true,
|
||||||
|
threshold: 0.6,
|
||||||
|
location: 0,
|
||||||
|
distance: 100,
|
||||||
|
findAllMatches: true,
|
||||||
|
maxPatternLength: 32,
|
||||||
|
minMatchCharLength: 2,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
results: [],
|
||||||
|
fuseResults: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
options() {
|
||||||
|
return { ...this.defaultOptions, ...{ keys: this.keys } };
|
||||||
|
},
|
||||||
|
autoResults() {
|
||||||
|
return this.fuseResults.length > 1 ? this.fuseResults : this.results;
|
||||||
|
},
|
||||||
|
fuse() {
|
||||||
|
return new Fuse(this.rawData, this.options);
|
||||||
|
},
|
||||||
|
isSearching() {
|
||||||
|
return this.search && this.search.length > 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search() {
|
||||||
|
try {
|
||||||
|
this.results = this.fuse.search(this.search.trim());
|
||||||
|
} catch {
|
||||||
|
this.results = this.rawData
|
||||||
|
.map(x => ({ item: x }))
|
||||||
|
.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||||
|
}
|
||||||
|
this.$emit(RESULTS_EVENT, this.results);
|
||||||
|
|
||||||
|
if (this.showResults === true) {
|
||||||
|
this.fuseResults = this.results;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -0,0 +1,136 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<base-dialog
|
||||||
|
ref="assignDialog"
|
||||||
|
title-icon="mdi-tag"
|
||||||
|
color="primary"
|
||||||
|
title="Bulk Assign"
|
||||||
|
:loading="loading"
|
||||||
|
modal-width="700"
|
||||||
|
:top="true"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="search"
|
||||||
|
autocomplete="off"
|
||||||
|
label="Keyword"
|
||||||
|
></v-text-field>
|
||||||
|
<CategoryTagSelector
|
||||||
|
:tag-selector="false"
|
||||||
|
v-model="catsToAssign"
|
||||||
|
:return-object="false"
|
||||||
|
/>
|
||||||
|
<CategoryTagSelector
|
||||||
|
:tag-selector="true"
|
||||||
|
v-model="tagsToAssign"
|
||||||
|
:return-object="false"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<template slot="card-actions">
|
||||||
|
<v-btn text color="grey" @click="closeDialog">
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
@click="assignAll"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="results.length < 1"
|
||||||
|
>
|
||||||
|
Assign All
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<template slot="below-actions">
|
||||||
|
<v-card-title class="headline"> </v-card-title>
|
||||||
|
<CardSection
|
||||||
|
class="px-2 pb-2"
|
||||||
|
:title="`${results.length || 0} Recipes Effected`"
|
||||||
|
:mobile-cards="true"
|
||||||
|
:recipes="results"
|
||||||
|
:single-column="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</base-dialog>
|
||||||
|
|
||||||
|
<v-btn @click="openDialog" small color="success" class="mr-1">
|
||||||
|
Bulk Assign
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CardSection from "@/components/UI/CardSection";
|
||||||
|
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||||
|
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
isTags: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
CardSection,
|
||||||
|
BaseDialog,
|
||||||
|
CategoryTagSelector,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search: "",
|
||||||
|
loading: false,
|
||||||
|
assignTargetRecipes: [],
|
||||||
|
catsToAssign: [],
|
||||||
|
tagsToAssign: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch("requestAllRecipes");
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
allRecipes() {
|
||||||
|
return this.$store.getters.getRecentRecipes;
|
||||||
|
},
|
||||||
|
results() {
|
||||||
|
if (this.search === null || this.search === "") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.allRecipes.filter(x => {
|
||||||
|
return (
|
||||||
|
this.checkForKeywords(x.name) || this.checkForKeywords(x.description)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
keywords() {
|
||||||
|
const lowered = this.search.toLowerCase();
|
||||||
|
return lowered.split(" ");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reset() {
|
||||||
|
this.search = "";
|
||||||
|
this.loading = false;
|
||||||
|
this.assignTargetRecipes = [];
|
||||||
|
this.catsToAssign = [];
|
||||||
|
this.tagsToAssign = [];
|
||||||
|
},
|
||||||
|
assignAll() {
|
||||||
|
console.log("Categories", this.catsToAssign);
|
||||||
|
console.log("Tags", this.tagsToAssign);
|
||||||
|
console.log("results", this.results);
|
||||||
|
},
|
||||||
|
closeDialog() {
|
||||||
|
this.$refs.assignDialog.close();
|
||||||
|
},
|
||||||
|
async openDialog() {
|
||||||
|
this.$refs.assignDialog.open();
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
checkForKeywords(str) {
|
||||||
|
const searchStr = str.toLowerCase();
|
||||||
|
return this.keywords.some(x => searchStr.includes(x));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<base-dialog
|
||||||
|
ref="deleteDialog"
|
||||||
|
title-icon="mdi-tag"
|
||||||
|
color="error"
|
||||||
|
:title="title"
|
||||||
|
:loading="loading"
|
||||||
|
modal-width="400"
|
||||||
|
>
|
||||||
|
<v-list v-if="deleteList.length > 0">
|
||||||
|
<v-list-item v-for="item in deleteList" :key="item.slug">
|
||||||
|
<v-list-item-content>
|
||||||
|
{{ item.name }}
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<v-card-text v-else class=" mt-4 text-center">
|
||||||
|
No Unused Items
|
||||||
|
</v-card-text>
|
||||||
|
<template slot="card-actions">
|
||||||
|
<v-btn text color="grey" @click="closeDialog">
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
@click="deleteUnused"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="deleteList.length < 1"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</base-dialog>
|
||||||
|
|
||||||
|
<v-btn @click="openDialog" small color="error" class="mr-1">
|
||||||
|
Remove Unused
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||||
|
import { api } from "@/api";
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
isTags: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BaseDialog,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
deleteList: [],
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
return this.isTags ? "Delete Tags" : "Delete Categories";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDialog() {
|
||||||
|
this.$refs.deleteDialog.close();
|
||||||
|
},
|
||||||
|
async openDialog() {
|
||||||
|
this.$refs.deleteDialog.open();
|
||||||
|
console.log(this.isTags);
|
||||||
|
if (this.isTags) {
|
||||||
|
this.deleteList = await api.tags.getEmpty();
|
||||||
|
} else {
|
||||||
|
this.deleteList = await api.categories.getEmpty();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteUnused() {
|
||||||
|
this.loading = true;
|
||||||
|
if (this.isTags) {
|
||||||
|
this.deleteList.forEach(async element => {
|
||||||
|
await api.tags.delete(element.slug, true);
|
||||||
|
});
|
||||||
|
this.$store.dispatch("requestTags");
|
||||||
|
} else {
|
||||||
|
this.deleteList.forEach(async element => {
|
||||||
|
await api.categories.delete(element.slug, true);
|
||||||
|
});
|
||||||
|
this.$store.dispatch("requestCategories");
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
this.closeDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
|
@ -33,12 +33,44 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</base-dialog>
|
</base-dialog>
|
||||||
<v-app-bar flat>
|
|
||||||
<v-spacer> </v-spacer>
|
<v-app-bar flat color="white">
|
||||||
<v-btn @click="titleCaseAll" small color="success">
|
<new-category-tag-dialog
|
||||||
|
ref="newDialog"
|
||||||
|
:tag-dialog="isTags"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
<v-btn @click="openNewDialog" small color="success">
|
||||||
|
New
|
||||||
|
</v-btn>
|
||||||
|
</new-category-tag-dialog>
|
||||||
|
<BulkAssign isTags="isTags" />
|
||||||
|
<v-btn @click="titleCaseAll" class="mr-1" small color="success">
|
||||||
Title Case All
|
Title Case All
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<RemoveUnused :isTags="isTags" />
|
||||||
|
<v-spacer> </v-spacer>
|
||||||
|
<fuse-search-bar
|
||||||
|
:raw-data="allItems"
|
||||||
|
@results="filterItems"
|
||||||
|
:search="searchString"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchString"
|
||||||
|
clearable
|
||||||
|
solo
|
||||||
|
dense
|
||||||
|
class="mx-2"
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
:placeholder="$t('search.search')"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
</fuse-search-bar>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col
|
<v-col
|
||||||
|
@ -46,16 +78,16 @@
|
||||||
:md="6"
|
:md="6"
|
||||||
:lg="4"
|
:lg="4"
|
||||||
:xl="3"
|
:xl="3"
|
||||||
v-for="item in allItems"
|
v-for="item in results"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
>
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-card-title class="py-1">{{ item.name }}</v-card-title>
|
<v-card-title class="py-1">{{ item.name }}</v-card-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn small text color="info" @click="openRename(item)"
|
<v-btn small text color="info" @click="openEditDialog(item)">
|
||||||
>Rename</v-btn
|
Edit
|
||||||
>
|
</v-btn>
|
||||||
<v-btn small text color="error" @click="deleteItem(item.slug)"
|
<v-btn small text color="error" @click="deleteItem(item.slug)"
|
||||||
>Delete
|
>Delete
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -68,15 +100,23 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
|
||||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { validators } from "@/mixins/validators";
|
import { validators } from "@/mixins/validators";
|
||||||
|
import RemoveUnused from "./RemoveUnused";
|
||||||
|
import BulkAssign from "./BulkAssign";
|
||||||
|
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog";
|
||||||
export default {
|
export default {
|
||||||
mixins: [validators],
|
mixins: [validators],
|
||||||
components: {
|
components: {
|
||||||
BaseDialog,
|
BaseDialog,
|
||||||
MobileRecipeCard,
|
MobileRecipeCard,
|
||||||
|
FuseSearchBar,
|
||||||
|
RemoveUnused,
|
||||||
|
NewCategoryTagDialog,
|
||||||
|
BulkAssign,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
isTags: {
|
isTags: {
|
||||||
|
@ -85,6 +125,8 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
searchString: "",
|
||||||
|
searchResults: [],
|
||||||
renameTarget: {
|
renameTarget: {
|
||||||
title: "",
|
title: "",
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -100,9 +142,21 @@ export default {
|
||||||
? this.$store.getters.getAllTags
|
? this.$store.getters.getAllTags
|
||||||
: this.$store.getters.getAllCategories;
|
: this.$store.getters.getAllCategories;
|
||||||
},
|
},
|
||||||
|
results() {
|
||||||
|
if (this.searchString != null && this.searchString.length >= 1) {
|
||||||
|
return this.searchResults;
|
||||||
|
}
|
||||||
|
return this.allItems;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async openRename(item) {
|
filterItems(val) {
|
||||||
|
this.searchResults = val.map(x => x.item);
|
||||||
|
},
|
||||||
|
openNewDialog() {
|
||||||
|
this.$refs.newDialog.open();
|
||||||
|
},
|
||||||
|
async openEditDialog(item) {
|
||||||
let fromAPI = {};
|
let fromAPI = {};
|
||||||
if (this.isTags) {
|
if (this.isTags) {
|
||||||
fromAPI = await api.tags.getRecipesInTag(item.slug);
|
fromAPI = await api.tags.getRecipesInTag(item.slug);
|
||||||
|
@ -151,11 +205,13 @@ export default {
|
||||||
|
|
||||||
if (this.isTags) {
|
if (this.isTags) {
|
||||||
renameList.forEach(async element => {
|
renameList.forEach(async element => {
|
||||||
|
if (element.name === element.newName) return;
|
||||||
await api.tags.update(element.slug, element.newName, true);
|
await api.tags.update(element.slug, element.newName, true);
|
||||||
});
|
});
|
||||||
this.$store.dispatch("requestTags");
|
this.$store.dispatch("requestTags");
|
||||||
} else {
|
} else {
|
||||||
renameList.forEach(async element => {
|
renameList.forEach(async element => {
|
||||||
|
if (element.name === element.newName) return;
|
||||||
await api.categories.update(element.slug, element.newName, true);
|
await api.categories.update(element.slug, element.newName, true);
|
||||||
});
|
});
|
||||||
this.$store.dispatch("requestCategories");
|
this.$store.dispatch("requestCategories");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue