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

Use composition API for more components, enable more type checking (#914)

* Activate more linting rules from eslint and typescript

* Properly add VForm as type information

* Fix usage of native types

* Fix more linting issues

* Rename vuetify types file, add VTooltip

* Fix some more typing problems

* Use composition API for more components

* Convert RecipeRating

* Convert RecipeNutrition

* Convert more components to composition API

* Fix globals plugin for type checking

* Add missing icon types

* Fix vuetify types in Nuxt context

* Use composition API for RecipeActionMenu

* Convert error.vue to composition API

* Convert RecipeContextMenu to composition API

* Use more composition API and type checking in recipe/create

* Convert AppButtonUpload to composition API

* Fix some type checking in RecipeContextMenu

* Remove unused components BaseAutoForm and BaseColorPicker

* Convert RecipeCategoryTagDialog to composition API

* Convert RecipeCardSection to composition API

* Convert RecipeCategoryTagSelector to composition API

* Properly import vuetify type definitions

* Convert BaseButton to composition API

* Convert AutoForm to composition API

* Remove unused requests API file

* Remove static routes from recipe API

* Fix more type errors

* Convert AppHeader to composition API, fixing some search bar focus problems

* Convert RecipeDialogSearch to composition API

* Update API types from pydantic models, handle undefined values

* Improve more typing problems

* Add types to other plugins

* Properly type the CRUD API access

* Fix typing of static image routes

* Fix more typing stuff

* Fix some more typing problems

* Turn off more rules
This commit is contained in:
Philipp Fischbeck 2022-01-09 07:15:23 +01:00 committed by GitHub
parent d5ab5ec66f
commit 86c99b10a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 2218 additions and 2033 deletions

View file

@ -22,50 +22,58 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent, onMounted, useContext } from "@nuxtjs/composition-api";
const UPDATE_EVENT = "input";
export default {
export default defineComponent({
props: {
importBackup: {
type: Boolean,
default: false,
},
},
data() {
return {
options: {
recipes: {
value: true,
text: this.$t("general.recipes"),
},
users: {
value: true,
text: this.$t("user.users"),
},
groups: {
value: true,
text: this.$t("group.groups"),
},
setup(_, context) {
const { i18n } = useContext();
const options = {
recipes: {
value: true,
text: i18n.t("general.recipes"),
},
forceImport: false,
};
},
mounted() {
this.emitValue();
},
methods: {
emitValue() {
this.$emit(UPDATE_EVENT, {
recipes: this.options.recipes.value,
users: {
value: true,
text: i18n.t("user.users"),
},
groups: {
value: true,
text: i18n.t("group.groups"),
},
}
const forceImport = false;
function emitValue() {
context.emit(UPDATE_EVENT, {
recipes: options.recipes.value,
settings: false,
themes: false,
pages: false,
users: this.options.users.value,
groups: this.options.groups.value,
users: options.users.value,
groups: options.groups.value,
notifications: false,
forceImport: this.forceImport,
forceImport,
});
},
}
onMounted(() => {
emitValue();
});
return {
options,
forceImport,
emitValue,
};
},
};
</script>
});
</script>

View file

@ -92,7 +92,8 @@
</v-toolbar>
</template>
<script>
<script lang="ts">
import {defineComponent, ref, useContext} from "@nuxtjs/composition-api";
import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
@ -101,7 +102,7 @@ const DELETE_EVENT = "delete";
const CLOSE_EVENT = "close";
const JSON_EVENT = "json";
export default {
export default defineComponent({
components: { RecipeContextMenu, RecipeFavoriteBadge },
props: {
slug: {
@ -129,69 +130,70 @@ export default {
default: false,
},
},
data() {
return {
deleteDialog: false,
edit: false,
};
},
setup(_, context) {
const deleteDialog = ref(false);
computed: {
editorButtons() {
return [
{
text: this.$t("general.delete"),
icon: this.$globals.icons.delete,
event: DELETE_EVENT,
color: "error",
},
{
text: this.$t("general.json"),
icon: this.$globals.icons.codeBraces,
event: JSON_EVENT,
color: "accent",
},
{
text: this.$t("general.close"),
icon: this.$globals.icons.close,
event: CLOSE_EVENT,
color: "",
},
{
text: this.$t("general.save"),
icon: this.$globals.icons.save,
event: SAVE_EVENT,
color: "success",
},
];
},
},
methods: {
emitHandler(event) {
const { i18n, $globals } = useContext();
const editorButtons = [
{
text: i18n.t("general.delete"),
icon: $globals.icons.delete,
event: DELETE_EVENT,
color: "error",
},
{
text: i18n.t("general.json"),
icon: $globals.icons.codeBraces,
event: JSON_EVENT,
color: "accent",
},
{
text: i18n.t("general.close"),
icon: $globals.icons.close,
event: CLOSE_EVENT,
color: "",
},
{
text: i18n.t("general.save"),
icon: $globals.icons.save,
event: SAVE_EVENT,
color: "success",
},
];
function emitHandler(event: string) {
switch (event) {
case CLOSE_EVENT:
this.$emit(CLOSE_EVENT);
this.$emit("input", false);
context.emit(CLOSE_EVENT);
context.emit("input", false);
break;
case SAVE_EVENT:
this.$emit(SAVE_EVENT);
context.emit(SAVE_EVENT);
break;
case JSON_EVENT:
this.$emit(JSON_EVENT);
context.emit(JSON_EVENT);
break;
case DELETE_EVENT:
this.deleteDialog = true;
deleteDialog.value = true;
break;
default:
break;
}
},
emitDelete() {
this.$emit(DELETE_EVENT);
this.$emit("input", false);
},
}
function emitDelete() {
context.emit(DELETE_EVENT);
context.emit("input", false);
}
return {
deleteDialog,
editorButtons,
emitHandler,
emitDelete,
}
},
};
});
</script>
<style scoped>

View file

@ -75,7 +75,7 @@
<script lang="ts">
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { useStaticRoutes, useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
const BASE_URL = window.location.origin;
@ -107,7 +107,6 @@ export default defineComponent({
},
});
// @ts-ignore
const { $globals, i18n } = useContext();
const iconOptions = [
@ -142,15 +141,16 @@ export default defineComponent({
return iconOptions.find((item) => item.name === icon) || iconOptions[0];
}
const { recipeAssetPath } = useStaticRoutes();
function assetURL(assetName: string) {
return api.recipes.recipeAssetPath(props.slug, assetName);
return recipeAssetPath(props.slug, assetName);
}
function assetEmbed(name: string) {
return `<img src="${BASE_URL}${assetURL(name)}" height="100%" width="100%"> </img>`;
}
function setFileObject(fileObject: any) {
function setFileObject(fileObject: File) {
state.fileObject = fileObject;
}

View file

@ -51,13 +51,15 @@
</v-lazy>
</template>
<script>
import RecipeFavoriteBadge from "./RecipeFavoriteBadge";
import RecipeChips from "./RecipeChips";
import RecipeContextMenu from "./RecipeContextMenu";
import RecipeCardImage from "./RecipeCardImage";
import RecipeRating from "./RecipeRating";
export default {
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeChips from "./RecipeChips.vue";
import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeCardImage from "./RecipeCardImage.vue";
import RecipeRating from "./RecipeRating.vue";
export default defineComponent({
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
props: {
name: {
@ -99,17 +101,17 @@ export default {
default: 200,
},
},
data() {
setup() {
const { $auth } = useContext();
const loggedIn = computed(() => {
return $auth.loggedIn;
});
return {
fallBackImage: false,
loggedIn,
};
},
computed: {
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
},
};
});
</script>
<style>

View file

@ -17,9 +17,11 @@
</div>
</template>
<script>
<script lang="ts">
import {computed, defineComponent, ref, watch} from "@nuxtjs/composition-api";
import { useStaticRoutes, useUserApi } from "~/composables/api";
export default {
export default defineComponent({
props: {
tiny: {
type: Boolean,
@ -50,44 +52,42 @@ export default {
default: 200,
},
},
setup() {
setup(props) {
const api = useUserApi();
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
return { api, recipeImage, recipeSmallImage, recipeTinyImage };
},
data() {
const fallBackImage = ref(false);
const imageSize = computed(() => {
if (props.tiny) return "tiny";
if (props.small) return "small";
if (props.large) return "large";
return "large";
})
watch(() => props.slug, () => {
fallBackImage.value = false;
});
function getImage(slug: string) {
switch (imageSize.value) {
case "tiny":
return recipeTinyImage(slug, props.imageVersion);
case "small":
return recipeSmallImage(slug, props.imageVersion);
case "large":
return recipeImage(slug, props.imageVersion);
}
}
return {
fallBackImage: false,
api,
fallBackImage,
imageSize,
getImage,
};
},
computed: {
imageSize() {
if (this.tiny) return "tiny";
if (this.small) return "small";
if (this.large) return "large";
return "large";
},
},
watch: {
slug() {
this.fallBackImage = false;
},
},
methods: {
getImage(slug) {
switch (this.imageSize) {
case "tiny":
return this.recipeTinyImage(slug, this.imageVersion);
case "small":
return this.recipeSmallImage(slug, this.imageVersion);
case "large":
return this.recipeImage(slug, this.imageVersion);
}
},
},
};
});
</script>
<style scoped>
@ -108,4 +108,4 @@ export default {
margin-left: auto !important;
margin-right: auto !important;
}
</style>
</style>

View file

@ -10,15 +10,7 @@
<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">
<v-img
v-if="!fallBackImage"
:src="getImage(slug)"
@load="fallBackImage = false"
@error="fallBackImage = true"
></v-img>
<v-icon v-else color="primary" class="icon-position" size="100">
{{ $globals.icons.primary }}
</v-icon>
<RecipeCardImage :icon-size="100" :height="125" :slug="slug" small :image-version="image"></RecipeCardImage>
</v-list-item-avatar>
</slot>
<v-list-item-content>
@ -61,15 +53,17 @@
</v-expand-transition>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge";
import RecipeContextMenu from "./RecipeContextMenu";
import { useUserApi } from "~/composables/api";
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeCardImage from "./RecipeCardImage.vue";
export default defineComponent({
components: {
RecipeFavoriteBadge,
RecipeContextMenu,
RecipeCardImage,
},
props: {
name: {
@ -89,8 +83,9 @@ export default defineComponent({
default: 0,
},
image: {
type: [String, null],
default: "",
type: String,
required: false,
default: "abc123",
},
route: {
type: Boolean,
@ -102,24 +97,14 @@ export default defineComponent({
},
},
setup() {
const api = useUserApi();
const { $auth } = useContext();
const loggedIn = computed(() => {
return $auth.loggedIn;
});
return { api };
},
data() {
return {
fallBackImage: false,
};
},
computed: {
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
},
methods: {
getImage(slug) {
return this.api.recipes.recipeSmallImage(slug, this.image);
},
loggedIn,
}
},
});
</script>

View file

@ -102,13 +102,16 @@
</div>
</template>
<script>
import RecipeCard from "./RecipeCard";
import RecipeCardMobile from "./RecipeCardMobile";
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
import RecipeCard from "./RecipeCard.vue";
import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useSorter } from "~/composables/recipes";
import {Recipe} from "~/types/api-types/recipe";
const SORT_EVENT = "sort";
export default {
export default defineComponent({
components: {
RecipeCard,
RecipeCardMobile,
@ -126,100 +129,90 @@ export default {
type: String,
default: null,
},
hardLimit: {
type: [String, Number],
default: 99999,
},
mobileCards: {
type: Boolean,
default: false,
},
singleColumn: {
type: Boolean,
defualt: false,
default: false,
},
recipes: {
type: Array,
type: Array as () => Recipe[],
default: () => [],
},
},
setup() {
setup(props, context) {
const utils = useSorter();
return { utils };
},
data() {
return {
sortLoading: false,
loading: false,
EVENTS: {
az: "az",
rating: "rating",
created: "created",
updated: "updated",
shuffle: "shuffle",
},
const EVENTS = {
az: "az",
rating: "rating",
created: "created",
updated: "updated",
shuffle: "shuffle",
};
},
computed: {
viewScale() {
if (this.mobileCards) return true;
switch (this.$vuetify.breakpoint.name) {
case "xs":
return true;
case "sm":
return true;
default:
return false;
}
},
effectiveHardLimit() {
return Math.min(this.hardLimit, this.recipes.length);
},
displayTitleIcon() {
return this.icon || this.$globals.icons.tags;
},
},
methods: {
async setLoader() {
this.loading = true;
// eslint-disable-next-line promise/param-names
await new Promise((r) => setTimeout(r, 1000));
this.loading = false;
},
navigateRandom() {
const recipe = this.recipes[Math.floor(Math.random() * this.recipes.length)];
this.$router.push(`/recipe/${recipe.slug}`);
},
sortRecipes(sortType) {
this.sortLoading = true;
const sortTarget = [...this.recipes];
const { $globals, $vuetify } = useContext();
const viewScale = computed(() => {
return props.mobileCards || $vuetify.breakpoint.smAndDown;
});
const displayTitleIcon = computed(() => {
return props.icon || $globals.icons.tags;
});
const state = reactive({
sortLoading: false,
})
const router = useRouter();
function navigateRandom() {
if (props.recipes.length > 0) {
const recipe = props.recipes[Math.floor(Math.random() * props.recipes.length)];
if (recipe.slug !== undefined) {
router.push(`/recipe/${recipe.slug}`);
}
}
}
function sortRecipes(sortType: string) {
state.sortLoading = true;
const sortTarget = [...props.recipes];
switch (sortType) {
case this.EVENTS.az:
this.utils.sortAToZ(sortTarget);
case EVENTS.az:
utils.sortAToZ(sortTarget);
break;
case this.EVENTS.rating:
this.utils.sortByRating(sortTarget);
case EVENTS.rating:
utils.sortByRating(sortTarget);
break;
case this.EVENTS.created:
this.utils.sortByCreated(sortTarget);
case EVENTS.created:
utils.sortByCreated(sortTarget);
break;
case this.EVENTS.updated:
this.utils.sortByUpdated(sortTarget);
case EVENTS.updated:
utils.sortByUpdated(sortTarget);
break;
case this.EVENTS.shuffle:
this.utils.shuffle(sortTarget);
case EVENTS.shuffle:
utils.shuffle(sortTarget);
break;
default:
console.log("Unknown Event", sortType);
return;
}
this.$emit(SORT_EVENT, sortTarget);
this.sortLoading = false;
},
context.emit(SORT_EVENT, sortTarget);
state.sortLoading = false;
}
return {
...toRefs(state),
EVENTS,
viewScale,
displayTitleIcon,
navigateRandom,
sortRecipes,
};
},
};
});
</script>
<style>

View file

@ -40,16 +40,14 @@
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api";
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, watch } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
const CREATED_ITEM_EVENT = "created-item";
export default defineComponent({
props: {
buttonText: {
type: String,
default: "Add",
},
value: {
type: String,
default: "",
@ -63,55 +61,49 @@ export default defineComponent({
default: true,
},
},
setup() {
const api = useUserApi();
setup(props, context) {
const title = computed(() => props.tagDialog ? "Create a Tag" : "Create a Category");
const inputLabel = computed(() => props.tagDialog ? "Tag Name" : "Category Name");
return { api };
},
data() {
return {
const rules = {
required: (val: string) => !!val || "A Name is Required",
};
const state = reactive({
dialog: false,
itemName: "",
rules: {
required: (val) => !!val || "A Name is Required",
},
};
},
});
computed: {
title() {
return this.tagDialog ? "Create a Tag" : "Create a Category";
},
inputLabel() {
return this.tagDialog ? "Tag Name" : "Category Name";
},
},
watch: {
dialog(val) {
if (!val) this.itemName = "";
},
},
watch(() => state.dialog, (val: boolean) => {
if (!val) state.itemName = "";
});
methods: {
open() {
this.dialog = true;
},
async select() {
const api = useUserApi();
async function select() {
const newItem = await (async () => {
if (this.tagDialog) {
const { data } = await this.api.tags.createOne({ name: this.itemName });
if (props.tagDialog) {
const { data } = await api.tags.createOne({ name: state.itemName });
return data;
} else {
const { data } = await this.api.categories.createOne({ name: this.itemName });
const { data } = await api.categories.createOne({ name: state.itemName });
return data;
}
})();
console.log(newItem);
this.$emit(CREATED_ITEM_EVENT, newItem);
this.dialog = false;
},
context.emit(CREATED_ITEM_EVENT, newItem);
state.dialog = false;
}
return {
...toRefs(state),
title,
inputLabel,
rules,
select,
};
},
});
</script>

View file

@ -42,18 +42,22 @@
</v-autocomplete>
</template>
<script>
<script lang="ts">
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
import { useUserApi } from "~/composables/api";
import { useTags, useCategories } from "~/composables/recipes";
import { Category } from "~/api/class-interfaces/categories";
import { Tag } from "~/api/class-interfaces/tags";
const MOUNTED_EVENT = "mounted";
export default {
export default defineComponent({
components: {
RecipeCategoryTagDialog,
},
props: {
value: {
type: Array,
type: Array as () => (Category | Tag | string)[],
required: true,
},
solo: {
@ -90,74 +94,74 @@ export default {
},
},
setup() {
const api = useUserApi();
setup(props, context) {
const { allTags, useAsyncGetAll: getAllTags } = useTags();
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
getAllCategories();
getAllTags();
return { api, allTags, allCategories, getAllCategories, getAllTags };
},
const state = reactive({
selected: props.value,
});
watch(() => props.value, (val) => {
state.selected = val;
});
data() {
return {
selected: [],
};
},
const { i18n } = useContext();
const inputLabel = computed(() => {
if (!props.showLabel) return null;
return props.tagSelector ? i18n.t("tag.tags") : i18n.t("recipe.categories");
});
computed: {
inputLabel() {
if (!this.showLabel) return null;
return this.tagSelector ? this.$t("tag.tags") : this.$t("recipe.categories");
},
activeItems() {
let ItemObjects = [];
if (this.tagSelector) ItemObjects = this.allTags;
const activeItems = computed(() => {
let itemObjects: Tag[] | Category[] | null;
if (props.tagSelector) itemObjects = allTags.value;
else {
ItemObjects = this.allCategories;
itemObjects = allCategories.value;
}
if (this.returnObject) return ItemObjects;
if (props.returnObject) return itemObjects;
else {
return ItemObjects.map((x) => x.name);
return itemObjects?.map((x: Tag | Category) => x.name);
}
},
flat() {
if (this.selected) {
return this.selected.length > 0 && this.solo;
});
const flat = computed(() => {
if (state.selected) {
return state.selected.length > 0 && props.solo;
}
return false;
},
},
});
watch: {
value(val) {
this.selected = val;
},
},
mounted() {
this.$emit(MOUNTED_EVENT);
this.setInit(this.value);
},
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;
function emitChange() {
context.emit("input", state.selected);
}
// TODO Is this needed?
onMounted(() => {
context.emit(MOUNTED_EVENT);
});
function removeByIndex(index: number) {
state.selected.splice(index, 1);
}
function pushToItem(createdItem: Tag | Category) {
// TODO: Remove excessive get calls
this.getAllCategories();
this.getAllTags();
this.selected.push(createdItem);
},
getAllCategories();
getAllTags();
state.selected.push(createdItem);
}
return {
...toRefs(state),
inputLabel,
activeItems,
flat,
emitChange,
removeByIndex,
pushToItem,
};
},
};
});
</script>

View file

@ -27,7 +27,7 @@
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, useContext, computed } from "@nuxtjs/composition-api";
import { defineComponent, reactive, toRefs, useContext, computed, useMeta } from "@nuxtjs/composition-api";
type ItemType = "tags" | "categories" | "tools";
@ -54,7 +54,6 @@ export default defineComponent({
},
},
setup(props) {
// @ts-ignore
const { i18n, $globals } = useContext();
const state = reactive({
@ -77,8 +76,12 @@ export default defineComponent({
break;
}
useMeta(() => ({
title: state.headline,
}));
const itemsSorted = computed(() => {
const byLetter: { [key: string]: Array<any> } = {};
const byLetter: { [key: string]: Array<GenericItem> } = {};
if (!props.items) return byLetter;
@ -99,10 +102,7 @@ export default defineComponent({
itemsSorted,
};
},
head() {
return {
title: this.headline as string,
}
},
// Needed for useMeta
head: {},
});
</script>

View file

@ -16,8 +16,10 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import {computed, defineComponent} from "@nuxtjs/composition-api";
export default defineComponent({
props: {
truncate: {
type: Boolean,
@ -48,39 +50,23 @@ export default {
default: null,
},
},
computed: {
allCategories() {
return this.$store.getters.getAllCategories || [];
},
allTags() {
return this.$store.getters.getAllTags || [];
},
urlParam() {
return this.isCategory ? "categories" : "tags";
},
},
methods: {
getSlug(name) {
if (!name) return;
setup(props) {
const urlParam = computed(() => props.isCategory ? "categories" : "tags");
if (this.isCategory) {
const matches = this.allCategories.filter((x) => x.name === name);
if (matches.length > 0) return matches[0].slug;
} else {
const matches = this.allTags.filter((x) => x.name === name);
if (matches.length > 0) return matches[0].slug;
}
},
truncateText(text, length = 20, clamp) {
if (!this.truncate) return text;
clamp = clamp || "...";
function truncateText(text: string, length = 20, clamp = "...") {
if (!props.truncate) return text;
const node = document.createElement("div");
node.innerHTML = text;
const content = node.textContent;
const content = node.textContent || "";
return content.length > length ? content.slice(0, length) + clamp : content;
},
}
return {
urlParam,
truncateText,
}
},
};
});
</script>
<style></style>

View file

@ -167,7 +167,6 @@ export default defineComponent({
pickerMenu: false,
});
// @ts-ignore
const { i18n, $globals } = useContext();
// ===========================================================================
@ -262,14 +261,12 @@ export default defineComponent({
}
// Note: Print is handled as an event in the parent component
const eventHandlers: { [key: string]: Function } = {
// @ts-ignore - Doens't know about open()
const eventHandlers: { [key: string]: () => void } = {
delete: () => {
state.recipeDeleteDialog = true;
},
edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"),
download: handleDownloadEvent,
// @ts-ignore - Doens't know about open()
mealplanner: () => {
state.mealplannerDialog = true;
},

View file

@ -38,7 +38,7 @@
</template>
</v-data-table>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
import RecipeChip from "./RecipeChips.vue";
@ -49,13 +49,13 @@ import { UserOut } from "~/types/api-types/user";
const INPUT_EVENT = "input";
interface ShowHeaders {
id: Boolean;
owner: Boolean;
tags: Boolean;
categories: Boolean;
tools: Boolean;
recipeYield: Boolean;
dateAdded: Boolean;
id: boolean;
owner: boolean;
tags: boolean;
categories: boolean;
tools: boolean;
recipeYield: boolean;
dateAdded: boolean;
}
export default defineComponent({
@ -129,7 +129,7 @@ export default defineComponent({
// ============
// Group Members
const api = useUserApi();
const members = ref<UserOut[] | null[]>([]);
const members = ref<UserOut[]>([]);
async function refreshMembers() {
const { data } = await api.groups.fetchMembers();
@ -142,10 +142,9 @@ export default defineComponent({
refreshMembers();
});
function getMember(id: number) {
function getMember(id: string) {
if (members.value[0]) {
// @ts-ignore
return members.value.find((m) => m.id === id).username;
return members.value.find((m) => m.id === id)?.username;
}
return "None";
@ -165,4 +164,4 @@ export default defineComponent({
},
},
});
</script>
</script>

View file

@ -54,7 +54,7 @@
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, ref, watch } from "@nuxtjs/composition-api";
import { defineComponent, toRefs, reactive, ref, watch, useRoute } from "@nuxtjs/composition-api";
import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useRecipes, allRecipes, useRecipeSearch } from "~/composables/recipes";
import { RecipeSummary } from "~/types/api-types/recipe";
@ -74,7 +74,7 @@ export default defineComponent({
});
// ===========================================================================
// Dialong State Management
// Dialog State Management
const dialog = ref(false);
// Reset or Grab Recipes on Change
@ -89,6 +89,53 @@ export default defineComponent({
}
});
// ===========================================================================
// Event Handlers
function selectRecipe() {
const recipeCards = document.getElementsByClassName("arrow-nav");
if (recipeCards) {
if (state.selectedIndex < 0) {
state.selectedIndex = -1;
document.getElementById("arrow-search")?.focus();
return;
}
if (state.selectedIndex >= recipeCards.length) {
state.selectedIndex = recipeCards.length - 1;
}
(recipeCards[state.selectedIndex] as HTMLElement).focus();
}
}
function onUpDown(e: KeyboardEvent) {
if (e.key === "Enter") {
console.log(document.activeElement);
// (document.activeElement as HTMLElement).click();
} else if (e.key === "ArrowUp") {
e.preventDefault();
state.selectedIndex--;
} else if (e.key === "ArrowDown") {
e.preventDefault();
state.selectedIndex++;
} else {
return;
}
selectRecipe();
}
watch(dialog, (val) => {
if (!val) {
document.removeEventListener("keyup", onUpDown);
} else {
document.addEventListener("keyup", onUpDown);
}
});
const route = useRoute();
watch(route, close);
function open() {
dialog.value = true;
}
@ -110,56 +157,6 @@ export default defineComponent({
return { allRecipes, refreshRecipes, ...toRefs(state), dialog, open, close, handleSelect, search, results };
},
data() {
return {};
},
computed: {},
watch: {
$route() {
this.dialog = false;
},
dialog() {
if (!this.dialog) {
document.removeEventListener("keyup", this.onUpDown);
} else {
document.addEventListener("keyup", this.onUpDown);
}
},
},
methods: {
onUpDown(e: KeyboardEvent) {
if (e.key === "Enter") {
console.log(document.activeElement);
// (document.activeElement as HTMLElement).click();
} else if (e.key === "ArrowUp") {
e.preventDefault();
this.selectedIndex--;
} else if (e.key === "ArrowDown") {
e.preventDefault();
this.selectedIndex++;
} else {
return;
}
this.selectRecipe();
},
selectRecipe() {
const recipeCards = document.getElementsByClassName("arrow-nav");
if (recipeCards) {
if (this.selectedIndex < 0) {
this.selectedIndex = -1;
document.getElementById("arrow-search")?.focus();
return;
}
if (this.selectedIndex >= recipeCards.length) {
this.selectedIndex = recipeCards.length - 1;
}
(recipeCards[this.selectedIndex] as HTMLElement).focus();
}
},
},
});
</script>
@ -167,4 +164,4 @@ export default defineComponent({
.scroll {
overflow-y: scroll;
}
</style>
</style>

View file

@ -20,9 +20,10 @@
</v-tooltip>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api";
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import {UserOut} from "~/types/api-types/user";
export default defineComponent({
props: {
slug: {
@ -38,31 +39,29 @@ export default defineComponent({
default: false,
},
},
setup() {
setup(props) {
const api = useUserApi();
const { $auth } = useContext();
return { api };
},
computed: {
user() {
return this.$auth.user;
},
isFavorite() {
return this.$auth.user.favoriteRecipes.includes(this.slug);
},
},
methods: {
async toggleFavorite() {
if (!this.isFavorite) {
await this.api.users.addFavorite(this.$auth.user.id, this.slug);
// TODO Setup the correct type for $auth.user
// See https://github.com/nuxt-community/auth-module/issues/1097
const user = computed(() => $auth.user as unknown as UserOut);
const isFavorite = computed(() => user.value?.favoriteRecipes?.includes(props.slug));
async function toggleFavorite() {
console.log("Favorited?");
if (!isFavorite.value) {
await api.users.addFavorite(user.value?.id, props.slug);
} else {
await this.api.users.removeFavorite(this.$auth.user.id, this.slug);
await api.users.removeFavorite(user.value?.id, props.slug);
}
this.$auth.fetchUser();
},
$auth.fetchUser();
};
return { isFavorite, toggleFavorite };
},
});
</script>
<style lang="scss" scoped>
</style>
</style>

View file

@ -25,7 +25,7 @@
</v-card-title>
<v-card-text class="mt-n5">
<div>
<v-text-field v-model="url" :label="$t('general.url')" class="pt-5" clearable :messages="getMessages()">
<v-text-field v-model="url" :label="$t('general.url')" class="pt-5" clearable :messages="messages">
<template #append-outer>
<v-btn class="ml-2" color="primary" :loading="loading" :disabled="!slug" @click="getImageFromURL">
{{ $t("general.get") }}
@ -39,11 +39,13 @@
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api";
<script lang="ts">
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload";
export default defineComponent({
props: {
slug: {
@ -51,32 +53,37 @@ export default defineComponent({
required: true,
},
},
setup() {
const api = useUserApi();
setup(props, context) {
const state = reactive({
url: "",
loading: false,
menu: false,
})
return { api };
},
data: () => ({
url: "",
loading: false,
menu: false,
}),
methods: {
uploadImage(fileObject) {
this.$emit(UPLOAD_EVENT, fileObject);
this.menu = false;
},
async getImageFromURL() {
this.loading = true;
if (await this.api.recipes.updateImagebyURL(this.slug, this.url)) {
this.$emit(REFRESH_EVENT);
function uploadImage(fileObject: File) {
context.emit(UPLOAD_EVENT, fileObject);
state.menu = false;
}
const api = useUserApi();
async function getImageFromURL() {
state.loading = true;
if (await api.recipes.updateImagebyURL(props.slug, state.url)) {
context.emit(REFRESH_EVENT);
}
this.loading = false;
this.menu = false;
},
getMessages() {
return this.slug ? [""] : [this.$i18n.t("recipe.save-recipe-before-use")];
},
state.loading = false;
state.menu = false;
}
const { i18n } = useContext();
const messages = props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")];
return {
...toRefs(state),
uploadImage,
getImageFromURL,
messages,
};
},
});
</script>

View file

@ -105,11 +105,12 @@
import { defineComponent, reactive, ref, toRefs } from "@nuxtjs/composition-api";
import { useFoods, useUnits } from "~/composables/recipes";
import { validators } from "~/composables/use-validators";
import { RecipeIngredient } from "~/types/api-types/recipe";
export default defineComponent({
props: {
value: {
type: Object,
type: Object as () => RecipeIngredient,
required: true,
},
disableAmount: {
@ -157,14 +158,14 @@ export default defineComponent({
}
function handleUnitEnter() {
if (value.unit === null || !value.unit.name.includes(unitSearch.value)) {
if (value.unit === undefined || !value.unit.name.includes(unitSearch.value)) {
console.log("Creating");
createAssignUnit();
}
}
function handleFoodEnter() {
if (value.food === null || !value.food.name.includes(foodSearch.value)) {
if (value.food === undefined || !value.food.name.includes(foodSearch.value)) {
console.log("Creating");
createAssignFood();
}
@ -194,4 +195,4 @@ export default defineComponent({
margin: 0 !important;
padding: 0 !important;
}
</style>
</style>

View file

@ -23,17 +23,20 @@
</div>
</template>
<script>
import { computed, defineComponent } from "@nuxtjs/composition-api";
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from "@nuxtjs/composition-api";
// @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown";
import { parseIngredientText } from "~/composables/recipes";
import { RecipeIngredient } from "~/types/api-types/recipe";
export default defineComponent({
components: {
VueMarkdown,
},
props: {
value: {
type: Array,
type: Array as () => RecipeIngredient[],
default: () => [],
},
disableAmount: {
@ -46,6 +49,15 @@ export default defineComponent({
},
},
setup(props) {
function validateTitle(title?: string) {
return !(title === undefined || title === "");
}
const state = reactive({
checked: props.value.map(() => false),
showTitleEditor: computed(() => props.value.map((x) => validateTitle(x.title))),
});
const ingredientCopyText = computed(() => {
return props.value
.map((ingredient) => {
@ -54,41 +66,18 @@ export default defineComponent({
.join("\n");
});
return { parseIngredientText, ingredientCopyText };
},
data() {
return {
drag: false,
checked: [],
showTitleEditor: [],
};
},
watch: {
value: {
handler() {
this.showTitleEditor = this.value.map((x) => this.validateTitle(x.title));
},
},
},
mounted() {
this.checked = this.value.map(() => false);
this.showTitleEditor = this.value.map((x) => this.validateTitle(x.title));
},
methods: {
toggleChecked(index) {
this.$set(this.checked, index, !this.checked[index]);
},
function toggleChecked(index: number) {
// TODO Find a better way to do this - $set is not available, and
// direct array modifications are not propagated for some reason
state.checked.splice(index, 1, !state.checked[index]);
}
validateTitle(title) {
return !(title === null || title === "");
},
toggleShowTitle(index) {
const newVal = !this.showTitleEditor[index];
if (!newVal) {
this.value[index].title = "";
}
this.$set(this.showTitleEditor, index, newVal);
},
return {
...toRefs(state),
parseIngredientText,
ingredientCopyText,
toggleChecked,
};
},
});
</script>

View file

@ -153,7 +153,7 @@ import draggable from "vuedraggable";
// @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown";
import { ref, toRefs, reactive, defineComponent, watch, onMounted } from "@nuxtjs/composition-api";
import { RecipeStep, IngredientToStepRef, RecipeIngredient } from "~/types/api-types/recipe";
import { RecipeStep, IngredientReferences, RecipeIngredient } from "~/types/api-types/recipe";
import { parseIngredientText } from "~/composables/recipes";
import { uuid4 } from "~/composables/use-utils";
@ -227,14 +227,18 @@ export default defineComponent({
state.disabledSteps = [];
v.forEach((element) => {
showTitleEditor.value[element.id] = validateTitle(element.title);
if (element.id !== undefined) {
showTitleEditor.value[element.id] = validateTitle(element.title);
}
});
});
// Eliminate state with an eager call to watcher?
onMounted(() => {
props.value.forEach((element) => {
showTitleEditor.value[element.id] = validateTitle(element.title);
if (element.id !== undefined) {
showTitleEditor.value[element.id] = validateTitle(element.title);
}
});
});
@ -268,23 +272,23 @@ export default defineComponent({
// ===============================================================
// Ingredient Linker
const activeRefs = ref<String[]>([]);
const activeRefs = ref<string[]>([]);
const activeIndex = ref(0);
const activeText = ref("");
function openDialog(idx: number, refs: IngredientToStepRef[], text: string) {
function openDialog(idx: number, refs: IngredientReferences[], text: string) {
setUsedIngredients();
activeText.value = text;
activeIndex.value = idx;
state.dialog = true;
activeRefs.value = refs.map((ref) => ref.referenceId);
activeRefs.value = refs.map((ref) => ref.referenceId ?? "");
}
function setIngredientIds() {
const instruction = props.value[activeIndex.value];
instruction.ingredientReferences = activeRefs.value.map((ref) => {
return {
referenceId: ref as string,
referenceId: ref,
};
});
state.dialog = false;
@ -294,17 +298,19 @@ export default defineComponent({
const usedRefs: { [key: string]: boolean } = {};
props.value.forEach((element) => {
element.ingredientReferences.forEach((ref) => {
usedRefs[ref.referenceId] = true;
element.ingredientReferences?.forEach((ref) => {
if (ref.referenceId !== undefined) {
usedRefs[ref.referenceId] = true;
}
});
});
state.usedIngredients = props.ingredients.filter((ing) => {
return ing.referenceId in usedRefs;
return ing.referenceId !== undefined && ing.referenceId in usedRefs;
});
state.unusedIngredients = props.ingredients.filter((ing) => {
return !(ing.referenceId in usedRefs);
return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
});
}
@ -343,6 +349,10 @@ export default defineComponent({
props.ingredients.forEach((ingredient) => {
const searchText = parseIngredientText(ingredient, props.disableAmount);
if (ingredient.referenceId === undefined) {
return;
}
if (searchText.toLowerCase().includes(" " + word) && !activeRefs.value.includes(ingredient.referenceId)) {
console.info("Word Matched", `'${word}'`, ingredient.note);
activeRefs.value.push(ingredient.referenceId);
@ -351,7 +361,7 @@ export default defineComponent({
});
}
function getIngredientByRefId(refId: String) {
function getIngredientByRefId(refId: string) {
const ing = props.ingredients.find((ing) => ing.referenceId === refId) || "";
if (ing === "") {
return "";

View file

@ -31,9 +31,12 @@
</div>
</template>
<script>
<script lang="ts">
// @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown";
export default {
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
components: {
VueMarkdown,
},
@ -48,15 +51,21 @@ export default {
default: true,
},
},
methods: {
addNote() {
this.value.push({ title: "", text: "" });
},
removeByIndex(list, index) {
setup(props) {
function addNote() {
props.value.push({ title: "", text: "" });
}
function removeByIndex(list: unknown[], index: number) {
list.splice(index, 1);
},
}
return {
addNote,
removeByIndex,
};
},
};
});
</script>
<style></style>

View file

@ -33,11 +33,14 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { Nutrition } from "~/types/api-types/recipe";
export default defineComponent({
props: {
value: {
type: Object,
type: Object as () => Nutrition,
required: true,
},
edit: {
@ -45,59 +48,59 @@ export default {
default: true,
},
},
data() {
return {
labels: {
calories: {
label: this.$t("recipe.calories"),
suffix: this.$t("recipe.calories-suffix"),
},
fatContent: {
label: this.$t("recipe.fat-content"),
suffix: this.$t("recipe.grams"),
},
fiberContent: {
label: this.$t("recipe.fiber-content"),
suffix: this.$t("recipe.grams"),
},
proteinContent: {
label: this.$t("recipe.protein-content"),
suffix: this.$t("recipe.grams"),
},
sodiumContent: {
label: this.$t("recipe.sodium-content"),
suffix: this.$t("recipe.milligrams"),
},
sugarContent: {
label: this.$t("recipe.sugar-content"),
suffix: this.$t("recipe.grams"),
},
carbohydrateContent: {
label: this.$t("recipe.carbohydrate-content"),
suffix: this.$t("recipe.grams"),
},
setup(props, context) {
const { i18n } = useContext();
const labels = {
calories: {
label: i18n.t("recipe.calories"),
suffix: i18n.t("recipe.calories-suffix"),
},
fatContent: {
label: i18n.t("recipe.fat-content"),
suffix: i18n.t("recipe.grams"),
},
fiberContent: {
label: i18n.t("recipe.fiber-content"),
suffix: i18n.t("recipe.grams"),
},
proteinContent: {
label: i18n.t("recipe.protein-content"),
suffix: i18n.t("recipe.grams"),
},
sodiumContent: {
label: i18n.t("recipe.sodium-content"),
suffix: i18n.t("recipe.milligrams"),
},
sugarContent: {
label: i18n.t("recipe.sugar-content"),
suffix: i18n.t("recipe.grams"),
},
carbohydrateContent: {
label: i18n.t("recipe.carbohydrate-content"),
suffix: i18n.t("recipe.grams"),
},
};
},
computed: {
showViewer() {
return !this.edit && this.valueNotNull;
},
valueNotNull() {
for (const property in this.value) {
const valueProperty = this.value[property];
const valueNotNull = computed(() => {
Object.values(props.value).forEach((valueProperty) => {
if (valueProperty && valueProperty !== "") return true;
}
});
return false;
},
},
});
methods: {
updateValue(key, value) {
this.$emit("input", { ...this.value, [key]: value });
},
const showViewer = computed(() => !props.edit && valueNotNull.value);
function updateValue(key: number | string, event: Event) {
context.emit("input", { ...props.value, [key]: event });
}
return {
labels,
valueNotNull,
showViewer,
updateValue
}
},
};
});
</script>
<style lang="scss" scoped></style>

View file

@ -16,8 +16,8 @@
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api";
<script lang="ts">
import { computed, defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
export default defineComponent({
props: {
@ -25,6 +25,7 @@ export default defineComponent({
type: Boolean,
default: false,
},
// TODO Remove name prop?
name: {
type: String,
default: "",
@ -42,36 +43,26 @@ export default defineComponent({
default: false,
},
},
setup() {
const api = useUserApi();
setup(props, context) {
const { $auth } = useContext();
const loggedIn = computed(() => {
return $auth.loggedIn;
});
return { api };
},
data() {
return {
rating: 0,
};
},
computed: {
loggedIn() {
return this.$auth.loggedIn;
},
},
mounted() {
this.rating = this.value;
},
methods: {
updateRating(val) {
if (this.emitOnly) {
this.$emit("input", val);
const rating = ref(props.value);
const api = useUserApi();
function updateRating(val: number) {
if (props.emitOnly) {
context.emit("input", val);
return;
}
this.api.recipes.patchOne(this.slug, {
name: this.name,
slug: this.slug,
api.recipes.patchOne(props.slug, {
rating: val,
});
},
}
return { loggedIn, rating, updateRating };
},
});
</script>

View file

@ -34,9 +34,10 @@
</div>
</template>
<script>
export default {
components: {},
<script lang="ts">
import { defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
value: {
type: Object,
@ -47,23 +48,23 @@ export default {
required: false,
},
},
setup() {
const { i18n } = useContext();
const labels = {
public: i18n.t("recipe.public-recipe"),
showNutrition: i18n.t("recipe.show-nutrition-values"),
showAssets: i18n.t("asset.show-assets"),
landscapeView: i18n.t("recipe.landscape-view-coming-soon"),
disableComments: i18n.t("recipe.disable-comments"),
disableAmount: i18n.t("recipe.disable-amount"),
locked: "Locked",
};
computed: {
labels() {
return {
public: this.$t("recipe.public-recipe"),
showNutrition: this.$t("recipe.show-nutrition-values"),
showAssets: this.$t("asset.show-assets"),
landscapeView: this.$t("recipe.landscape-view-coming-soon"),
disableComments: this.$t("recipe.disable-comments"),
disableAmount: this.$t("recipe.disable-amount"),
locked: "Locked",
};
},
return {
labels,
}
},
methods: {},
};
});
</script>
<style lang="scss" scoped></style>

View file

@ -10,8 +10,10 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
prepTime: {
type: String,
@ -26,29 +28,39 @@ export default {
default: null,
},
},
computed: {
showCards() {
return [this.prepTime, this.totalTime, this.performTime].some((x) => !this.isEmpty(x));
},
allTimes() {
return [this.validateTotalTime, this.validatePrepTime, this.validatePerformTime].filter((x) => x !== null);
},
validateTotalTime() {
return !this.isEmpty(this.totalTime) ? { name: this.$t("recipe.total-time"), value: this.totalTime } : null;
},
validatePrepTime() {
return !this.isEmpty(this.prepTime) ? { name: this.$t("recipe.prep-time"), value: this.prepTime } : null;
},
validatePerformTime() {
return !this.isEmpty(this.performTime) ? { name: this.$t("recipe.perform-time"), value: this.performTime } : null;
},
},
methods: {
isEmpty(str) {
setup(props) {
const { i18n } = useContext();
function isEmpty(str: string | null) {
return !str || str.length === 0;
},
}
const showCards = computed(() => {
return [props.prepTime, props.totalTime, props.performTime].some((x) => !isEmpty(x));
});
const validateTotalTime = computed(() => {
return !isEmpty(props.totalTime) ? { name: i18n.t("recipe.total-time"), value: props.totalTime } : null;
});
const validatePrepTime = computed(() => {
return !isEmpty(props.prepTime) ? { name: i18n.t("recipe.prep-time"), value: props.prepTime } : null;
});
const validatePerformTime = computed(() => {
return !isEmpty(props.performTime) ? { name: i18n.t("recipe.perform-time"), value: props.performTime } : null;
});
const allTimes = computed(() => {
return [validateTotalTime.value, validatePrepTime.value, validatePerformTime.value].filter((x) => x !== null);
});
return {
showCards,
allTimes,
}
},
};
});
</script>
<style scoped>

View file

@ -9,6 +9,7 @@
<script lang="ts">
import { defineComponent, toRefs, reactive, useContext, computed } from "@nuxtjs/composition-api";
import { UserOut } from "~/types/api-types/user";
export default defineComponent({
props: {
@ -33,7 +34,9 @@ export default defineComponent({
const { $auth } = useContext();
const imageURL = computed(() => {
const key = $auth?.user?.cacheKey || "";
// TODO Setup correct user type for $auth.user
const user = $auth.user as unknown as (UserOut | null);
const key = user?.cacheKey ?? "";
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
});
@ -43,4 +46,4 @@ export default defineComponent({
};
},
});
</script>
</script>