1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-05 05:25:26 +02:00

Frontend Refactor + Bug Fixes

* merge category and tag selector

* unifiy category selector

* add hint

* spacing

* fix nextcloud migration

* simplify email validator #261

* formatting

* cleanup

* auto-gen

* format

* update run script

* unified category/tag selector

* rename component

* Add advanced search link

* remove old code

* convert keywords to tags

* add proper behavior on rename

* proper image name association on rename

* fix test cleanup

* changelog

* set docker comppand

* minify on migration

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-04-06 22:29:02 -08:00 committed by GitHub
parent a396604520
commit 1cf95bb3b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 334 additions and 356 deletions

View file

@ -44,6 +44,11 @@ export const tagAPI = {
let response = await apiReq.get(tagURLs.getAll);
return response.data;
},
async create(name) {
let response = await apiReq.post(tagURLs.getAll, { name: name });
store.dispatch("requestTags");
return response.data;
},
async getRecipesInTag(tag) {
let response = await apiReq.get(tagURLs.getTag(tag));
return response.data;

View file

@ -19,7 +19,7 @@
v-model="page.name"
label="Page Name"
></v-text-field>
<CategorySelector
<CategoryTagSelector
v-model="page.categories"
ref="categoryFormSelector"
@mounted="catMounted = true"
@ -43,10 +43,10 @@
<script>
const NEW_PAGE_EVENT = "refresh-page";
import { api } from "@/api";
import CategorySelector from "@/components/FormHelpers/CategorySelector";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
export default {
components: {
CategorySelector,
CategoryTagSelector,
},
data() {
return {

View file

@ -84,7 +84,7 @@
</v-toolbar-title>
<v-spacer></v-spacer>
<NewCategoryDialog />
<NewCategoryTagDialog />
</v-app-bar>
<v-list height="300" dense style="overflow:auto">
<v-list-item-group>
@ -133,13 +133,13 @@
import { api } from "@/api";
import LanguageMenu from "@/components/UI/LanguageMenu";
import draggable from "vuedraggable";
import NewCategoryDialog from "./NewCategoryDialog.vue";
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog.vue";
export default {
components: {
draggable,
LanguageMenu,
NewCategoryDialog,
NewCategoryTagDialog,
},
data() {
return {

View file

@ -1,79 +0,0 @@
<template>
<v-select
:items="allCategories"
v-model="selected"
label="Categories"
chips
deletable-chips
:dense="dense"
item-text="name"
multiple
:solo="solo"
:return-object="returnObject"
:flat="flat"
@input="emitChange"
>
<template v-slot:selection="data">
<v-chip
class="ma-1"
:input-value="data.selected"
close
@click:close="removeByIndex(data.index)"
label
color="accent"
dark
>
{{ data.item.name }}
</v-chip>
</template></v-select
>
</template>
<script>
const MOUNTED_EVENT = "mounted";
export default {
props: {
value: Array,
solo: {
default: false,
},
dense: {
default: true,
},
returnObject: {
default: true,
},
},
data() {
return {
selected: [],
};
},
mounted() {
this.$emit(MOUNTED_EVENT);
},
computed: {
allCategories() {
return this.$store.getters.getAllCategories;
},
flat() {
return this.selected.length > 0 && this.solo;
},
},
methods: {
emitChange() {
this.$emit("input", this.selected);
},
setInit(val) {
this.selected = val;
},
removeByIndex(index) {
this.selected.splice(index, 1);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,129 @@
<template>
<v-autocomplete
:items="activeItems"
v-model="selected"
:value="value"
:label="inputLabel"
chips
deletable-chips
:dense="dense"
item-text="name"
persistent-hint
multiple
:hint="hint"
:solo="solo"
:return-object="returnObject"
:flat="flat"
@input="emitChange"
>
<template v-slot:selection="data">
<v-chip
class="ma-1"
:input-value="data.selected"
close
@click:close="removeByIndex(data.index)"
label
color="accent"
dark
:key="data.index"
>
{{ data.item.name || data.item }}
</v-chip>
</template>
<template v-slot:append-outer="">
<NewCategoryTagDialog
v-if="showAdd"
:tag-dialog="tagSelector"
@created-item="pushToItem"
/>
</template>
</v-autocomplete>
</template>
<script>
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog";
const MOUNTED_EVENT = "mounted";
export default {
components: {
NewCategoryTagDialog,
},
props: {
value: Array,
solo: {
default: false,
},
dense: {
default: true,
},
returnObject: {
default: true,
},
tagSelector: {
default: false,
},
hint: {
default: null,
},
showAdd: {
default: false,
},
showLabel: {
default: true,
},
},
data() {
return {
selected: [],
};
},
mounted() {
this.$emit(MOUNTED_EVENT);
this.setInit(this.value);
},
watch: {
value(val) {
this.selected = val;
},
},
computed: {
inputLabel() {
if (!this.showLabel) return null;
return this.tagSelector ? "Tags" : "Categories";
},
activeItems() {
let ItemObjects = [];
if (this.tagSelector) ItemObjects = this.$store.getters.getAllTags;
else {
ItemObjects = this.$store.getters.getAllCategories;
}
if (this.returnObject) return ItemObjects;
else {
return ItemObjects.map(x => x.name);
}
},
flat() {
return this.selected.length > 0 && this.solo;
},
},
methods: {
emitChange() {
this.$emit("input", this.selected);
},
setInit(val) {
this.selected = val;
},
removeByIndex(index) {
this.selected.splice(index, 1);
},
pushToItem(createdItem) {
createdItem = this.returnObject ? createdItem : createdItem.name;
this.selected.push(createdItem);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,79 +0,0 @@
<template>
<v-select
:items="allTags"
v-model="selected"
label="Tags"
chips
deletable-chips
:dense="dense"
:solo="solo"
:flat="flat"
item-text="name"
multiple
:return-object="returnObject"
@input="emitChange"
>
<template v-slot:selection="data">
<v-chip
class="ma-1"
:input-value="data.selected"
close
@click:close="removeByIndex(data.index)"
label
color="accent"
dark
>
{{ data.item.name }}
</v-chip>
</template>
</v-select>
</template>
<script>
const MOUNTED_EVENT = "mounted";
export default {
props: {
value: Array,
solo: {
default: false,
},
dense: {
default: true,
},
returnObject: {
default: true,
},
},
data() {
return {
selected: [],
};
},
mounted() {
this.$emit(MOUNTED_EVENT);
},
computed: {
allTags() {
return this.$store.getters.getAllTags;
},
flat() {
return this.selected.length > 0 && this.solo;
},
},
methods: {
emitChange() {
this.$emit("input", this.selected);
},
setInit(val) {
this.selected = val;
},
removeByIndex(index) {
this.selected.splice(index, 1);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -114,60 +114,21 @@
<BulkAdd @bulk-data="appendIngredients" />
<h2 class="mt-6">{{ $t("recipe.categories") }}</h2>
<v-combobox
dense
multiple
chips
item-color="secondary"
deletable-chips
<CategoryTagSelector
:return-object="false"
v-model="value.recipeCategory"
hide-selected
:items="allCategories"
text="name"
:search-input.sync="categoriesSearchInput"
@change="categoriesSearchInput = ''"
>
<template v-slot:selection="data">
<v-chip
class="ma-1"
:input-value="data.selected"
close
@click:close="removeCategory(data.index)"
label
color="accent"
dark
>
{{ data.item }}
</v-chip>
</template>
</v-combobox>
:show-add="true"
:show-label="false"
/>
<h2 class="mt-4">{{ $t("recipe.tags") }}</h2>
<v-combobox
dense
multiple
chips
deletable-chips
<CategoryTagSelector
:return-object="false"
v-model="value.tags"
hide-selected
:items="allTags"
:search-input.sync="tagsSearchInput"
@change="tagssSearchInput = ''"
>
<template v-slot:selection="data">
<v-chip
class="ma-1"
:input-value="data.selected"
close
label
@click:close="removeTags(data.index)"
color="accent"
dark
>
{{ data.item }}
</v-chip>
</template>
</v-combobox>
:show-add="true"
:tag-selector="true"
:show-label="false"
/>
<h2 class="my-4">{{ $t("recipe.notes") }}</h2>
<v-card
@ -265,11 +226,13 @@ import { api } from "@/api";
import utils from "@/utils";
import BulkAdd from "./BulkAdd";
import ExtrasEditor from "./ExtrasEditor";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
export default {
components: {
BulkAdd,
ExtrasEditor,
draggable,
CategoryTagSelector,
},
props: {
value: Object,
@ -285,26 +248,14 @@ export default {
v.split(" ").length <= 1 ||
this.$i18n.t("recipe.no-white-space-allowed"),
},
categoriesSearchInput: "",
tagsSearchInput: "",
};
},
computed: {
allCategories() {
const categories = this.$store.getters.getAllCategories;
return categories.map(cat => cat.name);
},
allTags() {
const tags = this.$store.getters.getAllTags;
return tags.map(cat => cat.name);
},
},
methods: {
uploadImage() {
this.$emit("upload", this.fileObject);
},
async updateImage() {
let slug = this.value.slug;
const slug = this.value.slug;
api.recipes.updateImage(slug, this.fileObject);
},
toggleDisabled(stepIndex) {
@ -327,9 +278,6 @@ export default {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
deleteRecipe() {
this.$emit("delete");
},
appendIngredients(ingredients) {
this.value.recipeIngredient.push(...ingredients);

View file

@ -1,7 +1,7 @@
<template>
<div>
<v-btn icon @click="dialog = true">
<v-icon color="white">mdi-plus</v-icon>
<v-btn icon @click="dialog = true" class="mt-n1">
<v-icon :color="color">mdi-plus</v-icon>
</v-btn>
<v-dialog v-model="dialog" width="500">
<v-card>
@ -11,7 +11,7 @@
</v-icon>
<v-toolbar-title class="headline">
Create a Category
{{ title }}
</v-toolbar-title>
<v-spacer></v-spacer>
@ -21,8 +21,8 @@
<v-card-text>
<v-text-field
dense
label="Category Name"
v-model="categoryName"
:label="inputLabel"
v-model="itemName"
:rules="[rules.required]"
></v-text-field>
</v-card-text>
@ -31,7 +31,7 @@
<v-btn color="grey" text @click="dialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="success" text type="submit" :disabled="!categoryName">
<v-btn color="success" text type="submit" :disabled="!itemName">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
@ -43,31 +43,56 @@
<script>
import { api } from "@/api";
const CREATED_ITEM_EVENT = "created-item";
export default {
props: {
buttonText: String,
value: String,
color: {
default: null,
},
tagDialog: {
default: true,
},
},
data() {
return {
dialog: false,
categoryName: "",
itemName: "",
rules: {
required: val =>
!!val || this.$t("settings.theme.theme-name-is-required"),
},
};
},
computed: {
title() {
return this.tagDialog ? "Create a Tag" : "Create a Category";
},
inputLabel() {
return this.tagDialog ? "Tag Name" : "Tag Category";
},
},
watch: {
dialog(val) {
if (!val) this.categoryName = "";
if (!val) this.itemName = "";
},
},
methods: {
async select() {
await api.categories.create(this.categoryName);
this.$emit("new-category", this.categoryName);
const newItem = await (async () => {
if (this.tagDialog) {
const newItem = await api.tags.create(this.itemName);
return newItem;
} else {
const newItem = await api.categories.create(this.itemName);
return newItem;
}
})();
this.$emit(CREATED_ITEM_EVENT, newItem);
this.dialog = false;
},
},

View file

@ -17,11 +17,18 @@
</v-text-field>
</template>
<v-card v-if="showResults" max-height="500" :max-width="maxWidth">
<v-card-text class="py-1">Results</v-card-text>
<v-card-text class="flex row mx-auto">
<div class="mr-auto">
Results
</div>
<router-link to="/search">
Advanced Search
</router-link>
</v-card-text>
<v-divider></v-divider>
<v-list scrollable>
<v-list scrollable v-if="autoResults">
<v-list-item
v-for="(item, index) in autoResults"
v-for="(item, index) in autoResults.slice(0, 15)"
:key="index"
:to="navOnClick ? `/recipe/${item.item.slug}` : null"
@click="navOnClick ? null : selected(item.item.slug, item.item.name)"

View file

@ -3,7 +3,7 @@ export const validators = {
return {
emailRule: v =>
!v ||
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v) ||
/^[^@\s]+@[^@\s.]+.[^@.\s]+$/.test(v) ||
this.$t('user.e-mail-must-be-valid'),
existsRule: value => !!value || this.$t('general.field-required'),

View file

@ -7,41 +7,19 @@
<v-card-text>
<h2 class="mt-1">{{ $t("recipe.categories") }}</h2>
<v-row>
<v-col sm="12" md="6">
<v-select
outlined
:flat="isFlat"
elavation="0"
v-model="groupSettings.categories"
:items="categories"
item-text="name"
return-object
multiple
chips
:hint="
$t(
'meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans'
)
"
class="mt-2"
persistent-hint
>
<template v-slot:selection="data">
<v-chip
outlined
:input-value="data.selected"
close
@click:close="removeCategory(data.index)"
color="secondary"
dark
>
{{ data.item.name }}
</v-chip>
</template>
</v-select>
</v-col>
</v-row>
<CategoryTagSelector
class="mt-4"
:solo="true"
:dense="false"
v-model="groupSettings.categories"
:return-object="true"
:show-add="true"
:hint="
$t(
'meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans'
)
"
/>
</v-card-text>
<v-divider> </v-divider>
<v-card-text>
@ -59,7 +37,7 @@
<v-row dense class="flex align-center">
<v-switch
class="mx-2"
class="mx-2"
v-model="groupSettings.webhookEnable"
:label="$t('general.enabled')"
></v-switch>
@ -105,9 +83,11 @@
<script>
import { api } from "@/api";
import TimePickerDialog from "@/components/Admin/MealPlanner/TimePickerDialog";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
export default {
components: {
TimePickerDialog,
CategoryTagSelector,
},
data() {
return {
@ -155,6 +135,7 @@ export default {
this.groupSettings.webhookUrls.splice(index, 1);
},
async saveGroupSettings() {
console.log(this.groupSettings);
await api.groups.update(this.groupSettings);
await this.$store.dispatch("requestCurrentGroup");
this.getSiteSettings();
@ -162,9 +143,6 @@ export default {
testWebhooks() {
api.settings.testWebhooks();
},
removeCategory(index) {
this.groupSettings.categories.splice(index, 1);
},
},
};
</script>

View file

@ -28,7 +28,7 @@
<v-col>
<h3 class="pl-2 text-center headline">Category Filter</h3>
<FilterSelector class="mb-1" @update="updateCatParams" />
<CategorySelector
<CategoryTagSelector
:solo="true"
:dense="false"
v-model="includeCategories"
@ -38,11 +38,13 @@
<v-col>
<h3 class="pl-2 text-center headline">Tag Filter</h3>
<FilterSelector class="mb-1" @update="updateTagParams" />
<TagSelector
<CategoryTagSelector
:solo="true"
:dense="false"
v-model="includeTags"
:return-object="false"
:tag-selector="true"
/>
</v-col>
</v-row>
@ -74,16 +76,14 @@
import Fuse from "fuse.js";
import RecipeCard from "@/components/Recipe/RecipeCard";
import CategorySidebar from "@/components/UI/CategorySidebar";
import CategorySelector from "@/components/FormHelpers/CategorySelector";
import TagSelector from "@/components/FormHelpers/TagSelector";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import FilterSelector from "./FilterSelector.vue";
export default {
components: {
RecipeCard,
CategorySidebar,
CategorySelector,
TagSelector,
CategoryTagSelector,
FilterSelector,
},
data() {