Continue converting to composition api

This commit is contained in:
Dan Elbert 2024-10-01 09:32:09 -05:00
parent a071e6b21e
commit 0d35f50dbf
17 changed files with 657 additions and 803 deletions

View File

@ -4,7 +4,7 @@
<script>
import { computed, nextTick, useTemplateRef, watch } from "vue";
import { computed, nextTick, onMounted, onUpdated, useTemplateRef } from "vue";
import Caret from "../iconic/svg/smart/caret";
import Check from "../iconic/svg/smart/check";
@ -130,14 +130,18 @@
svgElement.value.update();
}
watch(
() => props.icon,
() => {
ensureSvgApi(svgName.value, svgData.value.scriptBlocks);
nextTick(() => setupSvgApi(svgName.value));
},
{ immediate: true }
);
function updateScripts() {
ensureSvgApi(svgName.value, svgData.value.scriptBlocks);
setupSvgApi(svgName.value);
}
onMounted(() => {
updateScripts();
});
onUpdated(() => {
updateScripts();
});
return {
svgData,

View File

@ -11,96 +11,81 @@
</nav>
</template>
<script>
<script setup>
export default {
props: {
pagedItemName: {
required: false,
type: String,
default: ''
},
import { computed } from "vue";
currentPage: {
required: true,
type: Number
},
const emit = defineEmits(["changePage"]);
totalPages: {
required: true,
type: Number
},
const props = defineProps({
pagedItemName: {
required: false,
type: String,
default: ''
},
pageWindow: {
required: false,
type: Number,
default: 4
},
currentPage: {
required: true,
type: Number
},
pageOuterWindow: {
required: false,
type: Number,
default: 1
},
totalPages: {
required: true,
type: Number
},
showWithSinglePage: {
required: false,
type: Boolean,
default: false
}
},
pageWindow: {
required: false,
type: Number,
default: 4
},
computed: {
pageItems() {
const items = new Set();
pageOuterWindow: {
required: false,
type: Number,
default: 1
},
for (let x = 0; x < this.pageOuterWindow; x++) {
items.add(x + 1);
items.add(this.totalPages - x);
}
const start = this.currentPage - Math.ceil(this.pageWindow / 2);
const end = this.currentPage + Math.floor(this.pageWindow / 2);
for (let x = start; x <= end; x++) {
items.add(x);
}
let emptySpace = -1;
const finalList = [];
[...items.values()].filter(p => p > 0 && p <= this.totalPages).sort((a, b) => a - b).forEach((p, idx, list) => {
finalList.push(p);
if (list[idx + 1] && list[idx + 1] !== p + 1) {
finalList.push(emptySpace--);
}
});
return finalList;
},
isLastPage() {
return this.currentPage === this.totalPages;
},
isFirstPage() {
return this.currentPage === 1;
}
},
methods: {
changePage(idx) {
this.$emit("changePage", idx);
}
}
showWithSinglePage: {
required: false,
type: Boolean,
default: false
}
});
const pageItems = computed(() => {
const items = new Set();
for (let x = 0; x < props.pageOuterWindow; x++) {
items.add(x + 1);
items.add(props.totalPages - x);
}
const start = props.currentPage - Math.ceil(props.pageWindow / 2);
const end = props.currentPage + Math.floor(props.pageWindow / 2);
for (let x = start; x <= end; x++) {
items.add(x);
}
let emptySpace = -1;
const finalList = [];
[...items.values()].filter(p => p > 0 && p <= props.totalPages).sort((a, b) => a - b).forEach((p, idx, list) => {
finalList.push(p);
if (list[idx + 1] && list[idx + 1] !== p + 1) {
finalList.push(emptySpace--);
}
});
return finalList;
});
const isLastPage = computed(() => props.currentPage === props.totalPages);
const isFirstPage = computed(() => props.currentPage === 1);
function changePage(idx) {
emit("changePage", idx);
}
</script>
<style lang="scss" scoped>
ul.pagination {
}
</style>

View File

@ -9,97 +9,85 @@
</span>
</template>
<script>
<script setup>
import { computed, ref, useTemplateRef } from "vue";
export default {
props: {
starCount: {
required: false,
type: Number,
default: 5
},
const emit = defineEmits(["update:modelValue"]);
readonly: {
required: false,
type: Boolean,
default: false
},
const props = defineProps({
starCount: {
required: false,
type: Number,
default: 5
},
step: {
required: false,
type: Number,
default: 0.5
},
readonly: {
required: false,
type: Boolean,
default: false
},
modelValue: {
required: false,
type: Number,
default: 0
}
},
step: {
required: false,
type: Number,
default: 0.5
},
data() {
return {
temporaryValue: null
};
},
computed: {
ratingPercent() {
return ((this.modelValue || 0) / this.starCount) * 100.0;
},
temporaryPercent() {
if (this.temporaryValue !== null) {
return (this.temporaryValue / this.starCount) * 100.0;
} else {
return null;
}
},
filledStyle() {
const width = this.temporaryPercent || this.ratingPercent;
return {
width: width + "%"
};
}
},
methods: {
handleClick(evt) {
if (this.temporaryValue !== null) {
this.$emit("update:modelValue", this.temporaryValue);
}
},
handleMousemove(evt) {
if (this.readonly) {
return;
}
const wrapperBox = this.$refs.wrapper.getBoundingClientRect();
const wrapperWidth = wrapperBox.right - wrapperBox.left;
const mousePosition = evt.clientX;
if (mousePosition > wrapperBox.left && mousePosition < wrapperBox.right) {
const filledRatio = ((mousePosition - wrapperBox.left) / wrapperWidth);
const totalSteps = this.starCount / this.step;
const filledSteps = Math.round(totalSteps * filledRatio);
this.temporaryValue = filledSteps * this.step;
}
},
handleMouseleave(evt) {
this.temporaryValue = null;
}
},
components: {
}
modelValue: {
required: false,
type: Number,
default: 0
}
});
const temporaryValue = ref(null);
const ratingPercent = computed(() => ((props.modelValue || 0) / props.starCount) * 100.0);
const wrapperEl = useTemplateRef("wrapper");
const temporaryPercent = computed(() => {
if (temporaryValue.value !== null) {
return (temporaryValue.value / props.starCount) * 100.0;
} else {
return null;
}
});
const filledStyle = computed(() => {
const width = temporaryPercent.value || ratingPercent.value;
return {
width: width + "%"
};
});
function handleClick(evt) {
if (temporaryValue.value !== null) {
emit("update:modelValue", temporaryValue.value);
}
}
function handleMousemove(evt) {
if (props.readonly) {
return;
}
const wrapperBox = wrapperEl.value.getBoundingClientRect();
const wrapperWidth = wrapperBox.right - wrapperBox.left;
const mousePosition = evt.clientX;
if (mousePosition > wrapperBox.left && mousePosition < wrapperBox.right) {
const filledRatio = ((mousePosition - wrapperBox.left) / wrapperWidth);
const totalSteps = props.starCount / props.step;
const filledSteps = Math.round(totalSteps * filledRatio);
temporaryValue.value = filledSteps * props.step;
}
}
function handleMouseleave(evt) {
temporaryValue.value = null;
}
</script>

View File

@ -6,61 +6,45 @@
</div>
</template>
<script>
<script setup>
import { ref } from "vue";
import debounce from "lodash/debounce";
export default {
props: {
placeholder: {
required: false,
type: String,
default: ""
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
placeholder: {
required: false,
type: String,
default: ""
},
modelValue: {
required: false,
type: String,
default: ""
}
});
const text = ref(null);
const triggerInput = debounce(function() {
emit("update:modelValue", text.value);
},
250,
{ leading: false, trailing: true })
modelValue: {
required: false,
type: String,
default: ""
}
},
function userUpdateText(newText) {
if (text.value !== newText) {
text.value = newText;
triggerInput();
}
}
data() {
return {
text: null
};
},
computed: {
},
methods: {
triggerInput: debounce(function() {
this.$emit("update:modelValue", this.text);
}, 250, {leading: false, trailing: true}),
userUpdateText(text) {
if (this.text !== text) {
this.text = text;
this.triggerInput();
}
},
propUpdateText(text) {
if (this.text === null && this.text !== text) {
this.text = text;
}
}
},
created() {
this.$watch("modelValue",
val => this.propUpdateText(val),
{
immediate: true
}
);
function propUpdateText(newText) {
if (text.value === null && text.value !== newText) {
text.value = newText;
}
}

View File

@ -7,79 +7,66 @@
</div>
</template>
<script>
<script setup>
export default {
props: {
modelValue: {
required: true,
type: Array
}
},
import {computed, nextTick, ref, useTemplateRef} from "vue";
data() {
return {
hasFocus: false
};
},
const emit = defineEmits(["update:modelValue"]);
computed: {
tagText() {
return this.modelValue.join(" ");
}
},
const props = defineProps({
modelValue: {
required: true,
type: Array
}
});
watch: {
},
const hasFocus = ref(false);
const tagText = computed(() => props.modelValue.join(" "));
const inputElement = useTemplateRef("input");
methods: {
inputHandler(el) {
let str = el.target.value;
this.checkInput(str);
this.$nextTick(() => {
el.target.value = str;
});
},
function inputHandler(el) {
let str = el.target.value;
checkInput(str);
nextTick(() => {
el.target.value = str;
});
}
checkInput(str) {
if (this.hasFocus) {
const m = str.match(/\S\s+\S*$/);
function checkInput(str) {
if (hasFocus.value) {
const m = str.match(/\S\s+\S*$/);
if (m !== null) {
str = str.substring(0, m.index + 1);
} else {
str = "";
}
}
const newTags = [...new Set(str.toString().split(/\s+/).filter(t => t.length > 0))];
if (!this.arraysEqual(newTags, this.modelValue)) {
this.$emit("update:modelValue", newTags);
}
},
getFocus() {
this.hasFocus = true;
},
loseFocus() {
this.hasFocus = false;
this.checkInput(this.$refs.input.value);
},
arraysEqual(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
for(let i = arr1.length; i--;) {
if(arr1[i] !== arr2[i])
return false;
}
return true;
}
if (m !== null) {
str = str.substring(0, m.index + 1);
} else {
str = "";
}
}
const newTags = [...new Set(str.toString().split(/\s+/).filter(t => t.length > 0))];
if (!arraysEqual(newTags, props.modelValue)) {
emit("update:modelValue", newTags);
}
}
function getFocus() {
hasFocus.value = true;
}
function loseFocus() {
hasFocus.value = false;
checkInput(inputElement.value.value);
}
function arraysEqual(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
for(let i = arr1.length; i--;) {
if(arr1[i] !== arr2[i])
return false;
}
return true;
}
</script>

View File

@ -2,8 +2,8 @@
<div class="field">
<label v-if="label.length" class="label is-small-mobile">{{ label }}</label>
<div :class="controlClasses">
<textarea v-if="isTextarea" :class="inputClasses" :value="modelValue" @input="input" :disabled="disabled"></textarea>
<input v-else :type="type" :class="inputClasses" :value="modelValue" @input="input" :disabled="disabled">
<textarea v-if="isTextarea" :class="inputClasses" v-model="model" :disabled="disabled"></textarea>
<input v-else :type="type" :class="inputClasses" v-model="model" :disabled="disabled">
<app-icon class="is-right" icon="warning" v-if="validationError !== null"></app-icon>
</div>
<p v-if="helpMessage !== null" :class="helpClasses">
@ -12,81 +12,67 @@
</div>
</template>
<script>
<script setup>
export default {
props: {
label: {
required: false,
type: String,
default: ""
},
modelValue: {
required: false,
type: [String, Number],
default: ""
},
type: {
required: false,
type: String,
default: "text"
},
disabled: {
type: Boolean,
default: false
},
validationError: {
required: false,
type: String,
default: null
}
},
import { computed } from "vue";
computed: {
isTextarea() {
return this.type === "textarea";
},
controlClasses() {
return [
"control",
{
"has-icons-right": this.validationError !== null
}
]
},
inputClasses() {
return [
"is-small-mobile",
{
"textarea": this.isTextarea,
"input": !this.isTextarea,
"is-danger": this.validationError !== null
}
]
},
helpMessage() {
return this.validationError;
},
helpClasses() {
return [
"help",
{
"is-danger": this.validationError !== null
}
];
}
},
methods: {
input(evt) {
this.$emit("update:modelValue", evt.target.value);
}
}
const props = defineProps({
label: {
required: false,
type: String,
default: ""
},
modelValue: {
required: false,
type: [String, Number],
default: ""
},
type: {
required: false,
type: String,
default: "text"
},
disabled: {
type: Boolean,
default: false
},
validationError: {
required: false,
type: String,
default: null
}
});
const model = defineModel({
type: [String, Number],
default: ""
});
const isTextarea = computed(() => props.type === "textarea");
const controlClasses = computed(() => [
"control",
{
"has-icons-right": props.validationError !== null
}
]);
const inputClasses = computed(() =>[
"is-small-mobile",
{
"textarea": isTextarea.value,
"input": !isTextarea.value,
"is-danger": props.validationError !== null
}
]);
const helpMessage = computed(() => props.validationError);
const helpClasses = computed(() => [
"help",
{
"is-danger": props.validationError !== null
}
]);
</script>

View File

@ -6,16 +6,14 @@
</div>
</template>
<script>
<script setup>
export default {
props: {
errors: {
required: false,
type: Object,
default: {}
}
}
const props = defineProps({
errors: {
required: false,
type: Object,
default: {}
}
});
</script>

View File

@ -144,100 +144,82 @@
</div>
</template>
<script>
<script setup>
import { computed } from "vue";
import api from "../lib/Api";
import { mapState } from "pinia";
import { useNutrientStore } from "../stores/nutrient";
import { useLoadResource } from "../lib/useLoadResource";
export default {
props: {
food: {
required: true,
type: Object
},
validationErrors: {
required: false,
type: Object,
default: {}
},
action: {
required: false,
type: String,
default: "Editing"
}
const nutrientStore = useNutrientStore();
const nutrients = computed(() => nutrientStore.nutrientList);
const { loadResource } = useLoadResource();
const props = defineProps({
food: {
required: true,
type: Object
},
data() {
return {
};
validationErrors: {
required: false,
type: Object,
default: {}
},
computed: {
...mapState(useNutrientStore, {
nutrients: 'nutrientList'
}),
visibleFoodUnits() {
return this.food.food_units.filter(iu => iu._destroy !== true);
},
hasNdbn() {
return this.food.ndbn !== null;
}
},
methods: {
addUnit() {
this.food.food_units.push({
id: null,
name: null,
gram_weight: null
});
},
removeUnit(unit) {
if (unit.id) {
unit._destroy = true;
} else {
const idx = this.food.food_units.findIndex(i => i === unit);
this.food.food_units.splice(idx, 1);
}
},
removeNdbn() {
this.food.ndbn = null;
this.food.usda_food_name = null;
this.food.ndbn_units = [];
},
updateSearchItems(text) {
return api.getUsdaFoodSearch(text)
.then(data => data.map(f => {
return {
name: f.name,
ndbn: f.ndbn,
description: ["#", f.ndbn, ", Cal:", f.kcal, ", Carbs:", f.carbohydrates, ", Fat:", f.lipid, ", Protein:", f.protein].join("")
}
}));
},
searchItemSelected(food) {
this.food.ndbn = food.ndbn;
this.food.usda_food_name = food.name;
this.food.ndbn_units = [];
this.loadResource(
api.postIngredientSelectNdbn(this.food)
.then(i => Object.assign(this.food, i))
);
},
},
components: {
action: {
required: false,
type: String,
default: "Editing"
}
});
const visibleFoodUnits = computed(() => props.food.food_units.filter(iu => iu._destroy !== true));
const hasNdbn = computed(() => props.food.ndbn !== null);
function addUnit() {
props.food.food_units.push({
id: null,
name: null,
gram_weight: null
});
}
function removeUnit(unit) {
if (unit.id) {
unit._destroy = true;
} else {
const idx = props.food.food_units.findIndex(i => i === unit);
props.food.food_units.splice(idx, 1);
}
}
function removeNdbn() {
props.food.ndbn = null;
props.food.usda_food_name = null;
props.food.ndbn_units = [];
}
function updateSearchItems(text) {
return api.getUsdaFoodSearch(text)
.then(data => data.map(f => {
return {
name: f.name,
ndbn: f.ndbn,
description: ["#", f.ndbn, ", Cal:", f.kcal, ", Carbs:", f.carbohydrates, ", Fat:", f.lipid, ", Protein:", f.protein].join("")
}
}));
}
function searchItemSelected(food) {
props.food.ndbn = food.ndbn;
props.food.usda_food_name = food.name;
props.food.ndbn_units = [];
loadResource(
api.postIngredientSelectNdbn(props.food)
.then(i => Object.assign(props.food, i))
);
}
</script>

View File

@ -53,25 +53,20 @@
</div>
</template>
<script>
<script setup>
import { mapState } from "pinia";
import { computed } from "vue";
import { useNutrientStore } from "../stores/nutrient";
export default {
props: {
food: {
required: true,
type: Object
}
},
computed: {
...mapState(useNutrientStore, {
nutrients: 'nutrientList'
}),
const props = defineProps({
food: {
required: true,
type: Object
}
}
});
const nutrientStore = useNutrientStore();
const nutrients = computed(() => nutrientStore.nutrientList);
</script>

View File

@ -31,27 +31,21 @@
</template>
<script>
<script setup>
import RecipeEdit from "./RecipeEdit";
export default {
props: {
log: {
required: true,
type: Object
},
validationErrors: {
required: false,
type: Object,
default: {}
},
const props = defineProps({
log: {
required: true,
type: Object
},
components: {
RecipeEdit
}
}
validationErrors: {
required: false,
type: Object,
default: {}
},
});
</script>

View File

@ -44,22 +44,16 @@
</div>
</template>
<script>
<script setup>
import RecipeShow from "./RecipeShow";
export default {
props: {
log: {
required: true,
type: Object
}
},
components: {
RecipeShow
const props = defineProps({
log: {
required: true,
type: Object
}
}
});
</script>

View File

@ -19,37 +19,27 @@
</div>
</template>
<script>
<script setup>
export default {
props: {
note: {
required: true,
type: Object
}
},
import { computed } from "vue";
data() {
return {
};
},
computed: {
canSave() {
return this.note && this.note.content && this.note.content.length;
}
},
methods: {
save() {
this.$emit("save", this.note);
},
cancel() {
this.$emit("cancel");
}
}
const emit = defineEmits(["save", "cancel"]);
const props = defineProps({
note: {
required: true,
type: Object
}
});
const canSave = computed(() => props.note?.content?.length);
function save() {
emit("save", props.note);
}
function cancel() {
emit("cancel");
}
</script>

View File

@ -206,67 +206,47 @@ _underline_
</div>
</template>
<script>
<script setup>
import { computed, ref, watch } from "vue";
//import autosize from "autosize";
import debounce from "lodash/debounce";
import api from "../lib/Api";
import RecipeEditIngredientEditor from "./RecipeEditIngredientEditor";
export default {
props: {
recipe: {
required: true,
type: Object
},
forLogging: {
required: false,
type: Boolean,
default: false
}
const props = defineProps({
recipe: {
required: true,
type: Object
},
data() {
return {
stepPreviewCache: null,
isDescriptionHelpOpen: false
};
},
computed: {
stepPreview() {
if (this.stepPreviewCache === null) {
return this.recipe.rendered_steps;
} else {
return this.stepPreviewCache;
}
}
},
methods: {
updatePreview: debounce(function() {
api.postPreviewSteps(this.recipe.step_text)
.then(data => this.stepPreviewCache = data.rendered_steps)
.catch(err => this.stepPreviewCache = "?? Error ??");
}, 750)
},
watch: {
'recipe.step_text': function() {
this.updatePreview();
}
},
mounted() {
//autosize(this.$refs.step_text_area);
},
components: {
RecipeEditIngredientEditor
forLogging: {
required: false,
type: Boolean,
default: false
}
}
});
const stepPreviewCache = ref(null);
const isDescriptionHelpOpen = ref(false);
const stepPreview = computed(() => {
if (stepPreviewCache.value === null) {
return props.recipe.rendered_steps;
} else {
return stepPreviewCache.value;
}
});
const updatePreview = debounce(function() {
api.postPreviewSteps(props.recipe.step_text)
.then(data => stepPreviewCache.value = data.rendered_steps)
.catch(err => stepPreviewCache.value = "?? Error ??");
}, 750);
watch(
() => props.recipe.step_text,
() => updatePreview()
);
</script>

View File

@ -2,7 +2,7 @@
<div>
<h1 class="title">Recipes</h1>
<router-link v-if="isLoggedIn" :to="{name: 'new_recipe'}" class="button is-primary">Create Recipe</router-link>
<router-link v-if="appConfig.isLoggedIn" :to="{name: 'new_recipe'}" class="button is-primary">Create Recipe</router-link>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="recipe" @changePage="changePage"></app-pager>
@ -25,10 +25,10 @@
</tr>
<tr>
<td>
<app-search-text placeholder="search names" :value="search.name" @input="setSearchName($event)"></app-search-text>
<app-search-text placeholder="search names" :value="search.name" @update:modelValue="setSearchName($event)"></app-search-text>
</td>
<td>
<app-search-text placeholder="search tags" :value="search.tags" @input="setSearchTags($event)"></app-search-text>
<app-search-text placeholder="search tags" :value="search.tags" @update:modelValue="setSearchTags($event)"></app-search-text>
</td>
<td colspan="5"></td>
</tr>
@ -49,10 +49,12 @@
<td class="recipe-time">{{ formatRecipeTime(r.total_time, r.active_time) }}</td>
<td><app-date-time :date-time="r.created_at" :show-time="false"></app-date-time></td>
<td>
<app-dropdown hover v-if="isLoggedIn" class="is-right">
<button slot="button" class="button is-small">
<app-icon icon="menu"></app-icon>
</button>
<app-dropdown hover v-if="appConfig.isLoggedIn" class="is-right">
<template #button>
<button class="button is-small">
<app-icon icon="menu"></app-icon>
</button>
</template>
<div class="dropdown-item">
<router-link :to="{name: 'new_log', params: { recipeId: r.id } }" class="button is-primary is-fullwidth">
@ -88,232 +90,212 @@
</div>
</template>
<script>
<script setup>
import { computed, reactive, ref, watch } from "vue";
import { useRouter } from 'vue-router'
import api from "../lib/Api";
import { mapWritableState, mapState } from "pinia";
import AppLoading from "./AppLoading";
import { useAppConfigStore } from "../stores/appConfig";
import { useMediaQueryStore } from "../stores/mediaQuery";
import { useLoadResource } from "../lib/useLoadResource";
export default {
props: {
searchQuery: {
type: Object,
required: false,
default: {}
}
},
const appConfig = useAppConfigStore();
const mediaQueries = useMediaQueryStore();
const { loadResource, localLoading } = useLoadResource();
const router = useRouter();
data() {
return {
recipeData: null,
recipeForDeletion: null
};
},
const props = defineProps({
searchQuery: {
type: Object,
required: false,
default: {}
}
});
computed: {
...mapState(useMediaQueryStore, { isTouch: store => store.touch }),
...mapWritableState(useAppConfigStore, [
"initialLoad"
]),
const tableHeader = [
{name: 'name', label: 'Name', sort: true},
{name: 'tags', label: 'Tags', sort: false},
{name: 'rating', label: 'Rating', sort: true},
{name: 'yields', label: 'Yields', sort: false},
{name: 'total_time', label: 'Time', sort: true},
{name: 'created_at', label: 'Created', sort: true}
];
search() {
return {
name: this.searchQuery.name || null,
tags: this.searchQuery.tags || null,
column: this.searchQuery.column || "created_at",
direction: this.searchQuery.direction || "desc",
page: this.searchQuery.page || 1,
per: this.searchQuery.per || 25
}
},
const recipeData = ref(null);
const recipeForDeletion = ref(null);
const isTouch = computed(() => mediaQueries.touch);
recipes() {
if (this.recipeData) {
return this.recipeData.recipes;
} else {
return [];
}
},
const search = computed(() => ({
name: props.searchQuery.name || null,
tags: props.searchQuery.tags || null,
column: props.searchQuery.column || "created_at",
direction: props.searchQuery.direction || "desc",
page: props.searchQuery.page || 1,
per: props.searchQuery.per || 25
}));
tableHeader() {
return [
{name: 'name', label: 'Name', sort: true},
{name: 'tags', label: 'Tags', sort: false},
{name: 'rating', label: 'Rating', sort: true},
{name: 'yields', label: 'Yields', sort: false},
{name: 'total_time', label: 'Time', sort: true},
{name: 'created_at', label: 'Created', sort: true}
]
},
const recipes = computed(() => {
if (recipeData.value) {
return recipeData.value.recipes;
} else {
return [];
}
});
totalPages() {
if (this.recipeData) {
return this.recipeData.total_pages;
}
return 0;
},
const totalPages = computed(() => {
if (recipeData.value) {
return recipeData.value.total_pages;
}
return 0;
});
currentPage() {
if (this.recipeData) {
return this.recipeData.current_page;
}
return 0;
},
const currentPage = computed(() => {
if (recipeData.value) {
return recipeData.value.current_page;
}
return 0;
});
showConfirmRecipeDelete() {
return this.recipeForDeletion !== null;
},
const showConfirmRecipeDelete = computed(() => recipeForDeletion.value !== null);
confirmRecipeDeleteMessage() {
if (this.showConfirmRecipeDelete) {
return `Are you sure you want to delete ${this.recipeForDeletion.name}?`;
} else {
return "??";
}
}
},
const confirmRecipeDeleteMessage = computed(() => {
if (showConfirmRecipeDelete.value) {
return `Are you sure you want to delete ${recipeForDeletion.value.name}?`;
} else {
return "??";
}
});
methods: {
buildQueryParams() {
return {
name: this.searchQuery.name,
tags: this.searchQuery.tags,
column: this.searchQuery.column,
direction: this.searchQuery.direction,
page: this.searchQuery.page,
per: this.searchQuery.per
}
},
watch(search, () => {
getList().then(() => appConfig.initialLoad = true);
}, {
deep: true,
immediate: true
});
redirectToParams(params) {
const rParams = {};
function getList() {
return loadResource(
api.getRecipeList(search.value.page, search.value.per, search.value.column, search.value.direction, search.value.name, search.value.tags, data => recipeData.value = data)
);
}
if (params.name) {
rParams.name = params.name;
}
if (params.tags) {
rParams.tags = params.tags;
}
if (params.column) {
rParams.column = params.column;
}
if (params.direction) {
rParams.direction = params.direction;
}
if (params.page) {
rParams.page = params.page;
}
if (params.per) {
rParams.per = params.per;
}
this.$router.push({name: 'recipeList', query: rParams});
},
changePage(idx) {
const p = this.buildQueryParams();
p.page = idx;
this.redirectToParams(p);
},
setSort(col) {
const p = this.buildQueryParams();
if (p.column === col) {
p.direction = p.direction === "desc" ? "asc" : "desc";
} else {
p.column = col;
p.direction = "asc";
}
this.redirectToParams(p);
},
setSearchName(name) {
const p = this.buildQueryParams();
if (name !== p.name) {
p.name = name;
p.page = null;
this.redirectToParams(p);
}
},
setSearchTags(tags) {
const p = this.buildQueryParams();
if (tags !== p.tags) {
p.tags = tags;
p.page = null;
this.redirectToParams(p);
}
},
deleteRecipe(recipe) {
this.recipeForDeletion = recipe;
},
recipeDeleteConfirm() {
if (this.recipeForDeletion !== null) {
this.loadResource(
api.deleteRecipe(this.recipeForDeletion.id).then(() => {
this.recipeForDeletion = null;
return this.getList();
})
);
}
},
recipeDeleteCancel() {
this.recipeForDeletion = null;
},
getList() {
return this.loadResource(
api.getRecipeList(this.search.page, this.search.per, this.search.column, this.search.direction, this.search.name, this.search.tags, data => this.recipeData = data)
);
},
formatRecipeTime(total, active) {
let str = "";
if (total && total > 0) {
str += total;
}
if (active && active > 0) {
if (str.length) {
str += " (" + active + ")";
} else {
str += active;
}
}
return str;
}
},
created() {
this.$watch("search",
() => {
this.getList().then(() => this.initialLoad = true);
},
{
deep: true,
immediate: true
}
);
},
components: {
AppLoading
function buildQueryParams() {
return {
name: props.searchQuery.name,
tags: props.searchQuery.tags,
column: props.searchQuery.column,
direction: props.searchQuery.direction,
page: props.searchQuery.page,
per: props.searchQuery.per
}
}
function redirectToParams(params) {
const rParams = {};
if (params.name) {
rParams.name = params.name;
}
if (params.tags) {
rParams.tags = params.tags;
}
if (params.column) {
rParams.column = params.column;
}
if (params.direction) {
rParams.direction = params.direction;
}
if (params.page) {
rParams.page = params.page;
}
if (params.per) {
rParams.per = params.per;
}
router.push({name: 'recipeList', query: rParams});
}
function changePage(idx) {
const p = buildQueryParams();
p.page = idx;
redirectToParams(p);
}
function setSort(col) {
const p = buildQueryParams();
if (p.column === col) {
p.direction = p.direction === "desc" ? "asc" : "desc";
} else {
p.column = col;
p.direction = "asc";
}
redirectToParams(p);
}
function setSearchName(name) {
const p = buildQueryParams();
if (name !== p.name) {
p.name = name;
p.page = null;
redirectToParams(p);
}
}
function setSearchTags(tags) {
const p = buildQueryParams();
if (tags !== p.tags) {
p.tags = tags;
p.page = null;
redirectToParams(p);
}
}
function deleteRecipe(recipe) {
recipeForDeletion.value = recipe;
}
function recipeDeleteConfirm() {
if (recipeForDeletion.value !== null) {
loadResource(
api.deleteRecipe(recipeForDeletion.value.id).then(() => {
recipeForDeletion.value = null;
return getList();
})
);
}
}
function recipeDeleteCancel() {
recipeForDeletion.value = null;
}
function formatRecipeTime(total, active) {
let str = "";
if (total && total > 0) {
str += total;
}
if (active && active > 0) {
if (str.length) {
str += " (" + active + ")";
} else {
str += active;
}
}
return str;
}
</script>
<style lang="scss" scoped>

View File

@ -19,12 +19,14 @@ require "rails/test_unit/railtie"
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
require_relative '../lib/unit_conversion'
module Parsley
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.2
config.autoload_lib(ignore: %w(assets tasks unit_conversion))
config.autoload_lib(ignore: %w(assets tasks unit_conversion unit_conversion.rb))
config.action_dispatch.cookies_same_site_protection = :lax
config.active_record.collection_cache_versioning = true

View File

@ -1,11 +1,11 @@
require 'unit_conversion/constants'
require 'unit_conversion/errors'
require 'unit_conversion/formatters'
require 'unit_conversion/parsed_number'
require 'unit_conversion/parsed_unit'
require 'unit_conversion/conversions'
require 'unit_conversion/unitwise_patch'
require 'unit_conversion/value_unit'
require_relative './unit_conversion/constants'
require_relative './unit_conversion/errors'
require_relative './unit_conversion/formatters'
require_relative './unit_conversion/parsed_number'
require_relative './unit_conversion/parsed_unit'
require_relative './unit_conversion/conversions'
require_relative './unit_conversion/unitwise_patch'
require_relative './unit_conversion/value_unit'
module UnitConversion
class << self

View File

@ -19,6 +19,9 @@ RSpec.describe Food, type: :model do
i.density = '5 mile/hour'
expect(i).not_to be_valid
i.density = '1 g/ml'
expect(i).to be_valid
end
end