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

View File

@ -11,96 +11,81 @@
</nav> </nav>
</template> </template>
<script> <script setup>
export default { import { computed } from "vue";
props: {
pagedItemName: {
required: false,
type: String,
default: ''
},
currentPage: { const emit = defineEmits(["changePage"]);
required: true,
type: Number
},
totalPages: { const props = defineProps({
required: true, pagedItemName: {
type: Number required: false,
}, type: String,
default: ''
},
pageWindow: { currentPage: {
required: false, required: true,
type: Number, type: Number
default: 4 },
},
pageOuterWindow: { totalPages: {
required: false, required: true,
type: Number, type: Number
default: 1 },
},
showWithSinglePage: { pageWindow: {
required: false, required: false,
type: Boolean, type: Number,
default: false default: 4
} },
},
computed: { pageOuterWindow: {
pageItems() { required: false,
const items = new Set(); type: Number,
default: 1
},
for (let x = 0; x < this.pageOuterWindow; x++) { showWithSinglePage: {
items.add(x + 1); required: false,
items.add(this.totalPages - x); type: Boolean,
} default: false
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);
}
}
} }
});
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> </script>
<style lang="scss" scoped>
ul.pagination {
}
</style>

View File

@ -9,97 +9,85 @@
</span> </span>
</template> </template>
<script> <script setup>
import { computed, ref, useTemplateRef } from "vue";
export default { const emit = defineEmits(["update:modelValue"]);
props: {
starCount: {
required: false,
type: Number,
default: 5
},
readonly: { const props = defineProps({
required: false, starCount: {
type: Boolean, required: false,
default: false type: Number,
}, default: 5
},
step: { readonly: {
required: false, required: false,
type: Number, type: Boolean,
default: 0.5 default: false
}, },
modelValue: { step: {
required: false, required: false,
type: Number, type: Number,
default: 0 default: 0.5
} },
},
data() { modelValue: {
return { required: false,
temporaryValue: null type: Number,
}; default: 0
},
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: {
}
} }
});
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> </script>

View File

@ -6,61 +6,45 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref } from "vue";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
export default { const emit = defineEmits(["update:modelValue"]);
props: {
placeholder: { const props = defineProps({
required: false, placeholder: {
type: String, required: false,
default: "" 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: { function userUpdateText(newText) {
required: false, if (text.value !== newText) {
type: String, text.value = newText;
default: "" triggerInput();
} }
}, }
data() { function propUpdateText(newText) {
return { if (text.value === null && text.value !== newText) {
text: null text.value = newText;
};
},
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
}
);
} }
} }

View File

@ -7,79 +7,66 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { import {computed, nextTick, ref, useTemplateRef} from "vue";
props: {
modelValue: {
required: true,
type: Array
}
},
data() { const emit = defineEmits(["update:modelValue"]);
return {
hasFocus: false
};
},
computed: { const props = defineProps({
tagText() { modelValue: {
return this.modelValue.join(" "); required: true,
} type: Array
}, }
});
watch: { const hasFocus = ref(false);
}, const tagText = computed(() => props.modelValue.join(" "));
const inputElement = useTemplateRef("input");
methods: { function inputHandler(el) {
inputHandler(el) { let str = el.target.value;
let str = el.target.value; checkInput(str);
this.checkInput(str); nextTick(() => {
this.$nextTick(() => { el.target.value = str;
el.target.value = str; });
}); }
},
checkInput(str) { function checkInput(str) {
if (this.hasFocus) { if (hasFocus.value) {
const m = str.match(/\S\s+\S*$/); const m = str.match(/\S\s+\S*$/);
if (m !== null) { if (m !== null) {
str = str.substring(0, m.index + 1); str = str.substring(0, m.index + 1);
} else { } else {
str = ""; 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;
}
} }
} }
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> </script>

View File

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

View File

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

View File

@ -144,100 +144,82 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed } from "vue";
import api from "../lib/Api"; import api from "../lib/Api";
import { mapState } from "pinia"; import { mapState } from "pinia";
import { useNutrientStore } from "../stores/nutrient"; import { useNutrientStore } from "../stores/nutrient";
import { useLoadResource } from "../lib/useLoadResource";
export default { const nutrientStore = useNutrientStore();
props: { const nutrients = computed(() => nutrientStore.nutrientList);
food: { const { loadResource } = useLoadResource();
required: true,
type: Object const props = defineProps({
}, food: {
validationErrors: { required: true,
required: false, type: Object
type: Object,
default: {}
},
action: {
required: false,
type: String,
default: "Editing"
}
}, },
validationErrors: {
data() { required: false,
return { type: Object,
default: {}
};
}, },
action: {
computed: { required: false,
...mapState(useNutrientStore, { type: String,
nutrients: 'nutrientList' default: "Editing"
}),
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: {
} }
});
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> </script>

View File

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

View File

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

View File

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

View File

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

View File

@ -206,67 +206,47 @@ _underline_
</div> </div>
</template> </template>
<script> <script setup>
import { computed, ref, watch } from "vue";
//import autosize from "autosize"; //import autosize from "autosize";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import api from "../lib/Api"; import api from "../lib/Api";
import RecipeEditIngredientEditor from "./RecipeEditIngredientEditor"; import RecipeEditIngredientEditor from "./RecipeEditIngredientEditor";
export default { const props = defineProps({
props: { recipe: {
recipe: { required: true,
required: true, type: Object
type: Object
},
forLogging: {
required: false,
type: Boolean,
default: false
}
}, },
forLogging: {
data() { required: false,
return { type: Boolean,
stepPreviewCache: null, default: false
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
} }
} });
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> </script>

View File

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

View File

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

View File

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

View File

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