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
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

@ -34,8 +34,11 @@
</v-tooltip>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { VTooltip } from "~/types/vuetify";
export default defineComponent({
props: {
copyText: {
type: String,
@ -54,30 +57,34 @@ export default {
default: "",
},
},
data() {
return {
show: false,
};
},
setup(props) {
const show = ref(false);
const copyToolTip = ref<VTooltip | null>(null);
methods: {
toggleBlur() {
this.$refs.copyToolTip.deactivate();
},
textToClipboard() {
this.show = true;
const copyText = this.copyText;
function toggleBlur() {
copyToolTip.value?.deactivate();
}
function textToClipboard() {
show.value = true;
const copyText = props.copyText;
navigator.clipboard.writeText(copyText).then(
() => console.log(`Copied\n${copyText}`),
() => console.log(`Copied Failed\n${copyText}`)
);
setTimeout(() => {
this.toggleBlur();
toggleBlur();
}, 500);
},
}
return {
show,
copyToolTip,
textToClipboard,
}
},
};
});
</script>
<style lang="scss" scoped>
</style>
</style>

View file

@ -10,10 +10,13 @@
</v-form>
</template>
<script>
<script lang="ts">
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
const UPLOAD_EVENT = "uploaded";
export default {
export default defineComponent({
props: {
small: {
type: Boolean,
@ -48,65 +51,70 @@ export default {
default: "",
},
},
setup() {
setup(props, context) {
const file = ref<File | null>(null);
const uploader = ref<HTMLInputElement | null>(null);
const isSelecting = ref(false);
const { i18n, $globals } = useContext();
const effIcon = props.icon ? props.icon : $globals.icons.upload;
const defaultText = i18n.t("general.upload");
const api = useUserApi();
async function upload() {
if (file.value != null) {
isSelecting.value = true;
return { api };
},
data: () => ({
file: null,
isSelecting: false,
}),
computed: {
effIcon() {
return this.icon ? this.icon : this.$globals.icons.upload;
},
defaultText() {
return this.$t("general.upload");
},
},
methods: {
async upload() {
if (this.file != null) {
this.isSelecting = true;
if (!this.post) {
this.$emit(UPLOAD_EVENT, this.file);
this.isSelecting = false;
if (!props.post) {
context.emit(UPLOAD_EVENT, file.value);
isSelecting.value = false;
return;
}
const formData = new FormData();
formData.append(this.fileName, this.file);
formData.append(props.fileName, file.value);
const response = await this.api.upload.file(this.url, formData);
const response = await api.upload.file(props.url, formData);
if (response) {
this.$emit(UPLOAD_EVENT, response);
context.emit(UPLOAD_EVENT, response);
}
this.isSelecting = false;
isSelecting.value = false;
}
},
onButtonClick() {
this.isSelecting = true;
}
function onFileChanged(e: Event) {
const target = e.target as HTMLInputElement;
if (target.files !== null && target.files.length > 0 && file.value !== null) {
file.value = target.files[0];
upload();
}
}
function onButtonClick() {
isSelecting.value = true;
window.addEventListener(
"focus",
() => {
this.isSelecting = false;
isSelecting.value = false;
},
{ once: true }
);
uploader.value?.click();
}
this.$refs.uploader.click();
},
onFileChanged(e) {
this.file = e.target.files[0];
this.upload();
},
return {
file,
uploader,
isSelecting,
effIcon,
defaultText,
onFileChanged,
onButtonClick,
};
},
};
});
</script>
<style></style>

View file

@ -20,8 +20,10 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
loading: {
type: Boolean,
@ -40,15 +42,15 @@ export default {
default: false,
},
},
computed: {
size() {
if (this.small) {
setup(props) {
const size = computed(() => {
if (props.small) {
return {
width: 2,
icon: 30,
size: 50,
};
} else if (this.large) {
} else if (props.large) {
return {
width: 4,
icon: 120,
@ -60,10 +62,15 @@ export default {
icon: 75,
size: 125,
};
},
waitingText() {
return this.$t("general.loading-recipes");
},
});
const { i18n } = useContext();
const waitingText = i18n.t("general.loading-recipes");
return {
size,
waitingText,
};
},
};
});
</script>

View file

@ -137,14 +137,15 @@
</v-card>
</template>
<script>
import { ref } from "@nuxtjs/composition-api";
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api";
import { validators } from "@/composables/use-validators";
import { fieldTypes } from "@/composables/forms";
import { AutoFormItems } from "~/types/auto-forms";
const BLUR_EVENT = "blur";
export default {
export default defineComponent({
name: "AutoForm",
props: {
value: {
@ -157,7 +158,7 @@ export default {
},
items: {
default: null,
type: Array,
type: Array as () => AutoFormItems,
},
width: {
type: [Number, String],
@ -165,7 +166,7 @@ export default {
},
globalRules: {
default: null,
type: Array,
type: Array as () => string[],
},
color: {
default: null,
@ -176,94 +177,53 @@ export default {
type: Boolean,
},
},
setup() {
const menu = ref({});
setup(props, context) {
function rulesByKey(keys?: string[] | null) {
if (keys === undefined || keys === null) {
return [];
}
return {
menu,
fieldTypes,
validators,
};
},
computed: {
defaultRules() {
return this.rulesByKey(this.globalRules);
},
},
watch: {
items: {
immediate: true,
handler(val) {
// Initialize Value Object to Obtain all keys
if (!val) {
return;
const list = [] as ((v: string) => (boolean | string))[];
keys.forEach((key) => {
if (key in validators) {
list.push(validators[key]);
}
for (let i = 0; i < val.length; i++) {
try {
if (this.value[val[i].varName]) {
continue;
}
} catch {}
});
return list;
}
if (val[i].type === "text" || val[i].type === "textarea") {
this.$set(this.value, val[i].varName, "");
} else if (val[i].type === "select") {
if (!val[i].options[0]) {
continue;
}
const defaultRules = computed(() => rulesByKey(props.globalRules));
this.$set(this.value, val[i].varName, val[i].options[0].value);
} else if (val[i].type === "list") {
this.$set(this.value, val[i].varName, []);
} else if (val[i].type === "object") {
this.$set(this.value, val[i].varName, {});
} else if (val[i].type === "color") {
this.$set(this.value, val[i].varName, "");
this.$set(this.menu, val[i].varName, false);
}
}
},
},
},
methods: {
removeByIndex(list, index) {
function removeByIndex(list: never[], index: number) {
// Removes the item at the index
list.splice(index, 1);
},
getTemplate(item) {
const obj = {};
}
function getTemplate(item: AutoFormItems) {
const obj = {} as { [key: string]: string };
item.forEach((field) => {
obj[field.varName] = "";
});
return obj;
},
rulesByKey(keys) {
const list = [];
}
if (keys === undefined) {
return list;
}
if (keys === null) {
return list;
}
if (keys === list) {
return list;
}
function emitBlur() {
context.emit(BLUR_EVENT, props.value);
}
keys.forEach((key) => {
if (key in this.validators) {
list.push(this.validators[key]);
}
});
return list;
},
emitBlur() {
this.$emit(BLUR_EVENT, this.value);
},
return {
rulesByKey,
defaultRules,
removeByIndex,
getTemplate,
emitBlur,
fieldTypes,
validators,
};
},
};
});
</script>
<style lang="scss" scoped></style>

View file

@ -1,269 +0,0 @@
<template>
<v-card :color="color" :dark="dark" flat :width="width" class="my-2">
<v-row>
<v-col v-for="(inputField, index) in items" :key="index" class="py-0" cols="12" sm="12">
<v-divider v-if="inputField.section" class="my-2" />
<v-card-title v-if="inputField.section" class="pl-0">
{{ inputField.section }}
</v-card-title>
<v-card-text v-if="inputField.sectionDetails" class="pl-0 mt-0 pt-0">
{{ inputField.sectionDetails }}
</v-card-text>
<!-- Check Box -->
<v-checkbox
v-if="inputField.type === fieldTypes.BOOLEAN"
v-model="value[inputField.varName]"
class="my-0 py-0"
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
@change="emitBlur"
/>
<!-- Text Field -->
<v-text-field
v-else-if="inputField.type === fieldTypes.TEXT"
v-model="value[inputField.varName]"
:readonly="inputField.fixed && updateMode"
filled
rounded
class="rounded-lg"
dense
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
:rules="[...rulesByKey(inputField.rules), ...defaultRules]"
lazy-validation
@blur="emitBlur"
/>
<!-- Text Area -->
<v-textarea
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
v-model="value[inputField.varName]"
:readonly="inputField.fixed && updateMode"
filled
rounded
class="rounded-lg"
rows="3"
auto-grow
dense
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
:rules="[...rulesByKey(inputField.rules), ...defaultRules]"
lazy-validation
@blur="emitBlur"
/>
<!-- Option Select -->
<v-select
v-else-if="inputField.type === fieldTypes.SELECT"
v-model="value[inputField.varName]"
:readonly="inputField.fixed && updateMode"
filled
rounded
class="rounded-lg"
:prepend-icon="inputField.icons ? value[inputField.varName] : null"
:label="inputField.label"
:name="inputField.varName"
:items="inputField.options"
:return-object="false"
lazy-validation
@blur="emitBlur"
>
<template #item="{ item }">
<v-list-item-content>
<v-list-item-title>{{ item.text }}</v-list-item-title>
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
</v-list-item-content>
</template>
</v-select>
<!-- Color Picker -->
<div v-else-if="inputField.type === fieldTypes.COLOR" class="d-flex" style="width: 100%">
<v-menu offset-y>
<template #activator="{ on }">
<v-btn class="my-2 ml-auto" style="min-width: 200px" :color="value[inputField.varName]" dark v-on="on">
{{ inputField.label }}
</v-btn>
</template>
<v-color-picker
v-model="value[inputField.varName]"
value="#7417BE"
hide-canvas
hide-inputs
show-swatches
class="mx-auto"
@input="emitBlur"
/>
</v-menu>
</div>
<div v-else-if="inputField.type === fieldTypes.OBJECT">
<base-auto-form
v-model="value[inputField.varName]"
:color="color"
:items="inputField.items"
@blur="emitBlur"
/>
</div>
<!-- List Type -->
<div v-else-if="inputField.type === fieldTypes.LIST">
<div v-for="(item, idx) in value[inputField.varName]" :key="idx">
<p>
{{ inputField.label }} {{ idx + 1 }}
<span>
<BaseButton class="ml-5" x-small delete @click="removeByIndex(value[inputField.varName], idx)" />
</span>
</p>
<v-divider class="mb-5 mx-2" />
<base-auto-form
v-model="value[inputField.varName][idx]"
:color="color"
:items="inputField.items"
@blur="emitBlur"
/>
</div>
<v-card-actions>
<v-spacer />
<BaseButton small @click="value[inputField.varName].push(getTemplate(inputField.items))"> New </BaseButton>
</v-card-actions>
</div>
</v-col>
</v-row>
</v-card>
</template>
<script>
import { ref } from "@nuxtjs/composition-api";
import { validators } from "@/composables/use-validators";
import { fieldTypes } from "@/composables/forms";
const BLUR_EVENT = "blur";
export default {
name: "BaseAutoForm",
props: {
value: {
default: null,
type: [Object, Array],
},
updateMode: {
default: false,
type: Boolean,
},
items: {
default: null,
type: Array,
},
width: {
type: [Number, String],
default: "max",
},
globalRules: {
default: null,
type: Array,
},
color: {
default: null,
type: String,
},
dark: {
default: false,
type: Boolean,
},
},
setup() {
const menu = ref({});
return {
menu,
fieldTypes,
validators,
};
},
computed: {
defaultRules() {
return this.rulesByKey(this.globalRules);
},
},
watch: {
items: {
immediate: true,
handler(val) {
// Initialize Value Object to Obtain all keys
if (!val) {
return;
}
for (let i = 0; i < val.length; i++) {
try {
if (this.value[val[i].varName]) {
continue;
}
} catch {}
if (val[i].type === "text" || val[i].type === "textarea") {
this.$set(this.value, val[i].varName, "");
} else if (val[i].type === "select") {
if (!val[i].options[0]) {
continue;
}
this.$set(this.value, val[i].varName, val[i].options[0].value);
} else if (val[i].type === "list") {
this.$set(this.value, val[i].varName, []);
} else if (val[i].type === "object") {
this.$set(this.value, val[i].varName, {});
} else if (val[i].type === "color") {
this.$set(this.value, val[i].varName, "");
this.$set(this.menu, val[i].varName, false);
}
}
},
},
},
methods: {
removeByIndex(list, index) {
// Removes the item at the index
list.splice(index, 1);
},
getTemplate(item) {
const obj = {};
item.forEach((field) => {
obj[field.varName] = "";
});
return obj;
},
rulesByKey(keys) {
const list = [];
if (keys === undefined) {
return list;
}
if (keys === null) {
return list;
}
if (keys === list) {
return list;
}
keys.forEach((key) => {
if (key in this.validators) {
list.push(this.validators[key]);
}
});
return list;
},
emitBlur() {
this.$emit(BLUR_EVENT, this.value);
},
},
};
</script>
<style lang="scss" scoped></style>

View file

@ -28,9 +28,11 @@
</v-btn>
</template>
<script>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
export default {
export default defineComponent({
name: "BaseButton",
props: {
// Types
@ -106,103 +108,99 @@ export default {
default: false,
},
},
setup() {
const api = useUserApi();
return { api };
},
data() {
return {
buttonOptions: {
create: {
text: "Create",
icon: this.$globals.icons.createAlt,
color: "success",
},
update: {
text: "Update",
icon: this.$globals.icons.edit,
color: "success",
},
save: {
text: "Save",
icon: this.$globals.icons.save,
color: "success",
},
edit: {
text: "Edit",
icon: this.$globals.icons.edit,
color: "info",
},
delete: {
text: "Delete",
icon: this.$globals.icons.delete,
color: "error",
},
cancel: {
text: "Cancel",
icon: this.$globals.icons.close,
color: "grey",
},
download: {
text: "Download",
icon: this.$globals.icons.download,
color: "info",
},
setup(props) {
const { $globals } = useContext();
const buttonOptions = {
create: {
text: "Create",
icon: $globals.icons.createAlt,
color: "success",
},
buttonStyles: {
defaults: {
text: false,
outlined: false,
},
secondary: {
text: false,
outlined: true,
},
minor: {
outlined: false,
text: true,
},
update: {
text: "Update",
icon: $globals.icons.edit,
color: "success",
},
save: {
text: "Save",
icon: $globals.icons.save,
color: "success",
},
edit: {
text: "Edit",
icon: $globals.icons.edit,
color: "info",
},
delete: {
text: "Delete",
icon: $globals.icons.delete,
color: "error",
},
cancel: {
text: "Cancel",
icon: $globals.icons.close,
color: "grey",
},
download: {
text: "Download",
icon: $globals.icons.download,
color: "info",
},
};
},
computed: {
btnAttrs() {
if (this.delete) {
return this.buttonOptions.delete;
} else if (this.update) {
return this.buttonOptions.update;
} else if (this.edit) {
return this.buttonOptions.edit;
} else if (this.cancel) {
this.setMinor();
return this.buttonOptions.cancel;
} else if (this.save) {
return this.buttonOptions.save;
} else if (this.download) {
return this.buttonOptions.download;
const btnAttrs = computed(() => {
if (props.delete) {
return buttonOptions.delete;
} else if (props.update) {
return buttonOptions.update;
} else if (props.edit) {
return buttonOptions.edit;
} else if (props.cancel) {
return buttonOptions.cancel;
} else if (props.save) {
return buttonOptions.save;
} else if (props.download) {
return buttonOptions.download;
}
return this.buttonOptions.create;
},
btnStyle() {
if (this.secondary) {
return this.buttonStyles.secondary;
} else if (this.minor) {
return this.buttonStyles.minor;
return buttonOptions.create;
});
const buttonStyles = {
defaults: {
text: false,
outlined: false,
},
secondary: {
text: false,
outlined: true,
},
minor: {
text: true,
outlined: false,
},
};
const btnStyle = computed(() => {
if (props.secondary) {
return buttonStyles.secondary;
} else if (props.minor || props.cancel) {
return buttonStyles.minor;
}
return this.buttonStyles.defaults;
},
return buttonStyles.defaults;
});
const api = useUserApi();
function downloadFile() {
api.utils.download(props.downloadUrl);
}
return {
btnAttrs,
btnStyle,
downloadFile,
};
},
methods: {
setMinor() {
this.buttonStyles.defaults = this.buttonStyles.minor;
},
setSecondary() {
this.buttonStyles.defaults = this.buttonStyles.secondary;
},
downloadFile() {
this.api.utils.download(this.downloadUrl);
},
},
};
});
</script>

View file

@ -23,8 +23,10 @@
</v-card>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
title: {
type: String,
@ -39,5 +41,5 @@ export default {
default: false,
},
},
};
});
</script>

View file

@ -1,70 +0,0 @@
<template>
<div>
<div class="text-center">
<h3>{{ buttonText }}</h3>
</div>
<v-text-field v-model="color" hide-details class="ma-0 pa-0" solo>
<template #append>
<v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false">
<template #activator="{ on }">
<div :style="swatchStyle" swatches-max-height="300" v-on="on" />
</template>
<v-card>
<v-card-text class="pa-0">
<v-color-picker v-model="color" flat mode="hexa" show-swatches />
</v-card-text>
</v-card>
</v-menu>
</template>
</v-text-field>
</div>
</template>
<script>
export default {
props: {
buttonText: {
type: String,
default: "Choose a color",
},
value: {
type: String,
default: "#ff0000",
},
},
data() {
return {
dialog: false,
swatches: false,
color: this.value || "#1976D2",
mask: "!#XXXXXXXX",
menu: false,
};
},
computed: {
swatchStyle() {
const { value, menu } = this;
return {
backgroundColor: value,
cursor: "pointer",
height: "30px",
width: "30px",
borderRadius: menu ? "50%" : "4px",
transition: "border-radius 200ms ease-in-out",
};
},
},
watch: {
color() {
this.updateColor();
},
},
methods: {
updateColor() {
this.$emit("input", this.color);
},
},
};
</script>
<style></style>

View file

@ -110,7 +110,7 @@ export default defineComponent({
},
},
setup(props, context) {
const dialog = computed<Boolean>({
const dialog = computed<boolean>({
get() {
return props.value;
},
@ -129,12 +129,9 @@ export default defineComponent({
};
},
computed: {
determineClose(): Boolean {
determineClose(): boolean {
return this.submitted && !this.loading && !this.keepOpen;
},
displayicon(): Boolean {
return this.icon || this.$globals.icons.user;
},
},
watch: {
determineClose() {
@ -181,4 +178,4 @@ export default defineComponent({
position: fixed;
top: 0;
}
</style>
</style>

View file

@ -2,8 +2,10 @@
<v-divider :width="width" :class="color" :style="`border-width: ${thickness} !important`" />
</template>
<script>
export default {
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
width: {
type: String,
@ -18,5 +20,5 @@ export default {
default: "accent",
},
},
};
});
</script>

View file

@ -17,13 +17,15 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
divider: {
type: Boolean,
default: false,
},
},
};
</script>
});
</script>

View file

@ -1,4 +1,4 @@
w<template>
<template>
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
<div class="d-flex grow flex-wrap">
<slot name="avatar">
@ -40,8 +40,10 @@ w<template>
</v-card>
</template>
<script>
export default {
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
name: "MaterialCard",
props: {
@ -70,22 +72,25 @@ export default {
default: "",
},
},
setup() {
const { $vuetify } = useContext();
computed: {
classes() {
const hasHeading = computed(() => false);
const hasAltHeading = computed(() => false);
const classes = computed(() => {
return {
"v-card--material--has-heading": this.hasHeading,
"mt-3": this.$vuetify.breakpoint.name === "xs" || this.$vuetify.breakpoint.name === "sm",
"v-card--material--has-heading": hasHeading,
"mt-3": $vuetify.breakpoint.name === "xs" || $vuetify.breakpoint.name === "sm",
};
},
hasHeading() {
return false;
},
hasAltHeading() {
return false;
},
});
return {
hasHeading,
hasAltHeading,
classes,
};
},
};
});
</script>
<style lang="sass">

View file

@ -8,10 +8,12 @@
></VJsoneditor>
</template>
<script>
<script lang="ts">
// @ts-ignore
import VJsoneditor from "v-jsoneditor";
import { defineComponent } from "@nuxtjs/composition-api";
export default {
export default defineComponent({
components: { VJsoneditor },
props: {
value: {
@ -23,6 +25,6 @@ export default {
default: () => ({}),
},
},
};
});
</script>

View file

@ -47,7 +47,7 @@ export default defineComponent({
{ text: "Delete", value: "actions" },
];
function handleRowClick(item: any) {
function handleRowClick(item: ReportSummary) {
router.push("/user/group/data/reports/" + item.id);
}
@ -70,4 +70,4 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
</style>
</style>