mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-02 20:15:24 +02:00
feature: proper multi-tenant-support (#969)(WIP)
* update naming * refactor tests to use shared structure * shorten names * add tools test case * refactor to support multi-tenant * set group_id on creation * initial refactor for multitenant tags/cats * spelling * additional test case for same valued resources * fix recipe update tests * apply indexes to foreign keys * fix performance regressions * handle unknown exception * utility decorator for function debugging * migrate recipe_id to UUID * GUID for recipes * remove unused import * move image functions into package * move utilities to packages dir * update import * linter * image image and asset routes * update assets and images to use UUIDs * fix migration base * image asset test coverage * use ids for categories and tag crud functions * refactor recipe organizer test suite to reduce duplication * add uuid serlization utility * organizer base router * slug routes testing and fixes * fix postgres error * adopt UUIDs * move tags, categories, and tools under "organizers" umbrella * update composite label * generate ts types * fix import error * update frontend types * fix type errors * fix postgres errors * fix #978 * add null check for title validation * add note in docs on multi-tenancy
This commit is contained in:
parent
9a82a172cb
commit
c617251f4c
157 changed files with 1866 additions and 1578 deletions
|
@ -24,15 +24,7 @@
|
|||
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :slug="slug" show-always />
|
||||
<v-tooltip v-if="!locked" bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
fab
|
||||
small
|
||||
class="mx-1"
|
||||
color="info"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
@click="$emit('input', true)"
|
||||
>
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('input', true)">
|
||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
@ -40,14 +32,7 @@
|
|||
</v-tooltip>
|
||||
<v-tooltip v-else bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
fab
|
||||
small
|
||||
class="mx-1"
|
||||
color="info"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on">
|
||||
<v-icon> {{ $globals.icons.lock }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
@ -93,7 +78,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref, useContext} from "@nuxtjs/composition-api";
|
||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
|
||||
|
@ -123,7 +108,7 @@ export default defineComponent({
|
|||
},
|
||||
recipeId: {
|
||||
required: true,
|
||||
type: Number,
|
||||
type: String,
|
||||
},
|
||||
locked: {
|
||||
type: Boolean,
|
||||
|
@ -191,7 +176,7 @@ export default defineComponent({
|
|||
editorButtons,
|
||||
emitHandler,
|
||||
emitDelete,
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -86,6 +86,10 @@ export default defineComponent({
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
@ -143,7 +147,7 @@ export default defineComponent({
|
|||
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
function assetURL(assetName: string) {
|
||||
return recipeAssetPath(props.slug, assetName);
|
||||
return recipeAssetPath(props.recipeId, assetName);
|
||||
}
|
||||
|
||||
function assetEmbed(name: string) {
|
||||
|
|
|
@ -8,7 +8,14 @@
|
|||
:min-height="imageHeight + 75"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<RecipeCardImage :icon-size="imageHeight" :height="imageHeight" :slug="slug" small :image-version="image">
|
||||
<RecipeCardImage
|
||||
:icon-size="imageHeight"
|
||||
:height="imageHeight"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
:image-version="image"
|
||||
>
|
||||
<v-expand-transition v-if="description">
|
||||
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%">
|
||||
<v-card-text class="v-card--text-show white--text">
|
||||
|
@ -95,7 +102,7 @@ export default defineComponent({
|
|||
},
|
||||
recipeId: {
|
||||
required: true,
|
||||
type: Number,
|
||||
type: String,
|
||||
},
|
||||
imageHeight: {
|
||||
type: Number,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<v-img
|
||||
v-if="!fallBackImage"
|
||||
:height="height"
|
||||
:src="getImage(slug)"
|
||||
:src="getImage(recipeId)"
|
||||
@click="$emit('click')"
|
||||
@load="fallBackImage = false"
|
||||
@error="fallBackImage = true"
|
||||
|
@ -18,7 +18,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, ref, watch} from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, ref, watch } from "@nuxtjs/composition-api";
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -43,6 +43,10 @@ export default defineComponent({
|
|||
type: String,
|
||||
default: null,
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
imageVersion: {
|
||||
type: String,
|
||||
default: null,
|
||||
|
@ -63,20 +67,23 @@ export default defineComponent({
|
|||
if (props.small) return "small";
|
||||
if (props.large) return "large";
|
||||
return "large";
|
||||
})
|
||||
|
||||
watch(() => props.slug, () => {
|
||||
fallBackImage.value = false;
|
||||
});
|
||||
|
||||
function getImage(slug: string) {
|
||||
watch(
|
||||
() => props.recipeId,
|
||||
() => {
|
||||
fallBackImage.value = false;
|
||||
}
|
||||
);
|
||||
|
||||
function getImage(recipeId: string) {
|
||||
switch (imageSize.value) {
|
||||
case "tiny":
|
||||
return recipeTinyImage(slug, props.imageVersion);
|
||||
return recipeTinyImage(recipeId, props.imageVersion);
|
||||
case "small":
|
||||
return recipeSmallImage(slug, props.imageVersion);
|
||||
return recipeSmallImage(recipeId, props.imageVersion);
|
||||
case "large":
|
||||
return recipeImage(slug, props.imageVersion);
|
||||
return recipeImage(recipeId, props.imageVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,14 @@
|
|||
<v-list-item three-line>
|
||||
<slot name="avatar">
|
||||
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
|
||||
<RecipeCardImage :icon-size="100" :height="125" :slug="slug" small :image-version="image"></RecipeCardImage>
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="125"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
:image-version="image"
|
||||
></RecipeCardImage>
|
||||
</v-list-item-avatar>
|
||||
</slot>
|
||||
<v-list-item-content>
|
||||
|
@ -93,7 +100,7 @@ export default defineComponent({
|
|||
default: true,
|
||||
},
|
||||
recipeId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
@ -105,7 +112,7 @@ export default defineComponent({
|
|||
|
||||
return {
|
||||
loggedIn,
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -46,8 +46,7 @@
|
|||
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
|
||||
import { useTags, useCategories } from "~/composables/recipes";
|
||||
import { Category } from "~/api/class-interfaces/categories";
|
||||
import { Tag } from "~/api/class-interfaces/tags";
|
||||
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||
|
||||
const MOUNTED_EVENT = "mounted";
|
||||
|
||||
|
@ -57,7 +56,7 @@ export default defineComponent({
|
|||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => (Category | Tag | string)[],
|
||||
type: Array as () => (RecipeTag | RecipeCategory | string)[],
|
||||
required: true,
|
||||
},
|
||||
solo: {
|
||||
|
@ -103,9 +102,12 @@ export default defineComponent({
|
|||
const state = reactive({
|
||||
selected: props.value,
|
||||
});
|
||||
watch(() => props.value, (val) => {
|
||||
state.selected = val;
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
state.selected = val;
|
||||
}
|
||||
);
|
||||
|
||||
const { i18n } = useContext();
|
||||
const inputLabel = computed(() => {
|
||||
|
@ -114,14 +116,14 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const activeItems = computed(() => {
|
||||
let itemObjects: Tag[] | Category[] | null;
|
||||
let itemObjects: RecipeTag[] | RecipeCategory[] | null;
|
||||
if (props.tagSelector) itemObjects = allTags.value;
|
||||
else {
|
||||
itemObjects = allCategories.value;
|
||||
}
|
||||
if (props.returnObject) return itemObjects;
|
||||
else {
|
||||
return itemObjects?.map((x: Tag | Category) => x.name);
|
||||
return itemObjects?.map((x: RecipeTag | RecipeCategory) => x.name);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -145,7 +147,7 @@ export default defineComponent({
|
|||
state.selected.splice(index, 1);
|
||||
}
|
||||
|
||||
function pushToItem(createdItem: Tag | Category) {
|
||||
function pushToItem(createdItem: RecipeTag | RecipeCategory) {
|
||||
// TODO: Remove excessive get calls
|
||||
getAllCategories();
|
||||
getAllTags();
|
||||
|
@ -164,4 +166,3 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
recipeId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
@ -114,4 +114,4 @@ export default defineComponent({
|
|||
return { api, comments, ...toRefs(state), submitComment, deleteComment };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -168,7 +168,7 @@ export default defineComponent({
|
|||
},
|
||||
recipeId: {
|
||||
required: true,
|
||||
type: Number,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
|
|
|
@ -70,7 +70,7 @@ export default defineComponent({
|
|||
default: false,
|
||||
},
|
||||
recipeId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
|
|
|
@ -158,14 +158,15 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
function handleUnitEnter() {
|
||||
if (value.unit === undefined || !value.unit.name.includes(unitSearch.value)) {
|
||||
if (value.unit === undefined || value.unit === null || !value.unit.name.includes(unitSearch.value)) {
|
||||
console.log("Creating");
|
||||
createAssignUnit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFoodEnter() {
|
||||
if (value.food === undefined || !value.food.name.includes(foodSearch.value)) {
|
||||
console.log(value.food);
|
||||
if (value.food === undefined || value.food === null || !value.food.name.includes(foodSearch.value)) {
|
||||
console.log("Creating");
|
||||
createAssignFood();
|
||||
}
|
||||
|
@ -190,7 +191,7 @@ export default defineComponent({
|
|||
});
|
||||
</script>
|
||||
|
||||
<style >
|
||||
<style>
|
||||
.v-input__append-outer {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
|
|
|
@ -50,7 +50,7 @@ export default defineComponent({
|
|||
},
|
||||
setup(props) {
|
||||
function validateTitle(title?: string) {
|
||||
return !(title === undefined || title === "");
|
||||
return !(title === undefined || title === "" || title === null);
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue