зеркало из https://github.com/nextcloud/cookbook.git
style: Update prettier, eslint, stylelint config. Switch to single quotes and require semicolons in JS code.
Run stylelint and prettier to fix style Signed-off-by: Sebastian Fey <info@sebastianfey.de>
This commit is contained in:
Родитель
776b0f05ce
Коммит
af2a55becb
|
@ -18,6 +18,17 @@ rules:
|
|||
no-plusplus:
|
||||
- error
|
||||
- allowForLoopAfterthoughts: true
|
||||
quotes:
|
||||
- error
|
||||
- single
|
||||
- avoidEscape: true
|
||||
string-quotes:
|
||||
- error
|
||||
- single
|
||||
- avoidEscape: true
|
||||
selector-attribute-quotes:
|
||||
- error
|
||||
- always
|
||||
|
||||
root: true
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"semi": false,
|
||||
"tabWidth": 4
|
||||
"semi": true,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -62,7 +62,7 @@
|
|||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ t("cookbook", "Edit") }}
|
||||
{{ t('cookbook', 'Edit') }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
v-if="isEdit || isCreate"
|
||||
|
@ -78,7 +78,7 @@
|
|||
/>
|
||||
<CheckmarkIcon v-else :size="20" />
|
||||
</template>
|
||||
{{ t("cookbook", "Save") }}
|
||||
{{ t('cookbook', 'Save') }}
|
||||
</NcButton>
|
||||
<!-- This is clumsy design but the component cannot display just one input element on the breadcrumbs bar -->
|
||||
<NcActions
|
||||
|
@ -92,10 +92,10 @@
|
|||
:value="filterValue"
|
||||
@update:value="updateFilters"
|
||||
>
|
||||
{{ t("cookbook", "Filter") }}
|
||||
{{ t('cookbook', 'Filter') }}
|
||||
</NcActionInput>
|
||||
<NcActionInput icon="icon-search" @submit="search">
|
||||
{{ t("cookbook", "Search") }}
|
||||
{{ t('cookbook', 'Search') }}
|
||||
</NcActionInput>
|
||||
</NcActions>
|
||||
{{/* Overflow buttons (3-dot menu) */}}
|
||||
|
@ -115,7 +115,7 @@
|
|||
:aria-label="t('cookbook', 'Reload recipe')"
|
||||
@click="reloadRecipeEdit()"
|
||||
>
|
||||
{{ t("cookbook", "Reload recipe") }}
|
||||
{{ t('cookbook', 'Reload recipe') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton
|
||||
v-if="isEdit"
|
||||
|
@ -123,7 +123,7 @@
|
|||
:aria-label="t('cookbook', 'Abort editing')"
|
||||
@click="goToRecipe(store.state.recipe.id)"
|
||||
>
|
||||
{{ t("cookbook", "Abort editing") }}
|
||||
{{ t('cookbook', 'Abort editing') }}
|
||||
<template #icon>
|
||||
<NcLoadingIcon
|
||||
v-if="
|
||||
|
@ -146,7 +146,7 @@
|
|||
:aria-label="t('cookbook', 'Reload recipe')"
|
||||
@click="reloadRecipeView()"
|
||||
>
|
||||
{{ t("cookbook", "Reload recipe") }}
|
||||
{{ t('cookbook', 'Reload recipe') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton
|
||||
v-if="isRecipe"
|
||||
|
@ -155,7 +155,7 @@
|
|||
@click="printRecipe()"
|
||||
>
|
||||
<template #icon=""><printer-icon :size="20" /></template>
|
||||
{{ t("cookbook", "Print recipe") }}
|
||||
{{ t('cookbook', 'Print recipe') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton
|
||||
v-if="isRecipe"
|
||||
|
@ -164,7 +164,7 @@
|
|||
:aria-label="t('cookbook', 'Delete recipe')"
|
||||
@click="deleteRecipe()"
|
||||
>
|
||||
{{ t("cookbook", "Delete recipe") }}
|
||||
{{ t('cookbook', 'Delete recipe') }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</div>
|
||||
|
@ -179,38 +179,38 @@ export default {
|
|||
<script setup>
|
||||
import { computed, getCurrentInstance, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router/composables';
|
||||
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
||||
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions';
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
|
||||
// Cannot use `Button` else get `vue/no-reserved-component-names` eslint errors
|
||||
import NcButton from "@nextcloud/vue/dist/Components/NcButton";
|
||||
import NcActionInput from "@nextcloud/vue/dist/Components/NcActionInput";
|
||||
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton';
|
||||
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput';
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
|
||||
|
||||
import PencilIcon from "icons/Pencil.vue";
|
||||
import LoadingIcon from "icons/Loading.vue";
|
||||
import CheckmarkIcon from "icons/Check.vue";
|
||||
import PrinterIcon from "icons/Printer.vue";
|
||||
import EyeIcon from "icons/Eye.vue";
|
||||
import PencilIcon from 'icons/Pencil.vue';
|
||||
import LoadingIcon from 'icons/Loading.vue';
|
||||
import CheckmarkIcon from 'icons/Check.vue';
|
||||
import PrinterIcon from 'icons/Printer.vue';
|
||||
import EyeIcon from 'icons/Eye.vue';
|
||||
|
||||
import helpers from "cookbook/js/helper";
|
||||
import helpers from 'cookbook/js/helper';
|
||||
import {
|
||||
showSimpleAlertModal,
|
||||
showSimpleConfirmModal,
|
||||
} from "cookbook/js/modals";
|
||||
} from 'cookbook/js/modals';
|
||||
|
||||
import Location from "./Location.vue";
|
||||
import ModeIndicator from "./ModeIndicator.vue";
|
||||
import Location from './Location.vue';
|
||||
import ModeIndicator from './ModeIndicator.vue';
|
||||
import { useStore } from '../../store';
|
||||
import emitter from '../../bus';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const filterValue = ref("");
|
||||
const filterValue = ref('');
|
||||
|
||||
/** Computed values **/
|
||||
|
||||
const isCreate = computed(() => {
|
||||
return store.state.page === "create";
|
||||
return store.state.page === 'create';
|
||||
});
|
||||
const isEdit = computed(() => {
|
||||
// A recipe is being loaded
|
||||
|
@ -218,16 +218,14 @@ const isEdit = computed(() => {
|
|||
return false; // Do not show both at the same time
|
||||
}
|
||||
// Editing requires that a recipe was found
|
||||
return !!(
|
||||
store.state.page === "edit" && store.state.recipe
|
||||
);
|
||||
return !!(store.state.page === 'edit' && store.state.recipe);
|
||||
});
|
||||
const isIndex = computed(() => {
|
||||
// A recipe is being loaded
|
||||
if (!!store.state.loadingRecipe) {
|
||||
return false; // Do not show both at the same time
|
||||
}
|
||||
return store.state.page === "index";
|
||||
return store.state.page === 'index';
|
||||
});
|
||||
const isLoading = computed(() => {
|
||||
// The page is being loaded
|
||||
|
@ -239,38 +237,36 @@ const isRecipe = computed(() => {
|
|||
return false; // Do not show both at the same time
|
||||
}
|
||||
// Viewing recipe requires that one was found
|
||||
return !!(
|
||||
store.state.page === "recipe" && store.state.recipe
|
||||
);
|
||||
return !!(store.state.page === 'recipe' && store.state.recipe);
|
||||
});
|
||||
const isSearch = computed(() => {
|
||||
// A recipe is being loaded
|
||||
if (!!store.state.loadingRecipe) {
|
||||
return false // Do not show both at the same time
|
||||
return false; // Do not show both at the same time
|
||||
}
|
||||
return store.state.page === "search";
|
||||
return store.state.page === 'search';
|
||||
});
|
||||
const pageNotFound = computed(() => {
|
||||
return store.state.page === "notfound";
|
||||
return store.state.page === 'notfound';
|
||||
});
|
||||
const recipeNotFound = computed(() => {
|
||||
// Editing or viewing recipe was attempted, but no recipe was found
|
||||
return (
|
||||
["edit", "recipe"].indexOf(store.state.page) !== -1 &&
|
||||
['edit', 'recipe'].indexOf(store.state.page) !== -1 &&
|
||||
!store.state.recipe
|
||||
);
|
||||
});
|
||||
const searchTitle = computed(() => {
|
||||
if (route.name === "search-category") {
|
||||
return t("cookbook", "Category")
|
||||
if (route.name === 'search-category') {
|
||||
return t('cookbook', 'Category');
|
||||
}
|
||||
if (route.name === "search-name") {
|
||||
return t("cookbook", "Recipe name")
|
||||
if (route.name === 'search-name') {
|
||||
return t('cookbook', 'Recipe name');
|
||||
}
|
||||
if (route.name === "search-tags") {
|
||||
return t("cookbook", "Tags")
|
||||
if (route.name === 'search-tags') {
|
||||
return t('cookbook', 'Tags');
|
||||
}
|
||||
return t("cookbook", "Search for recipes");
|
||||
return t('cookbook', 'Search for recipes');
|
||||
});
|
||||
|
||||
// Methods
|
||||
|
@ -283,18 +279,18 @@ const deleteRecipe = async () => {
|
|||
t("cookbook", "Are you sure you want to delete this recipe?"),
|
||||
))
|
||||
) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await store.dispatch("deleteRecipe", {
|
||||
await store.dispatch('deleteRecipe', {
|
||||
id: store.state.recipe.id,
|
||||
})
|
||||
helpers.goTo("/")
|
||||
});
|
||||
helpers.goTo('/');
|
||||
} catch (e) {
|
||||
await showSimpleAlertModal(t("cookbook", "Delete failed"))
|
||||
await showSimpleAlertModal(t('cookbook', 'Delete failed'));
|
||||
if (e && e instanceof Error) {
|
||||
throw e
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -304,15 +300,15 @@ const printRecipe = () => {
|
|||
};
|
||||
|
||||
const reloadRecipeEdit = () => {
|
||||
emitter.emit("reloadRecipeEdit");
|
||||
emitter.emit('reloadRecipeEdit');
|
||||
};
|
||||
|
||||
const reloadRecipeView = () => {
|
||||
emitter.emit("reloadRecipeView");
|
||||
emitter.emit('reloadRecipeView');
|
||||
};
|
||||
|
||||
const saveChanges = () => {
|
||||
emitter.emit("saveRecipe");
|
||||
emitter.emit('saveRecipe');
|
||||
};
|
||||
|
||||
const search = (e) => {
|
||||
|
|
|
@ -14,6 +14,6 @@ export default {
|
|||
import { defineProps } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: "",}
|
||||
title: { type: String, default: '' },
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<span class="mode-indicator">{{ title }}</span>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ModeIndicator',
|
||||
|
@ -10,9 +9,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<script setup>
|
||||
import {defineProps} from 'vue';
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
defineProps({
|
||||
title: { type: String, default: '' }
|
||||
title: { type: String, default: '' },
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'Location',
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script setup>
|
||||
|
||||
import api from 'cookbook/js/api-interface';
|
||||
import { computed, getCurrentInstance, onMounted, ref, watch } from 'vue';
|
||||
|
||||
|
@ -27,7 +26,7 @@ const recipes = ref([]);
|
|||
* Is the Cookbook recipe directory currently being changed?
|
||||
*/
|
||||
const updatingRecipeDirectory = computed(() => {
|
||||
return store.state.updatingRecipeDirectory
|
||||
return store.state.updatingRecipeDirectory;
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -43,23 +42,23 @@ watch(updatingRecipeDirectory, async (newVal, oldVal) => {
|
|||
onMounted(() => {
|
||||
getCurrentInstance().proxy.$log.info('AppIndex mounted');
|
||||
loadAll();
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Load all recipes from the database
|
||||
*/
|
||||
const loadAll = () => {
|
||||
const loadAll = () => {
|
||||
api.recipes
|
||||
.getAll()
|
||||
.then((response) => {
|
||||
recipes.value = response.data
|
||||
recipes.value = response.data;
|
||||
|
||||
// Always set page name last
|
||||
store.dispatch('setPage', { page: 'index' })
|
||||
store.dispatch('setPage', { page: 'index' });
|
||||
})
|
||||
.catch(() => {
|
||||
// Always set page name last
|
||||
store.dispatch('setPage', { page: 'index' })
|
||||
store.dispatch('setPage', { page: 'index' });
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="main">
|
||||
<div class="dialog">
|
||||
<div class="message">
|
||||
{{ t("cookbook", "Cannot access recipe folder.") }}
|
||||
{{ t('cookbook', 'Cannot access recipe folder.') }}
|
||||
</div>
|
||||
<div>
|
||||
{{
|
||||
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<button @click.prevent="selectFolder">
|
||||
{{ t("cookbook", "Select recipe folder") }}
|
||||
{{ t('cookbook', 'Select recipe folder') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,7 +39,7 @@ const selectFolder = () => {
|
|||
this.$store
|
||||
.dispatch('updateRecipeDirectory', { dir: path })
|
||||
.then(() => {
|
||||
window.location.reload()
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -47,7 +47,7 @@ const selectFolder = () => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'InvalidGuest'
|
||||
name: 'InvalidGuest',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ const isMobile = useIsMobile();
|
|||
*/
|
||||
const isNavigationOpen = ref(false);
|
||||
|
||||
|
||||
// previously there was this commented section in this component. I leave it here for reference:
|
||||
// watch: {
|
||||
// // This might be handy when routing of Vue components needs fixing.
|
||||
|
@ -68,7 +67,6 @@ const updateAppNavigationOpen = ({ open }) => {
|
|||
const closeNavigation = () => {
|
||||
emit('toggle-navigation', { open: false });
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
@ -159,7 +157,7 @@ export default {
|
|||
|
||||
a:link::after,
|
||||
a:visited::after {
|
||||
content: " [" attr(href) "] ";
|
||||
content: ' [' attr(href) '] ';
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
@submit="downloadRecipe"
|
||||
@update:value="updateUrl"
|
||||
>
|
||||
{{ t("cookbook", "Download recipe from URL") }}
|
||||
{{ t('cookbook', 'Download recipe from URL') }}
|
||||
</NcActionInput>
|
||||
|
||||
<NcAppNavigationItem
|
||||
|
@ -53,7 +53,11 @@
|
|||
<NcAppNavigationItem
|
||||
v-for="(cat, idx) in categories"
|
||||
:key="cat + idx"
|
||||
:ref="el => { categoryItemElements[idx] = el }"
|
||||
:ref="
|
||||
(el) => {
|
||||
categoryItemElements[idx] = el;
|
||||
}
|
||||
"
|
||||
:name="cat.name"
|
||||
:icon="'icon-category-files'"
|
||||
:to="'/category/' + cat.name"
|
||||
|
@ -63,7 +67,7 @@
|
|||
@update:open="categoryOpen(idx)"
|
||||
@update:title="
|
||||
(val) => {
|
||||
categoryUpdateName(idx, val)
|
||||
categoryUpdateName(idx, val);
|
||||
}
|
||||
"
|
||||
>
|
||||
|
@ -84,7 +88,14 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { emit } from '@nextcloud/event-bus';
|
||||
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput';
|
||||
|
@ -102,7 +113,7 @@ import { showSimpleAlertModal } from 'cookbook/js/modals';
|
|||
|
||||
import emitter from '../bus';
|
||||
import { SHOW_SETTINGS_EVENT } from '../composables/useSettingsDialog';
|
||||
import { useStore } from "../store";
|
||||
import { useStore } from '../store';
|
||||
|
||||
const log = getCurrentInstance().proxy.$log;
|
||||
const store = useStore();
|
||||
|
@ -165,12 +176,15 @@ const categoryUpdating = computed(() => {
|
|||
|
||||
// Watchers
|
||||
// Register a method hook for navigation refreshing
|
||||
watch(() => refreshRequired.value, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal && newVal === true) {
|
||||
log.debug('Calling getCategories from refreshRequired');
|
||||
getCategories();
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => refreshRequired.value,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal && newVal === true) {
|
||||
log.debug('Calling getCategories from refreshRequired');
|
||||
getCategories();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Methods
|
||||
/**
|
||||
|
@ -198,7 +212,7 @@ const openCategory = async (idx) => {
|
|||
const response = await api.recipes.allInCategory(cat.name);
|
||||
cat.recipes = response.data;
|
||||
} catch (e) {
|
||||
cat.recipes = []
|
||||
cat.recipes = [];
|
||||
await showSimpleAlertModal(
|
||||
// prettier-ignore
|
||||
t('cookbook', 'Failed to load category {category} recipes',
|
||||
|
@ -338,9 +352,7 @@ const getCategories = async () => {
|
|||
}
|
||||
if (categoryItemElements[i][0].opened) {
|
||||
log.info(
|
||||
`Reloading recipes in ${
|
||||
categoryItemElements[i][0].title
|
||||
}`,
|
||||
`Reloading recipes in ${categoryItemElements[i][0].title}`,
|
||||
);
|
||||
await openCategory(i);
|
||||
}
|
||||
|
@ -349,11 +361,8 @@ const getCategories = async () => {
|
|||
store.dispatch('setAppNavigationRefreshRequired', {
|
||||
isRequired: false,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
await showSimpleAlertModal(
|
||||
t('cookbook', 'Failed to fetch categories'),
|
||||
);
|
||||
await showSimpleAlertModal(t('cookbook', 'Failed to fetch categories'));
|
||||
if (e && e instanceof Error) {
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -25,34 +25,33 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FilePickerType, getFilePickerBuilder } from "@nextcloud/dialogs";
|
||||
import { defineProps } from "vue";
|
||||
|
||||
import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs';
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
const props = defineProps({
|
||||
value: { type: String, default: ''},
|
||||
fieldLabel: { type: String, default: ""}
|
||||
value: { type: String, default: '' },
|
||||
fieldLabel: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const pickImage = (e) => {
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
const filePicker = getFilePickerBuilder(
|
||||
t("cookbook", "Path to your recipe image"),
|
||||
t('cookbook', 'Path to your recipe image'),
|
||||
)
|
||||
.addMimeTypeFilter("image/jpeg")
|
||||
.addMimeTypeFilter("image/png")
|
||||
.addMimeTypeFilter('image/jpeg')
|
||||
.addMimeTypeFilter('image/png')
|
||||
.setType(FilePickerType.Choose)
|
||||
.build();
|
||||
filePicker.pick().then((path) => {
|
||||
emit('input', path);
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditImageField'
|
||||
name: 'EditImageField',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
{{ fieldLabel }}
|
||||
</label>
|
||||
<textarea
|
||||
v-if="props.fieldType === 'textarea' || props.fieldType === 'markdown'"
|
||||
v-if="
|
||||
props.fieldType === 'textarea' || props.fieldType === 'markdown'
|
||||
"
|
||||
ref="inputField"
|
||||
v-model="content"
|
||||
@input="handleInput"
|
||||
|
@ -40,7 +42,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, nextTick, ref, watch } from "vue";
|
||||
import { getCurrentInstance, nextTick, ref, watch } from 'vue';
|
||||
import SuggestionsPopup from '../Modals/SuggestionsPopup';
|
||||
import useSuggestionPopup from '../../composables/useSuggestionsPopup';
|
||||
const log = getCurrentInstance().proxy.$log;
|
||||
|
@ -50,11 +52,11 @@ const emit = defineEmits(['input']);
|
|||
const props = defineProps({
|
||||
fieldLabel: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
fieldType: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
hide: {
|
||||
type: Boolean,
|
||||
|
@ -62,13 +64,13 @@ const props = defineProps({
|
|||
required: false,
|
||||
},
|
||||
suggestionOptions: {
|
||||
type: Array
|
||||
type: Array,
|
||||
},
|
||||
// Value (passed in v-model)
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
@ -106,14 +108,25 @@ let {
|
|||
handleSuggestionsPopupBlur,
|
||||
handleSuggestionsPopupMouseUp,
|
||||
handleSuggestionsPopupSelectedEvent,
|
||||
} = useSuggestionPopup(suggestionsPopupElement, lastCursorPosition, suggestionsData, null, emit, log, props);
|
||||
} = useSuggestionPopup(
|
||||
suggestionsPopupElement,
|
||||
lastCursorPosition,
|
||||
suggestionsData,
|
||||
null,
|
||||
emit,
|
||||
log,
|
||||
props,
|
||||
);
|
||||
|
||||
watch(() => props.value, (newValue) => {
|
||||
content.value = newValue;
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
content.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
const handleInput = () => {
|
||||
emit("input", content.value);
|
||||
emit('input', content.value);
|
||||
};
|
||||
|
||||
const keyDown = (e) => {
|
||||
|
@ -127,7 +140,7 @@ const pasteCanceled = async () => {
|
|||
// set cursor to position after pasted string
|
||||
await nextTick();
|
||||
const field = inputField;
|
||||
if (props.fieldType === "markdown") {
|
||||
if (props.fieldType === 'markdown') {
|
||||
field.editor.setCursor(lastCursorPosition.value);
|
||||
field.editor.focus();
|
||||
} else {
|
||||
|
@ -145,13 +158,13 @@ const pasteCanceled = async () => {
|
|||
const pasteString = async (str) => {
|
||||
const field = inputField;
|
||||
|
||||
if (props.fieldType === "markdown") {
|
||||
if (props.fieldType === 'markdown') {
|
||||
// insert at last cursor position
|
||||
field.editor.replaceRange(str, {
|
||||
line: lastCursorPosition.value.line,
|
||||
ch: lastCursorPosition.value.ch,
|
||||
});
|
||||
emit("input", content.value);
|
||||
emit('input', content.value);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
|
@ -166,7 +179,7 @@ const pasteString = async (str) => {
|
|||
content.value.slice(0, lastCursorPosition.value) +
|
||||
str +
|
||||
content.value.slice(lastCursorPosition.value);
|
||||
emit("input", content.value);
|
||||
emit('input', content.value);
|
||||
|
||||
// set cursor to position after pasted string. Waiting two ticks is necessary for
|
||||
// the data to be updated in the field
|
||||
|
@ -178,13 +191,11 @@ const pasteString = async (str) => {
|
|||
field.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditInputField'
|
||||
name: 'EditInputField',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -224,7 +235,7 @@ fieldset > div > input {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
fieldset input[type="number"] {
|
||||
fieldset input[type='number'] {
|
||||
width: 5em;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
@ -246,7 +257,7 @@ Hack to overwrite the heavy-handed global unscoped styles of Nextcloud core
|
|||
that cause our markdown editor CodeMirror to behave strangely on mobile
|
||||
See: https://github.com/nextcloud/cookbook/issues/908
|
||||
*/
|
||||
.editor:deep(div[contenteditable="true"]) {
|
||||
.editor:deep(div[contenteditable='true']) {
|
||||
width: revert;
|
||||
min-height: revert;
|
||||
padding: revert;
|
||||
|
|
|
@ -69,20 +69,22 @@
|
|||
ref="suggestionsPopupElement"
|
||||
v-bind="suggestionsData"
|
||||
:options="filteredSuggestionOptions"
|
||||
v-on:suggestions-selected="handleSuggestionsPopupSelectedEvent"
|
||||
v-on:suggestions-selected="
|
||||
handleSuggestionsPopupSelectedEvent
|
||||
"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="button add-list-item pad-icon" @click="addNewEntry()">
|
||||
<span class="icon-add"></span> {{ t("cookbook", "Add") }}
|
||||
<span class="icon-add"></span> {{ t('cookbook', 'Add') }}
|
||||
</button>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, nextTick, ref, watch } from "vue";
|
||||
import TriangleUpIcon from "icons/TriangleSmallUp.vue";
|
||||
import TriangleDownIcon from "icons/TriangleSmallDown.vue";
|
||||
import { getCurrentInstance, nextTick, ref, watch } from 'vue';
|
||||
import TriangleUpIcon from 'icons/TriangleSmallUp.vue';
|
||||
import TriangleDownIcon from 'icons/TriangleSmallDown.vue';
|
||||
|
||||
import SuggestionsPopup from '../Modals/SuggestionsPopup';
|
||||
import useSuggestionPopup from '../../composables/useSuggestionsPopup';
|
||||
|
@ -97,11 +99,11 @@ const props = defineProps({
|
|||
},
|
||||
fieldType: {
|
||||
type: String,
|
||||
default: "text",
|
||||
default: 'text',
|
||||
},
|
||||
fieldName: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
showStepNumber: {
|
||||
type: Boolean,
|
||||
|
@ -109,7 +111,7 @@ const props = defineProps({
|
|||
},
|
||||
fieldLabel: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
// If true, add new fields, for newlines in pasted data
|
||||
createFieldsOnNewlines: {
|
||||
|
@ -117,7 +119,7 @@ const props = defineProps({
|
|||
default: false,
|
||||
},
|
||||
suggestionOptions: {
|
||||
type: Array
|
||||
type: Array,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -145,9 +147,12 @@ const lastCursorPosition = ref(-1);
|
|||
*/
|
||||
const ignoreNextKeyUp = ref(false);
|
||||
|
||||
watch(() => props.value, (newValue) => {
|
||||
buffer.value = newValue.slice();
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
buffer.value = newValue.slice();
|
||||
},
|
||||
);
|
||||
|
||||
// deconstruct composable
|
||||
let {
|
||||
|
@ -166,14 +171,22 @@ let {
|
|||
handleSuggestionsPopupBlur,
|
||||
handleSuggestionsPopupMouseUp,
|
||||
handleSuggestionsPopupSelectedEvent,
|
||||
} = useSuggestionPopup(suggestionsPopupElement, lastCursorPosition, suggestionsData, buffer, emit, log, props);
|
||||
} = useSuggestionPopup(
|
||||
suggestionsPopupElement,
|
||||
lastCursorPosition,
|
||||
suggestionsData,
|
||||
buffer,
|
||||
emit,
|
||||
log,
|
||||
props,
|
||||
);
|
||||
|
||||
|
||||
watch(() => props.value,
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
buffer.value = val.slice();
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const linesMatchAtPosition = (lines, i) =>
|
||||
|
@ -184,7 +197,7 @@ const findCommonPrefix = (lines) => {
|
|||
// Inspired from https://stackoverflow.com/questions/68702774/longest-common-prefix-in-javascript
|
||||
|
||||
// Check border cases size 1 array and empty first word)
|
||||
if (!lines[0] || lines.length === 1) return lines[0] || ""
|
||||
if (!lines[0] || lines.length === 1) return lines[0] || '';
|
||||
|
||||
// Loop up index until the characters do not match
|
||||
for (let i = 0; ; i++) {
|
||||
|
@ -192,7 +205,7 @@ const findCommonPrefix = (lines) => {
|
|||
// or the character of each line at position i is not identical
|
||||
if (!lines[0][i] || !linesMatchAtPosition(lines, i)) {
|
||||
// Then the desired prefix is the substring from the beginning to i
|
||||
return lines[0].substr(0, i)
|
||||
return lines[0].substr(0, i);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -201,12 +214,16 @@ const findCommonPrefix = (lines) => {
|
|||
* if focusAfterInsert=true, the element is focussed after inserting
|
||||
* the content is inserted into the newly created field
|
||||
* */
|
||||
const addNewEntry = async (index = -1, focusAfterInsert = true, content = "") => {
|
||||
let entryIdx = index
|
||||
const addNewEntry = async (
|
||||
index = -1,
|
||||
focusAfterInsert = true,
|
||||
content = '',
|
||||
) => {
|
||||
let entryIdx = index;
|
||||
if (entryIdx === -1) {
|
||||
entryIdx = buffer.value.length
|
||||
entryIdx = buffer.value.length;
|
||||
}
|
||||
buffer.value.splice(entryIdx, 0, content)
|
||||
buffer.value.splice(entryIdx, 0, content);
|
||||
|
||||
if (focusAfterInsert) {
|
||||
await nextTick();
|
||||
|
@ -222,7 +239,7 @@ const addNewEntry = async (index = -1, focusAfterInsert = true, content = "") =>
|
|||
*/
|
||||
const deleteEntry = (index) => {
|
||||
buffer.value.splice(index, 1);
|
||||
emit("input", buffer.value);
|
||||
emit('input', buffer.value);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -234,12 +251,12 @@ const handleInput = (e) => {
|
|||
// https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/inputType
|
||||
// https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
|
||||
if (
|
||||
e.inputType === "insertFromPaste" ||
|
||||
e.inputType === "insertFromPasteAsQuotation"
|
||||
e.inputType === 'insertFromPaste' ||
|
||||
e.inputType === 'insertFromPasteAsQuotation'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
emit("input", buffer.value);
|
||||
emit('input', buffer.value);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -249,16 +266,16 @@ const handlePaste = async (e) => {
|
|||
// get data from clipboard to keep newline characters, which are stripped
|
||||
// from the data pasted in the input field (e.target.value)
|
||||
const clipboardData = e.clipboardData || window.clipboardData;
|
||||
const pastedData = clipboardData.getData("Text");
|
||||
const pastedData = clipboardData.getData('Text');
|
||||
const inputLinesArray = pastedData
|
||||
.split(/\r\n|\r|\n/g)
|
||||
// Remove empty lines
|
||||
.filter((line) => line.trim() !== "");
|
||||
.filter((line) => line.trim() !== '');
|
||||
|
||||
// If only a single line pasted, emit that line and exit
|
||||
// Treat it as if that single line was typed
|
||||
if (inputLinesArray.length === 1) {
|
||||
emit("input", buffer.value);
|
||||
emit('input', buffer.value);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -267,14 +284,11 @@ const handlePaste = async (e) => {
|
|||
return;
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
|
||||
const $li = e.currentTarget.closest("li");
|
||||
const $ul = $li.closest("ul");
|
||||
const $insertedIndex = Array.prototype.indexOf.call(
|
||||
$ul.childNodes,
|
||||
$li,
|
||||
);
|
||||
const $li = e.currentTarget.closest('li');
|
||||
const $ul = $li.closest('ul');
|
||||
const $insertedIndex = Array.prototype.indexOf.call($ul.childNodes, $li);
|
||||
|
||||
// Remove the common prefix from each line of the pasted text
|
||||
// For example, if the pasted text uses - for a bullet list
|
||||
|
@ -287,9 +301,7 @@ const handlePaste = async (e) => {
|
|||
// as it should work for any alphabet
|
||||
const re =
|
||||
/[^\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~]/g;
|
||||
const prefixLength = re.test(prefix)
|
||||
? prefix.search(re)
|
||||
: prefix.length;
|
||||
const prefixLength = re.test(prefix) ? prefix.search(re) : prefix.length;
|
||||
|
||||
for (let i = 0; i < inputLinesArray.length; ++i) {
|
||||
inputLinesArray[i] = inputLinesArray[i].slice(prefixLength);
|
||||
|
@ -300,24 +312,18 @@ const handlePaste = async (e) => {
|
|||
// to accidentally replace all newlines with spaces before splitting
|
||||
// Fixes #713
|
||||
for (let i = 0; i < inputLinesArray.length; ++i) {
|
||||
inputLinesArray[i] = inputLinesArray[i]
|
||||
.trim()
|
||||
.replaceAll(/\s+/g, " ");
|
||||
inputLinesArray[i] = inputLinesArray[i].trim().replaceAll(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
for (let i = 0; i < inputLinesArray.length; ++i) {
|
||||
await addNewEntry(
|
||||
$insertedIndex + i + 1,
|
||||
false,
|
||||
inputLinesArray[i],
|
||||
);
|
||||
await addNewEntry($insertedIndex + i + 1, false, inputLinesArray[i]);
|
||||
}
|
||||
emit("input", buffer.value)
|
||||
emit('input', buffer.value);
|
||||
|
||||
await nextTick();
|
||||
let indexToFocus = $insertedIndex + inputLinesArray.length
|
||||
let indexToFocus = $insertedIndex + inputLinesArray.length;
|
||||
// Delete field if it's empty
|
||||
if (buffer.value[$insertedIndex].trim() === "") {
|
||||
if (buffer.value[$insertedIndex].trim() === '') {
|
||||
deleteEntry($insertedIndex);
|
||||
indexToFocus -= 1;
|
||||
}
|
||||
|
@ -354,7 +360,7 @@ const keyDown = async (e) => {
|
|||
}
|
||||
|
||||
// Only do anything for enter
|
||||
if (e.key !== "Enter") {
|
||||
if (e.key !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -363,21 +369,18 @@ const keyDown = async (e) => {
|
|||
e.preventDefault();
|
||||
|
||||
// Get the index of the pressed list item
|
||||
const $li = e.currentTarget.closest("li");
|
||||
const $ul = $li.closest("ul");
|
||||
const $pressedLiIndex = Array.prototype.indexOf.call(
|
||||
$ul.childNodes,
|
||||
$li,
|
||||
);
|
||||
const $li = e.currentTarget.closest('li');
|
||||
const $ul = $li.closest('ul');
|
||||
const $pressedLiIndex = Array.prototype.indexOf.call($ul.childNodes, $li);
|
||||
|
||||
if ($pressedLiIndex >= this.$refs["list-field"].length - 1) {
|
||||
if ($pressedLiIndex >= this.$refs['list-field'].length - 1) {
|
||||
await addNewEntry();
|
||||
} else {
|
||||
// Focus the next input or textarea
|
||||
// We have to check for both, as inputs are used for
|
||||
// ingredients and textareas are used for instructions
|
||||
$ul.children[$pressedLiIndex + 1]
|
||||
.querySelector("input, textarea")
|
||||
.querySelector('input, textarea')
|
||||
.focus();
|
||||
}
|
||||
};
|
||||
|
@ -393,15 +396,12 @@ const keyUp = (e) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const $li = e.currentTarget.closest("li");
|
||||
const $ul = $li.closest("ul");
|
||||
const $li = e.currentTarget.closest('li');
|
||||
const $ul = $li.closest('ul');
|
||||
// noinspection UnnecessaryLocalVariableJS
|
||||
const $pressedLiIndex = Array.prototype.indexOf.call(
|
||||
$ul.childNodes,
|
||||
$li,
|
||||
);
|
||||
lastFocusedFieldIndex.value = $pressedLiIndex
|
||||
handleSuggestionsPopupKeyUp(e)
|
||||
const $pressedLiIndex = Array.prototype.indexOf.call($ul.childNodes, $li);
|
||||
lastFocusedFieldIndex.value = $pressedLiIndex;
|
||||
handleSuggestionsPopupKeyUp(e);
|
||||
};
|
||||
|
||||
const moveEntryDown = (index) => {
|
||||
|
@ -415,7 +415,7 @@ const moveEntryDown = (index) => {
|
|||
} else {
|
||||
buffer.value.push(entry);
|
||||
}
|
||||
emit("input", buffer.value);
|
||||
emit('input', buffer.value);
|
||||
};
|
||||
|
||||
const moveEntryUp = (index) => {
|
||||
|
@ -425,18 +425,15 @@ const moveEntryUp = (index) => {
|
|||
}
|
||||
const entry = buffer.value.splice(index, 1)[0];
|
||||
buffer.value.splice(index - 1, 0, entry);
|
||||
emit("input", buffer.value);
|
||||
emit('input', buffer.value);
|
||||
};
|
||||
|
||||
const pasteCanceled = async () => {
|
||||
const field = listField[this.lastFocusedFieldIndex];
|
||||
// set cursor back to previous position
|
||||
await nextTick()
|
||||
await nextTick();
|
||||
field.focus();
|
||||
field.setSelectionRange(
|
||||
lastCursorPosition.value,
|
||||
lastCursorPosition.value,
|
||||
);
|
||||
field.setSelectionRange(lastCursorPosition.value, lastCursorPosition.value);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -453,7 +450,7 @@ const pasteString = async (str, ignoreKeyup = true) => {
|
|||
str +
|
||||
content.slice(lastCursorPosition.value);
|
||||
buffer.value[lastFocusedFieldIndex.value] = updatedContent;
|
||||
emit("input", buffer.value);
|
||||
emit('input', buffer.value);
|
||||
|
||||
// set cursor to position after pasted string. Waiting two ticks is necessary for
|
||||
// the data to be updated in the field
|
||||
|
@ -468,7 +465,7 @@ const pasteString = async (str, ignoreKeyup = true) => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditInputGroup'
|
||||
name: 'EditInputGroup',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -561,7 +558,7 @@ li .controls > button:last-child:not(:hover):not(:focus) {
|
|||
.textarea::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.step-number {
|
||||
|
|
|
@ -15,14 +15,14 @@ import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect';
|
|||
defineProps({
|
||||
fieldLabel: {
|
||||
type: String,
|
||||
default: "",
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditMultiselect'
|
||||
name: 'EditMultiselect',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -12,7 +12,11 @@
|
|||
:placeholder="labelSelectPlaceholder"
|
||||
class="key"
|
||||
/>
|
||||
<input v-model="row.customText" :placeholder="row.options.placeholder" class="val" />
|
||||
<input
|
||||
v-model="row.customText"
|
||||
:placeholder="row.options.placeholder"
|
||||
class="val"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
@ -27,7 +31,7 @@ const emits = defineEmits(['input']);
|
|||
const props = defineProps({
|
||||
fieldLabel: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
labelSelectPlaceholder: {
|
||||
type: String,
|
||||
|
@ -57,10 +61,13 @@ const props = defineProps({
|
|||
*/
|
||||
const rows = ref([]);
|
||||
|
||||
watch(() => props.value, async (newModelValue) => {
|
||||
// React to external changes in modelValue
|
||||
await createRowsBasedOnModelValue(newModelValue);
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
async (newModelValue) => {
|
||||
// React to external changes in modelValue
|
||||
await createRowsBasedOnModelValue(newModelValue);
|
||||
},
|
||||
);
|
||||
|
||||
const createRowsBasedOnModelValue = async () => {
|
||||
const initialModelValue = props.value || {};
|
||||
|
@ -70,8 +77,8 @@ const createRowsBasedOnModelValue = async () => {
|
|||
const option = props.options.find((opt) => opt.key === key);
|
||||
const row = rows.value.find((row) => row.selectedOption.key === key);
|
||||
// Update row with key if it already exists
|
||||
if(row){
|
||||
row.customText = initialModelValue[key] || ''
|
||||
if (row) {
|
||||
row.customText = initialModelValue[key] || '';
|
||||
}
|
||||
// otherwise create new row
|
||||
else {
|
||||
|
@ -87,12 +94,12 @@ const createRowsBasedOnModelValue = async () => {
|
|||
|
||||
await recalculateAvailableOptions();
|
||||
await createRow(); // Create an additional row for future selections
|
||||
}
|
||||
};
|
||||
|
||||
const createRow = async () => {
|
||||
// Remove empty rows at the end before creating a new one
|
||||
for (let i = rows.value.length - 1; i >= 0; i--){
|
||||
if(!(rows.value[i].selectedOption)){
|
||||
for (let i = rows.value.length - 1; i >= 0; i--) {
|
||||
if (!rows.value[i].selectedOption) {
|
||||
rows.value.pop();
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +113,7 @@ const createRow = async () => {
|
|||
customText: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMultiselectChange = async (changedIndex) => {
|
||||
// Wait for the DOM to update after the multiselect change
|
||||
|
@ -122,20 +129,20 @@ const handleMultiselectChange = async (changedIndex) => {
|
|||
emits('input', getSelectedValues());
|
||||
|
||||
await createRow();
|
||||
}
|
||||
};
|
||||
|
||||
const recalculateAvailableOptions = async () => {
|
||||
// Update options in all rows based on the current selections
|
||||
for (let i = 0; i < rows.value.length; i++) {
|
||||
rows.value[i].options = await getAvailableOptions();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getAvailableOptions = async () => {
|
||||
// Calculate available options by excluding those already selected
|
||||
const selectedOptions = rows.value.map((row) => row.selectedOption);
|
||||
return props.options.filter((option) => !selectedOptions.includes(option));
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedValues = () => {
|
||||
const selectedValues = {};
|
||||
|
@ -146,13 +153,12 @@ const getSelectedValues = () => {
|
|||
}
|
||||
});
|
||||
return selectedValues;
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditMultiselectInputGroup'
|
||||
name: 'EditMultiselectInputGroup',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ const props = defineProps({
|
|||
},
|
||||
fieldLabel: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -49,20 +49,23 @@ const hours = ref(null);
|
|||
*/
|
||||
const minutes = ref(null);
|
||||
|
||||
watch(() => props.value, () => {
|
||||
hours.value = props.value.time[0];
|
||||
minutes.value = props.value.time[1];
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
hours.value = props.value.time[0];
|
||||
minutes.value = props.value.time[1];
|
||||
},
|
||||
);
|
||||
|
||||
const handleInput = () => {
|
||||
minutes.value = minutes.value ? minutes.value : 0;
|
||||
hours.value = hours.value ? hours.value : 0;
|
||||
|
||||
// create padded time string
|
||||
const hoursPadded = hours.value.toString().padStart(2, "0");
|
||||
const minutesPadded = minutes.value.toString().padStart(2, "0");
|
||||
const hoursPadded = hours.value.toString().padStart(2, '0');
|
||||
const minutesPadded = minutes.value.toString().padStart(2, '0');
|
||||
|
||||
emit("input", {
|
||||
emit('input', {
|
||||
time: [hours.value, minutes.value],
|
||||
paddedTime: `PT${hoursPadded}H${minutesPadded}M`,
|
||||
});
|
||||
|
@ -71,7 +74,7 @@ const handleInput = () => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditTimeField'
|
||||
name: 'EditTimeField',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -42,29 +42,29 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from "@nextcloud/moment"
|
||||
import LazyPicture from "../Utilities/LazyPicture.vue"
|
||||
import moment from '@nextcloud/moment';
|
||||
import LazyPicture from '../Utilities/LazyPicture.vue';
|
||||
|
||||
defineProps({
|
||||
recipe: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const formatDateTime = (dt) => {
|
||||
if (!dt) return null
|
||||
const date = moment(dt, moment.ISO_8601)
|
||||
if (!dt) return null;
|
||||
const date = moment(dt, moment.ISO_8601);
|
||||
if (!date.isValid()) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
return date.format("L, LT").toString()
|
||||
return date.format('L, LT').toString();
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeCard'
|
||||
name: 'RecipeCard',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed,onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router/composables';
|
||||
import { useStore } from '../../store';
|
||||
import RecipeIcon from 'vue-material-design-icons/ChefHat.vue';
|
||||
|
@ -190,7 +190,7 @@ const orderBy = ref(recipeOrderingOptions.value[0]);
|
|||
|
||||
onMounted(() => {
|
||||
store.dispatch('clearRecipeFilters');
|
||||
})
|
||||
});
|
||||
|
||||
// Computed properties
|
||||
/**
|
||||
|
@ -252,7 +252,9 @@ const filteredRecipes = computed(() => {
|
|||
let ret = recipesFilteredByKeywords.value;
|
||||
if (!!store.state.recipeFilters) {
|
||||
ret = ret.filter((r) =>
|
||||
r.name.toLowerCase().includes(store.state.recipeFilters.toLowerCase()),
|
||||
r.name
|
||||
.toLowerCase()
|
||||
.includes(store.state.recipeFilters.toLowerCase()),
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
|
@ -314,7 +316,7 @@ const recipeObjects = computed(() => {
|
|||
}
|
||||
if (orderBy.value.recipeProperty === 'dateModified') {
|
||||
if (orderBy.value.order === 'ascending') {
|
||||
return recipesDateModifiedAsc.value.map(makeObject)
|
||||
return recipesDateModifiedAsc.value.map(makeObject);
|
||||
}
|
||||
return recipesDateModifiedDesc.value.map(makeObject);
|
||||
}
|
||||
|
@ -346,18 +348,13 @@ const sortRecipes = (recipes, recipeProperty, order) => {
|
|||
recipeProperty === 'dateModified'
|
||||
) {
|
||||
return (
|
||||
new Date(r1[recipeProperty]) -
|
||||
new Date(r2[recipeProperty])
|
||||
new Date(r1[recipeProperty]) - new Date(r2[recipeProperty])
|
||||
);
|
||||
}
|
||||
if (recipeProperty === 'name') {
|
||||
return r1[recipeProperty].localeCompare(
|
||||
r2[recipeProperty],
|
||||
);
|
||||
return r1[recipeProperty].localeCompare(r2[recipeProperty]);
|
||||
}
|
||||
if (
|
||||
!Number.isNaN(r1[recipeProperty] - r2[recipeProperty])
|
||||
) {
|
||||
if (!Number.isNaN(r1[recipeProperty] - r2[recipeProperty])) {
|
||||
return r1[recipeProperty] - r2[recipeProperty];
|
||||
}
|
||||
return 0;
|
||||
|
@ -367,10 +364,7 @@ const sortRecipes = (recipes, recipeProperty, order) => {
|
|||
recipeProperty === 'dateCreated' ||
|
||||
recipeProperty === 'dateModified'
|
||||
) {
|
||||
return (
|
||||
new Date(r2[recipeProperty]) -
|
||||
new Date(r1[recipeProperty])
|
||||
);
|
||||
return new Date(r2[recipeProperty]) - new Date(r1[recipeProperty]);
|
||||
}
|
||||
if (recipeProperty === 'name') {
|
||||
return r2[recipeProperty].localeCompare(r1[recipeProperty]);
|
||||
|
@ -385,7 +379,7 @@ const sortRecipes = (recipes, recipeProperty, order) => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeList'
|
||||
name: 'RecipeList',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -130,9 +130,7 @@ const keywordsWithCount = computed(() => {
|
|||
}))
|
||||
.sort((k1, k2) => {
|
||||
if (isOrderedAlphabetically.value) {
|
||||
return k1.name.toLowerCase() > k2.name.toLowerCase()
|
||||
? 1
|
||||
: -1;
|
||||
return k1.name.toLowerCase() > k2.name.toLowerCase() ? 1 : -1;
|
||||
}
|
||||
// else: order by number of recipe with this keyword (decreasing)
|
||||
if (k1.count !== k2.count) {
|
||||
|
@ -140,9 +138,7 @@ const keywordsWithCount = computed(() => {
|
|||
return k2.count - k1.count;
|
||||
}
|
||||
// Distinguish by keyword name
|
||||
return k1.name.toLowerCase() > k2.name.toLowerCase()
|
||||
? 1
|
||||
: -1;
|
||||
return k1.name.toLowerCase() > k2.name.toLowerCase() ? 1 : -1;
|
||||
});
|
||||
});
|
||||
/**
|
||||
|
@ -155,11 +151,7 @@ const selectableKeywords = computed(() => {
|
|||
|
||||
return unselectedKeywords.value.filter((kw) =>
|
||||
props.filteredRecipes
|
||||
.map(
|
||||
(r) =>
|
||||
r.keywords &&
|
||||
r.keywords.split(',').includes(kw.name),
|
||||
)
|
||||
.map((r) => r.keywords && r.keywords.split(',').includes(kw.name))
|
||||
.reduce((l, r) => l || r, false),
|
||||
);
|
||||
});
|
||||
|
@ -192,11 +184,12 @@ const unselectedKeywords = computed(() => {
|
|||
/**
|
||||
* Watch array of selected keywords for changes
|
||||
*/
|
||||
watch(() => props.value,
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
selectedKeywordsBuffer.value = props.value.slice()
|
||||
selectedKeywordsBuffer.value = props.value.slice();
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Methods
|
||||
|
@ -204,7 +197,7 @@ watch(() => props.value,
|
|||
* Callback for click on keyword, add to or remove from list
|
||||
*/
|
||||
const keywordClicked = (keyword) => {
|
||||
const index = selectedKeywordsBuffer.value.indexOf(keyword.name)
|
||||
const index = selectedKeywordsBuffer.value.indexOf(keyword.name);
|
||||
if (index > -1) {
|
||||
selectedKeywordsBuffer.value.splice(index, 1);
|
||||
} else {
|
||||
|
@ -222,7 +215,7 @@ const toggleOrderCriterion = () => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeListKeywordCloud'
|
||||
name: 'RecipeListKeywordCloud',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -173,10 +173,14 @@
|
|||
</NcAppSettingsDialog>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import {
|
||||
getCurrentInstance,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { subscribe, unsubscribe } from '@nextcloud/event-bus';
|
||||
import { getFilePickerBuilder } from '@nextcloud/dialogs';
|
||||
|
||||
|
@ -264,83 +268,94 @@ const visibleInfoBlocks = ref([...INFO_BLOCK_KEYS]);
|
|||
const resetVisibleInfoBlocks = ref(true);
|
||||
|
||||
// Watchers
|
||||
watch(() => printImage.value, async (newVal, oldVal) => {
|
||||
// Avoid infinite loop on page load and when resetting value after failed submit
|
||||
if (resetPrintImage.value) {
|
||||
resetPrintImage.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.config.printImage.update(newVal);
|
||||
// Should this check the response of the query? To catch some errors that redirect the page
|
||||
} catch {
|
||||
await showSimpleAlertModal(
|
||||
// prettier-ignore
|
||||
t('cookbook','Could not set preference for image printing'),
|
||||
);
|
||||
resetPrintImage.value = true;
|
||||
printImage.value = oldVal;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => printImage.value,
|
||||
async (newVal, oldVal) => {
|
||||
// Avoid infinite loop on page load and when resetting value after failed submit
|
||||
if (resetPrintImage.value) {
|
||||
resetPrintImage.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.config.printImage.update(newVal);
|
||||
// Should this check the response of the query? To catch some errors that redirect the page
|
||||
} catch {
|
||||
await showSimpleAlertModal(
|
||||
// prettier-ignore
|
||||
t('cookbook','Could not set preference for image printing'),
|
||||
);
|
||||
resetPrintImage.value = true;
|
||||
printImage.value = oldVal;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
watch(() => showTagCloudInRecipeList.value, (newVal) => {
|
||||
store.dispatch('setShowTagCloudInRecipeList', {
|
||||
showTagCloud: newVal,
|
||||
});
|
||||
});
|
||||
watch(
|
||||
() => showTagCloudInRecipeList.value,
|
||||
(newVal) => {
|
||||
store.dispatch('setShowTagCloudInRecipeList', {
|
||||
showTagCloud: newVal,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
watch(() => updateInterval.value, async (newVal, oldVal) => {
|
||||
// Avoid infinite loop on page load and when resetting value after failed submit
|
||||
if (resetInterval.value) {
|
||||
resetInterval.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.config.updateInterval.update(newVal);
|
||||
// Should this check the response of the query? To catch some errors that redirect the page
|
||||
} catch {
|
||||
await showSimpleAlertModal(
|
||||
// prettier-ignore
|
||||
t('cookbook','Could not set recipe update interval to {interval}',
|
||||
watch(
|
||||
() => updateInterval.value,
|
||||
async (newVal, oldVal) => {
|
||||
// Avoid infinite loop on page load and when resetting value after failed submit
|
||||
if (resetInterval.value) {
|
||||
resetInterval.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.config.updateInterval.update(newVal);
|
||||
// Should this check the response of the query? To catch some errors that redirect the page
|
||||
} catch {
|
||||
await showSimpleAlertModal(
|
||||
// prettier-ignore
|
||||
t('cookbook','Could not set recipe update interval to {interval}',
|
||||
{
|
||||
interval: newVal,
|
||||
}
|
||||
),
|
||||
);
|
||||
resetInterval.value = true;
|
||||
updateInterval.value = oldVal;
|
||||
}
|
||||
});
|
||||
);
|
||||
resetInterval.value = true;
|
||||
updateInterval.value = oldVal;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(() => visibleInfoBlocks.value, async (newVal, oldVal) => {
|
||||
// Avoid infinite loop on page load and when resetting value after failed submit
|
||||
if (resetVisibleInfoBlocks.value) {
|
||||
resetVisibleInfoBlocks.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = visibleInfoBlocksEncode(newVal);
|
||||
await api.config.visibleInfoBlocks.update(data);
|
||||
await store.dispatch('refreshConfig');
|
||||
// Should this check the response of the query? To catch some errors that redirect the page
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error while trying to save info blocks', err);
|
||||
await showSimpleAlertModal(
|
||||
t('cookbook', 'Could not save visible info blocks'),
|
||||
);
|
||||
resetVisibleInfoBlocks.value = true;
|
||||
visibleInfoBlocks.value = oldVal;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => visibleInfoBlocks.value,
|
||||
async (newVal, oldVal) => {
|
||||
// Avoid infinite loop on page load and when resetting value after failed submit
|
||||
if (resetVisibleInfoBlocks.value) {
|
||||
resetVisibleInfoBlocks.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = visibleInfoBlocksEncode(newVal);
|
||||
await api.config.visibleInfoBlocks.update(data);
|
||||
await store.dispatch('refreshConfig');
|
||||
// Should this check the response of the query? To catch some errors that redirect the page
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error while trying to save info blocks', err);
|
||||
await showSimpleAlertModal(
|
||||
t('cookbook', 'Could not save visible info blocks'),
|
||||
);
|
||||
resetVisibleInfoBlocks.value = true;
|
||||
visibleInfoBlocks.value = oldVal;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
setup();
|
||||
subscribe(SHOW_SETTINGS_EVENT, handleShowSettings);
|
||||
});
|
||||
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe(SHOW_SETTINGS_EVENT, handleShowSettings);
|
||||
});
|
||||
|
@ -381,7 +396,7 @@ const pickRecipeFolder = () => {
|
|||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -408,9 +423,7 @@ const setup = async () => {
|
|||
recipeFolder.value = config.folder;
|
||||
} catch (err) {
|
||||
log.error('Error setting up SettingsDialog', err);
|
||||
await showSimpleAlertModal(
|
||||
t('cookbook', 'Loading config failed'),
|
||||
);
|
||||
await showSimpleAlertModal(t('cookbook', 'Loading config failed'));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -428,9 +441,7 @@ const reindex = () => {
|
|||
.then(() => {
|
||||
scanningLibrary.value = false;
|
||||
log.info('Library reindexing complete');
|
||||
if (
|
||||
['index', 'search'].indexOf(store.state.page) > -1
|
||||
) {
|
||||
if (['index', 'search'].indexOf(store.state.page) > -1) {
|
||||
// This refreshes the current router view in case items in it changed during reindex
|
||||
router.go();
|
||||
} else {
|
||||
|
@ -444,7 +455,7 @@ const reindex = () => {
|
|||
};
|
||||
|
||||
const enableLogger = () => {
|
||||
enableLogging()
|
||||
enableLogging();
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -461,8 +472,8 @@ export default {
|
|||
</style>
|
||||
|
||||
<style>
|
||||
#app-settings input[type="text"],
|
||||
#app-settings input[type="number"],
|
||||
#app-settings input[type='text'],
|
||||
#app-settings input[type='number'],
|
||||
#app-settings .button.disable {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineProps, onMounted, ref, watch } from "vue";
|
||||
import { computed, defineProps, onMounted, ref, watch } from 'vue';
|
||||
const SUGGESTIONS_POPUP_WIDTH = 300;
|
||||
|
||||
const emit = defineEmits(['suggestions-selected']);
|
||||
|
@ -72,9 +72,7 @@ const offset = computed(() => {
|
|||
return {
|
||||
left: Math.min(
|
||||
props.field.offsetLeft + caretPos.left,
|
||||
field.offsetLeft +
|
||||
field.offsetWidth -
|
||||
SUGGESTIONS_POPUP_WIDTH
|
||||
field.offsetLeft + field.offsetWidth - SUGGESTIONS_POPUP_WIDTH,
|
||||
),
|
||||
top: field.offsetTop + caretPos.top + caretPos.height,
|
||||
};
|
||||
|
@ -84,22 +82,25 @@ const offset = computed(() => {
|
|||
* Scroll to centre the focused element in the parent when it changes
|
||||
* (with arrow keys, for example)
|
||||
*/
|
||||
watch(() => props.focusIndex, (focusIndex) => {
|
||||
if(scroller.value === null) return;
|
||||
watch(
|
||||
() => props.focusIndex,
|
||||
(focusIndex) => {
|
||||
if (scroller.value === null) return;
|
||||
|
||||
const parentHeight = scroller.value.offsetHeight;
|
||||
const childHeight = scroller.value.children[0].offsetHeight;
|
||||
const parentHeight = scroller.value.offsetHeight;
|
||||
const childHeight = scroller.value.children[0].offsetHeight;
|
||||
|
||||
// Get the scroll position of the top of the focused element
|
||||
const focusedChildTop = childHeight * focusIndex;
|
||||
// Get the centre
|
||||
const focusedChildMiddle = focusedChildTop + childHeight / 2;
|
||||
// Offset to centre in the parent scrolling element
|
||||
const parentMiddle = focusedChildMiddle - parentHeight / 2;
|
||||
// Get the scroll position of the top of the focused element
|
||||
const focusedChildTop = childHeight * focusIndex;
|
||||
// Get the centre
|
||||
const focusedChildMiddle = focusedChildTop + childHeight / 2;
|
||||
// Offset to centre in the parent scrolling element
|
||||
const parentMiddle = focusedChildMiddle - parentHeight / 2;
|
||||
|
||||
// Scroll to that position
|
||||
scroller.value.scrollTo(0, parentMiddle);
|
||||
});
|
||||
// Scroll to that position
|
||||
scroller.value.scrollTo(0, parentMiddle);
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scroller.value.scrollTo(0, 0);
|
||||
|
@ -118,7 +119,7 @@ const handleClick = (e) => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SuggestionsPopup'
|
||||
name: 'SuggestionsPopup',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<h2>{{ t("cookbook", "The page was not found") }}</h2>
|
||||
<h2>{{ t('cookbook', 'The page was not found') }}</h2>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
@ -9,13 +9,13 @@ import { useStore } from '../store';
|
|||
const store = useStore();
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch("setPage", { page: "notfound" });
|
||||
store.dispatch('setPage', { page: 'notfound' });
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotFound'
|
||||
name: 'NotFound',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -113,16 +113,28 @@
|
|||
: 'icon-checkmark'
|
||||
"
|
||||
></span>
|
||||
{{ t("cookbook", "Save") }}
|
||||
{{ t('cookbook', 'Save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import {onBeforeRouteLeave, onBeforeRouteUpdate, useRoute} from 'vue-router/composables';
|
||||
import {
|
||||
onBeforeRouteLeave,
|
||||
onBeforeRouteUpdate,
|
||||
useRoute,
|
||||
} from 'vue-router/composables';
|
||||
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions';
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
|
||||
|
@ -320,14 +332,13 @@ const overlayVisible = computed(() => {
|
|||
store.state.loadingRecipe ||
|
||||
store.state.reloadingRecipe ||
|
||||
(store.state.categoryUpdating &&
|
||||
store.state.categoryUpdating ===
|
||||
recipe.value.recipeCategory)
|
||||
store.state.categoryUpdating === recipe.value.recipeCategory)
|
||||
);
|
||||
});
|
||||
const recipeWithCorrectedYield = computed(() => {
|
||||
const r = recipe.value;
|
||||
if (!showRecipeYield.value) {
|
||||
r.recipeYield = null
|
||||
r.recipeYield = null;
|
||||
}
|
||||
return r;
|
||||
});
|
||||
|
@ -337,41 +348,45 @@ const isNavigationDangerous = computed(() => {
|
|||
return !savingRecipe.value && formDirty.value;
|
||||
});
|
||||
|
||||
|
||||
// ===================
|
||||
// Watchers
|
||||
// ===================
|
||||
|
||||
watch(() => prepTime.value,
|
||||
watch(
|
||||
() => prepTime.value,
|
||||
() => {
|
||||
recipe.value.prepTime = prepTime.value.paddedTime;
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
watch(() => cookTime.value,
|
||||
watch(
|
||||
() => cookTime.value,
|
||||
() => {
|
||||
recipe.value.cookTime = cookTime.value.paddedTime;
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
watch(() => totalTime.value,
|
||||
watch(
|
||||
() => totalTime.value,
|
||||
() => {
|
||||
recipe.value.totalTime = totalTime.value.paddedTime;
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
watch(() => selectedKeywords.value,
|
||||
watch(
|
||||
() => selectedKeywords.value,
|
||||
() => {
|
||||
// convert keyword array to comma-separated string
|
||||
recipe.value.keywords = selectedKeywords.value.join();
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
watch(() => recipe.value,
|
||||
watch(
|
||||
() => recipe.value,
|
||||
() => {
|
||||
formDirty.value = true
|
||||
formDirty.value = true;
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// ===================
|
||||
|
@ -436,7 +451,7 @@ onMounted(() => {
|
|||
// Register data load method hook for access from the controls components
|
||||
emitter.off('reloadRecipeEdit');
|
||||
emitter.on('reloadRecipeEdit', () => {
|
||||
loadRecipeData()
|
||||
loadRecipeData();
|
||||
});
|
||||
emitter.off('categoryRenamed');
|
||||
emitter.on('categoryRenamed', (val) => {
|
||||
|
@ -465,14 +480,13 @@ onMounted(() => {
|
|||
.then(() => {
|
||||
// finally
|
||||
loadingRecipeReferences.value = false;
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('beforeunload', beforeWindowUnload);
|
||||
});
|
||||
|
||||
|
||||
// ===================
|
||||
// Methods
|
||||
// ===================
|
||||
|
@ -521,9 +535,7 @@ const fetchCategories = async () => {
|
|||
}
|
||||
isFetchingCategories.value = false;
|
||||
} catch (e) {
|
||||
await showSimpleAlertModal(
|
||||
t('cookbook', 'Failed to fetch categories'),
|
||||
);
|
||||
await showSimpleAlertModal(t('cookbook', 'Failed to fetch categories'));
|
||||
if (e && e instanceof Error) {
|
||||
throw e;
|
||||
}
|
||||
|
@ -546,9 +558,7 @@ const fetchKeywords = async () => {
|
|||
}
|
||||
isFetchingKeywords.value = false;
|
||||
} catch (e) {
|
||||
await showSimpleAlertModal(
|
||||
t('cookbook', 'Failed to fetch keywords'),
|
||||
);
|
||||
await showSimpleAlertModal(t('cookbook', 'Failed to fetch keywords'));
|
||||
if (e && e instanceof Error) {
|
||||
throw e;
|
||||
}
|
||||
|
@ -560,10 +570,7 @@ const loadRecipeData = async () => {
|
|||
store.dispatch('setLoadingRecipe', {
|
||||
recipe: -1,
|
||||
});
|
||||
} else if (
|
||||
store.state.recipe.id ===
|
||||
parseInt(route.params.id, 10)
|
||||
) {
|
||||
} else if (store.state.recipe.id === parseInt(route.params.id, 10)) {
|
||||
// Make the control row show that the recipe is reloading
|
||||
store.dispatch('setReloadingRecipe', {
|
||||
recipe: route.params.id,
|
||||
|
@ -575,12 +582,10 @@ const loadRecipeData = async () => {
|
|||
store.dispatch('setRecipe', { recipe });
|
||||
setup();
|
||||
} catch {
|
||||
await showSimpleAlertModal(
|
||||
t('cookbook', 'Loading recipe failed'),
|
||||
);
|
||||
await showSimpleAlertModal(t('cookbook', 'Loading recipe failed'));
|
||||
// Disable loading indicator
|
||||
if (store.state.loadingRecipe) {
|
||||
store.dispatch('setLoadingRecipe', { recipe: 0 })
|
||||
store.dispatch('setLoadingRecipe', { recipe: 0 });
|
||||
} else if ($store.state.reloadingRecipe) {
|
||||
store.dispatch('setReloadingRecipe', {
|
||||
recipe: 0,
|
||||
|
@ -643,7 +648,7 @@ const save = async () => {
|
|||
savingRecipe.value = false;
|
||||
}
|
||||
};
|
||||
const setup = async () =>{
|
||||
const setup = async () => {
|
||||
fetchCategories();
|
||||
fetchKeywords();
|
||||
if (route.params.id) {
|
||||
|
@ -687,7 +692,7 @@ const setup = async () =>{
|
|||
// fallback if fetching all keywords fails
|
||||
selectedKeywords.value.forEach((kw) => {
|
||||
if (!allKeywords.value.includes(kw)) {
|
||||
allKeywords.value.push(kw)
|
||||
allKeywords.value.push(kw);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -752,7 +757,6 @@ loadRecipeData();
|
|||
</script>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
// We can check if the user has browsed from the same recipe's view to this
|
||||
// edit and save some time by not reloading the recipe data, leading to a
|
||||
|
|
|
@ -32,12 +32,12 @@ const clicked = () => {
|
|||
if (!link.value.classList.contains('disabled')) {
|
||||
emit('keyword-clicked');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeKeyword'
|
||||
name: 'RecipeKeyword',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* @type {import('vue').Ref<boolean>}
|
||||
|
@ -23,13 +23,13 @@ import { ref } from "vue";
|
|||
const collapsed = ref(true);
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
this.collapsed = !this.collapsed
|
||||
this.collapsed = !this.collapsed;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeImages'
|
||||
name: 'RecipeImages',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const headerPrefix = "## ";
|
||||
const headerPrefix = '## ';
|
||||
|
||||
const props = defineProps({
|
||||
/* Ingredient HTML string to display. Content should be sanitized.
|
||||
*/
|
||||
ingredient: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
ingredientHasCorrectSyntax: {
|
||||
type: Boolean,
|
||||
|
@ -57,7 +57,7 @@ const toggleDone = () => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeIngredient'
|
||||
name: 'RecipeIngredient',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ defineProps({
|
|||
*/
|
||||
instruction: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -28,7 +28,7 @@ const toggleDone = () => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeInstruction'
|
||||
name: 'RecipeInstruction',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -65,11 +65,11 @@ li:hover::before {
|
|||
}
|
||||
|
||||
.done::before {
|
||||
content: "✔";
|
||||
content: '✔';
|
||||
}
|
||||
|
||||
li span,
|
||||
li input[type="checkbox"] {
|
||||
li input[type='checkbox'] {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: auto;
|
||||
|
|
|
@ -24,7 +24,7 @@ defineProps({
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeNutritionInfoItem'
|
||||
name: 'RecipeNutritionInfoItem',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ const props = defineProps({
|
|||
value: {
|
||||
type: Object,
|
||||
default() {
|
||||
return { hours: 0, minutes: 0 }
|
||||
return { hours: 0, minutes: 0 };
|
||||
},
|
||||
},
|
||||
label: {
|
||||
|
@ -71,9 +71,12 @@ const displayTime = computed(() => {
|
|||
});
|
||||
|
||||
// Watchers
|
||||
watch(() => props.value, () => {
|
||||
resetTimeDisplay();
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
resetTimeDisplay();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
resetTimeDisplay();
|
||||
|
@ -82,9 +85,7 @@ onMounted(() => {
|
|||
|
||||
// Source for the sound https://pixabay.com/sound-effects/alarm-clock-short-6402/
|
||||
// Voted by poll https://nextcloud.christian-wolf.click/nextcloud/apps/polls/s/Wke3s6CscDwQEjPV
|
||||
audio.value = new Audio(
|
||||
linkTo('cookbook', 'assets/alarm-continuous.mp3'),
|
||||
);
|
||||
audio.value = new Audio(linkTo('cookbook', 'assets/alarm-continuous.mp3'));
|
||||
|
||||
// For now, the alarm should play continuously until it is dismissed
|
||||
// See https://github.com/nextcloud/cookbook/issues/671#issuecomment-1279030452
|
||||
|
@ -164,7 +165,7 @@ const timerToggle = () => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeTimer'
|
||||
name: 'RecipeTimer',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ defineProps({
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeTool'
|
||||
name: 'RecipeTool',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
class="markdown-description"
|
||||
/>
|
||||
<p v-if="$store.state.recipe.url">
|
||||
<strong>{{ t("cookbook", "Source") }}: </strong
|
||||
<strong>{{ t('cookbook', 'Source') }}: </strong
|
||||
><a
|
||||
target="_blank"
|
||||
:href="$store.state.recipe.url"
|
||||
|
@ -65,7 +65,7 @@
|
|||
</p>
|
||||
<div>
|
||||
<p v-if="$store.state.recipe.recipeYield != null">
|
||||
<strong>{{ t("cookbook", "Servings") }}: </strong>
|
||||
<strong>{{ t('cookbook', 'Servings') }}: </strong>
|
||||
<span>
|
||||
<button
|
||||
:disabled="recipeYield === 1"
|
||||
|
@ -130,7 +130,7 @@
|
|||
<section class="container">
|
||||
<section class="ingredients">
|
||||
<h3 v-if="scaledIngredients.length" class="section-title">
|
||||
<span>{{ t("cookbook", "Ingredients") }}</span>
|
||||
<span>{{ t('cookbook', 'Ingredients') }}</span>
|
||||
<NcButton
|
||||
class="copy-ingredients"
|
||||
:type="'tertiary'"
|
||||
|
@ -178,7 +178,7 @@
|
|||
|
||||
<section v-if="visibleInfoBlocks.tools" class="tools">
|
||||
<h3 v-if="parsedTools.length">
|
||||
{{ t("cookbook", "Tools") }}
|
||||
{{ t('cookbook', 'Tools') }}
|
||||
</h3>
|
||||
<ul v-if="parsedTools.length">
|
||||
<RecipeTool
|
||||
|
@ -190,7 +190,7 @@
|
|||
</section>
|
||||
|
||||
<section v-if="showNutritionData" class="nutrition">
|
||||
<h3>{{ t("cookbook", "Nutrition Information") }}</h3>
|
||||
<h3>{{ t('cookbook', 'Nutrition Information') }}</h3>
|
||||
<ul class="nutrition-items">
|
||||
<recipe-nutrition-info-item
|
||||
v-if="
|
||||
|
@ -306,7 +306,7 @@
|
|||
</section>
|
||||
|
||||
<main v-if="parsedInstructions.length">
|
||||
<h3>{{ t("cookbook", "Instructions") }}</h3>
|
||||
<h3>{{ t('cookbook', 'Instructions') }}</h3>
|
||||
<ol class="instructions">
|
||||
<RecipeInstruction
|
||||
v-for="(instruction, idx) in parsedInstructions"
|
||||
|
@ -322,7 +322,11 @@
|
|||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, onMounted, ref, watch } from 'vue';
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router/composables';
|
||||
import {
|
||||
onBeforeRouteUpdate,
|
||||
useRoute,
|
||||
useRouter,
|
||||
} from 'vue-router/composables';
|
||||
|
||||
import api from 'cookbook/js/api-interface';
|
||||
import helpers from 'cookbook/js/helper';
|
||||
|
@ -330,7 +334,7 @@ import normalizeMarkdown from 'cookbook/js/title-rename';
|
|||
import { showSimpleAlertModal } from 'cookbook/js/modals';
|
||||
import { useStore } from '../../store';
|
||||
import emitter from '../../bus';
|
||||
import { parseDateTime } from "../../composables/dateTimeHandling";
|
||||
import { parseDateTime } from '../../composables/dateTimeHandling';
|
||||
import yieldCalculator from 'cookbook/js/yieldCalculator';
|
||||
|
||||
import ContentCopyIcon from 'icons/ContentCopy.vue';
|
||||
|
@ -350,7 +354,6 @@ const router = useRouter();
|
|||
const store = useStore();
|
||||
const log = getCurrentInstance().proxy.$log;
|
||||
|
||||
|
||||
/**
|
||||
* This is one tricky feature of Vue router. If different paths lead to
|
||||
* the same component (such as '/recipe/xxx' and '/recipe/yyy)',
|
||||
|
@ -426,9 +429,7 @@ const recipe = computed(() => {
|
|||
}
|
||||
|
||||
if (store.state.recipe.description) {
|
||||
recipe.description = helpers.escapeHTML(
|
||||
store.state.recipe.description,
|
||||
);
|
||||
recipe.description = helpers.escapeHTML(store.state.recipe.description);
|
||||
}
|
||||
|
||||
if (store.state.recipe.recipeIngredient) {
|
||||
|
@ -444,14 +445,11 @@ const recipe = computed(() => {
|
|||
}
|
||||
|
||||
if (store.state.recipe.keywords) {
|
||||
recipe.keywords = String(
|
||||
store.state.recipe.keywords,
|
||||
).split(',');
|
||||
recipe.keywords = String(store.state.recipe.keywords).split(',');
|
||||
}
|
||||
|
||||
if (store.state.recipe.cookTime) {
|
||||
const cookT =
|
||||
store.state.recipe.cookTime.match(/PT(\d+?)H(\d+?)M/);
|
||||
const cookT = store.state.recipe.cookTime.match(/PT(\d+?)H(\d+?)M/);
|
||||
const hh = parseInt(cookT[1], 10);
|
||||
const mm = parseInt(cookT[2], 10);
|
||||
if (hh > 0 || mm > 0) {
|
||||
|
@ -460,8 +458,7 @@ const recipe = computed(() => {
|
|||
}
|
||||
|
||||
if (store.state.recipe.prepTime) {
|
||||
const prepT =
|
||||
store.state.recipe.prepTime.match(/PT(\d+?)H(\d+?)M/);
|
||||
const prepT = store.state.recipe.prepTime.match(/PT(\d+?)H(\d+?)M/);
|
||||
const hh = parseInt(prepT[1], 10);
|
||||
const mm = parseInt(prepT[2], 10);
|
||||
if (hh > 0 || mm > 0) {
|
||||
|
@ -470,8 +467,7 @@ const recipe = computed(() => {
|
|||
}
|
||||
|
||||
if (store.state.recipe.totalTime) {
|
||||
const totalT =
|
||||
store.state.recipe.totalTime.match(/PT(\d+?)H(\d+?)M/);
|
||||
const totalT = store.state.recipe.totalTime.match(/PT(\d+?)H(\d+?)M/);
|
||||
const hh = parseInt(totalT[1], 10);
|
||||
const mm = parseInt(totalT[2], 10);
|
||||
if (hh > 0 || mm > 0) {
|
||||
|
@ -486,17 +482,13 @@ const recipe = computed(() => {
|
|||
}
|
||||
|
||||
if (store.state.recipe.dateCreated) {
|
||||
const date = parseDateTime(
|
||||
store.state.recipe.dateCreated,
|
||||
);
|
||||
const date = parseDateTime(store.state.recipe.dateCreated);
|
||||
recipe.dateCreated =
|
||||
date != null ? date.format('L, LT').toString() : null;
|
||||
}
|
||||
|
||||
if (store.state.recipe.dateModified) {
|
||||
const date = parseDateTime(
|
||||
store.state.recipe.dateModified,
|
||||
);
|
||||
const date = parseDateTime(store.state.recipe.dateModified);
|
||||
recipe.dateModified =
|
||||
date != null ? date.format('L, LT').toString() : null;
|
||||
}
|
||||
|
@ -517,11 +509,7 @@ const recipe = computed(() => {
|
|||
const recipeIngredientsHaveSubgroups = computed(() => {
|
||||
if (recipe.value.ingredients && recipe.value.ingredients.length > 0) {
|
||||
for (let idx = 0; idx < recipe.value.ingredients.length; ++idx) {
|
||||
if (
|
||||
recipe.value.ingredients[idx].startsWith(
|
||||
headerPrefix,
|
||||
)
|
||||
) {
|
||||
if (recipe.value.ingredients[idx].startsWith(headerPrefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -540,8 +528,7 @@ const showModifiedDate = computed(() => {
|
|||
return !(
|
||||
store.state.recipe.dateCreated &&
|
||||
store.state.recipe.dateModified &&
|
||||
store.state.recipe.dateCreated ===
|
||||
store.state.recipe.dateModified
|
||||
store.state.recipe.dateCreated === store.state.recipe.dateModified
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -567,9 +554,7 @@ const scaledIngredients = computed(() => {
|
|||
});
|
||||
|
||||
const ingredientsWithValidSyntax = computed(() => {
|
||||
return parsedIngredients.value.map(
|
||||
yieldCalculator.isValidIngredientSyntax,
|
||||
);
|
||||
return parsedIngredients.value.map(yieldCalculator.isValidIngredientSyntax);
|
||||
});
|
||||
|
||||
const ingredientsSyntaxCorrect = computed(() => {
|
||||
|
@ -577,82 +562,88 @@ const ingredientsSyntaxCorrect = computed(() => {
|
|||
});
|
||||
|
||||
// Watchers
|
||||
watch(() => recipe.value, (r) => {
|
||||
log.debug('Recipe has been updated');
|
||||
if (r) {
|
||||
log.debug('Recipe', r);
|
||||
watch(
|
||||
() => recipe.value,
|
||||
(r) => {
|
||||
log.debug('Recipe has been updated');
|
||||
if (r) {
|
||||
log.debug('Recipe', r);
|
||||
|
||||
if (r.description) {
|
||||
parsedDescription.value = t('cookbook', 'Loading…');
|
||||
normalizeMarkdown(r.description).then((x) => {
|
||||
parsedDescription.value = x;
|
||||
});
|
||||
} else {
|
||||
parsedDescription.value = '';
|
||||
if (r.description) {
|
||||
parsedDescription.value = t('cookbook', 'Loading…');
|
||||
normalizeMarkdown(r.description).then((x) => {
|
||||
parsedDescription.value = x;
|
||||
});
|
||||
} else {
|
||||
parsedDescription.value = '';
|
||||
}
|
||||
|
||||
if (r.ingredients) {
|
||||
parsedIngredients.value = r.ingredients.map(() =>
|
||||
t('cookbook', 'Loading…'),
|
||||
);
|
||||
r.ingredients.forEach((ingredient, idx) => {
|
||||
normalizeMarkdown(ingredient)
|
||||
.then((x) => {
|
||||
parsedIngredients.value.splice(idx, 1, x);
|
||||
})
|
||||
.catch((ex) => {
|
||||
log.error(ex);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
parsedIngredients.value = [];
|
||||
}
|
||||
|
||||
if (r.instructions) {
|
||||
parsedInstructions.value = r.instructions.map(() =>
|
||||
t('cookbook', 'Loading…'),
|
||||
);
|
||||
r.instructions.forEach((instruction, idx) => {
|
||||
normalizeMarkdown(instruction)
|
||||
.then((x) => {
|
||||
parsedInstructions.value.splice(idx, 1, x);
|
||||
})
|
||||
.catch((ex) => {
|
||||
log.error(ex);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
parsedInstructions.value = [];
|
||||
}
|
||||
|
||||
if (r.tools) {
|
||||
parsedTools.value = r.tools.map(() =>
|
||||
t('cookbook', 'Loading…'),
|
||||
);
|
||||
r.tools.forEach((tool, idx) => {
|
||||
normalizeMarkdown(tool)
|
||||
.then((x) => {
|
||||
parsedTools.value.splice(idx, 1, x);
|
||||
})
|
||||
.catch((ex) => {
|
||||
log.error(ex);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
parsedTools.value = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (r.ingredients) {
|
||||
parsedIngredients.value = r.ingredients.map(() =>
|
||||
t('cookbook', 'Loading…'),
|
||||
);
|
||||
r.ingredients.forEach((ingredient, idx) => {
|
||||
normalizeMarkdown(ingredient)
|
||||
.then((x) => {
|
||||
parsedIngredients.value.splice(idx, 1, x);
|
||||
})
|
||||
.catch((ex) => {
|
||||
log.error(ex);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
parsedIngredients.value = [];
|
||||
watch(
|
||||
() => recipeYield.value,
|
||||
() => {
|
||||
if (recipeYield.value < 0) {
|
||||
restoreOriginalRecipeYield();
|
||||
}
|
||||
|
||||
if (r.instructions) {
|
||||
parsedInstructions.value = r.instructions.map(() =>
|
||||
t('cookbook', 'Loading…'),
|
||||
);
|
||||
r.instructions.forEach((instruction, idx) => {
|
||||
normalizeMarkdown(instruction)
|
||||
.then((x) => {
|
||||
parsedInstructions.value.splice(idx, 1, x);
|
||||
})
|
||||
.catch((ex) => {
|
||||
log.error(ex);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
parsedInstructions.value = [];
|
||||
}
|
||||
|
||||
if (r.tools) {
|
||||
parsedTools.value = r.tools.map(() =>
|
||||
t('cookbook', 'Loading…'),
|
||||
);
|
||||
r.tools.forEach((tool, idx) => {
|
||||
normalizeMarkdown(tool)
|
||||
.then((x) => {
|
||||
parsedTools.value.splice(idx, 1, x);
|
||||
})
|
||||
.catch((ex) => {
|
||||
log.error(ex);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
parsedTools.value = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => recipeYield.value, () => {
|
||||
if (recipeYield.value < 0) {
|
||||
restoreOriginalRecipeYield();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Methods
|
||||
const isNullOrEmpty = (str) => {
|
||||
return !str || (typeof str === 'string' && str.trim().length === 0)
|
||||
return !str || (typeof str === 'string' && str.trim().length === 0);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -670,10 +661,7 @@ const setup = async () => {
|
|||
store.dispatch('setLoadingRecipe', { recipe: -1 });
|
||||
|
||||
// Make the control row show that the recipe is reloading
|
||||
} else if (
|
||||
store.state.recipe.id ===
|
||||
parseInt(route.params.id, 10)
|
||||
) {
|
||||
} else if (store.state.recipe.id === parseInt(route.params.id, 10)) {
|
||||
store.dispatch('setReloadingRecipe', {
|
||||
recipe: route.params.id,
|
||||
});
|
||||
|
@ -708,16 +696,14 @@ const setup = async () => {
|
|||
|
||||
store.dispatch('setPage', { page: 'recipe' });
|
||||
|
||||
await showSimpleAlertModal(
|
||||
t('cookbook', 'Loading recipe failed'),
|
||||
);
|
||||
await showSimpleAlertModal(t('cookbook', 'Loading recipe failed'));
|
||||
}
|
||||
|
||||
recipeYield.value = store.state.recipe.recipeYield;
|
||||
};
|
||||
|
||||
const changeRecipeYield = (increase = true) => {
|
||||
recipeYield.value = recipeYield.value + (increase ? 1 : -1)
|
||||
recipeYield.value = recipeYield.value + (increase ? 1 : -1);
|
||||
};
|
||||
|
||||
const copyIngredientsToClipboard = () => {
|
||||
|
@ -726,12 +712,8 @@ const copyIngredientsToClipboard = () => {
|
|||
if (navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(ingredientsToCopy)
|
||||
.then(() =>
|
||||
log.info('JSON array copied to clipboard'),
|
||||
)
|
||||
.catch((err) =>
|
||||
log.error('Failed to copy JSON array: ', err),
|
||||
);
|
||||
.then(() => log.info('JSON array copied to clipboard'))
|
||||
.catch((err) => log.error('Failed to copy JSON array: ', err));
|
||||
} else {
|
||||
// fallback solution
|
||||
const input = document.createElement('textarea');
|
||||
|
@ -762,7 +744,7 @@ const restoreOriginalRecipeYield = () => {
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecipeView'
|
||||
name: 'RecipeView',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -786,7 +768,7 @@ export default {
|
|||
}
|
||||
|
||||
.header a::after {
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -899,7 +881,7 @@ section {
|
|||
section::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@ -935,7 +917,7 @@ aside ul li {
|
|||
}
|
||||
|
||||
aside ul li span,
|
||||
aside ul li input[type="checkbox"] {
|
||||
aside ul li input[type='checkbox'] {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: auto;
|
||||
|
@ -1021,7 +1003,7 @@ main {
|
|||
}
|
||||
|
||||
.instructions .instruction.done::before {
|
||||
content: "✔";
|
||||
content: '✔';
|
||||
}
|
||||
|
||||
.content > .container {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<script setup>
|
||||
import { onActivated, onDeactivated, onMounted, ref } from 'vue';
|
||||
import { onBeforeRouteUpdate, useRoute } from "vue-router/composables";
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router/composables';
|
||||
import api from 'cookbook/js/api-interface';
|
||||
import helpers from 'cookbook/js/helper';
|
||||
import { showSimpleAlertModal } from 'cookbook/js/modals';
|
||||
|
@ -119,9 +119,7 @@ const setup = async () => {
|
|||
} else {
|
||||
// General search
|
||||
try {
|
||||
const response = await api.recipes.search(
|
||||
route.params.value,
|
||||
);
|
||||
const response = await api.recipes.search(route.params.value);
|
||||
results.value = response.data;
|
||||
} catch (e) {
|
||||
results.value = [];
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import lozad from 'lozad';
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -65,7 +65,7 @@ const fullImage = ref(null);
|
|||
const previewImage = ref(null);
|
||||
|
||||
const style = computed(() => {
|
||||
const style = {}
|
||||
const style = {};
|
||||
if (props.width) {
|
||||
style.width = `${props.width}px`;
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ onMounted(() => {
|
|||
enableAutoReload: true,
|
||||
load(el) {
|
||||
previewImage.value.addEventListener(
|
||||
"load",
|
||||
'load',
|
||||
onThumbnailPreviewLoaded,
|
||||
);
|
||||
previewImage.value.src = props.blurredPreviewSrc;
|
||||
|
@ -91,43 +91,39 @@ onMounted(() => {
|
|||
observer.observe();
|
||||
});
|
||||
|
||||
|
||||
// callback for fully-loaded image event
|
||||
const onThumbnailFullyLoaded = () => {
|
||||
fullImage.value.removeEventListener("load", onThumbnailFullyLoaded);
|
||||
fullImage.value.removeEventListener('load', onThumbnailFullyLoaded);
|
||||
pictureElement.value.removeChild(previewImage.value);
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// callback for preview-image-loaded event
|
||||
const onThumbnailPreviewLoaded = () => {
|
||||
// cleanup event listener on preview
|
||||
previewImage.value.removeEventListener(
|
||||
"load",
|
||||
onThumbnailPreviewLoaded,
|
||||
);
|
||||
previewImage.value.removeEventListener('load', onThumbnailPreviewLoaded);
|
||||
// add event listener for full-resolution image
|
||||
fullImage.value.addEventListener("load", onThumbnailFullyLoaded);
|
||||
fullImage.value.addEventListener('load', onThumbnailFullyLoaded);
|
||||
fullImage.value.src = props.lazySrc;
|
||||
isPreviewLoading.value = false;
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if(previewImage.value !== 'undefined' && previewImage.value != null){
|
||||
if (previewImage.value !== 'undefined' && previewImage.value != null) {
|
||||
previewImage.value.removeEventListener(
|
||||
"load",
|
||||
'load',
|
||||
onThumbnailPreviewLoaded,
|
||||
);
|
||||
}
|
||||
if(fullImage.value !== 'undefined' && fullImage.value != null) {
|
||||
fullImage.value.removeEventListener("load", onThumbnailFullyLoaded);
|
||||
if (fullImage.value !== 'undefined' && fullImage.value != null) {
|
||||
fullImage.value.removeEventListener('load', onThumbnailFullyLoaded);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LazyPicture'
|
||||
name: 'LazyPicture',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import moment from "@nextcloud/moment";
|
||||
import moment from '@nextcloud/moment';
|
||||
|
||||
/**
|
||||
* The schema.org standard requires the dates formatted as Date (https://schema.org/Date)
|
||||
|
@ -6,11 +6,11 @@ import moment from "@nextcloud/moment";
|
|||
* @param dt
|
||||
* @returns {null|moment.Moment|*}
|
||||
*/
|
||||
export function parseDateTime(dt) {
|
||||
export function parseDateTime(dt) {
|
||||
if (!dt) return null;
|
||||
const date = moment(dt, moment.ISO_8601);
|
||||
if (!date.isValid()) {
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,20 +22,21 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { readonly, ref } from 'vue'
|
||||
import { readonly, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* The minimal width of the viewport to be considered a desktop device
|
||||
*/
|
||||
export const MOBILE_BREAKPOINT = 1024
|
||||
export const MOBILE_BREAKPOINT = 1024;
|
||||
|
||||
const checkIfIsMobile = () => document.documentElement.clientWidth < MOBILE_BREAKPOINT
|
||||
const checkIfIsMobile = () =>
|
||||
document.documentElement.clientWidth < MOBILE_BREAKPOINT;
|
||||
|
||||
const isMobile = ref(checkIfIsMobile())
|
||||
const isMobile = ref(checkIfIsMobile());
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
isMobile.value = checkIfIsMobile()
|
||||
})
|
||||
isMobile.value = checkIfIsMobile();
|
||||
});
|
||||
|
||||
/**
|
||||
* Use global isMobile state, based on the viewport width
|
||||
|
@ -43,7 +44,7 @@ window.addEventListener('resize', () => {
|
|||
* @return {import('vue').DeepReadonly<import('vue').Ref<boolean>>}
|
||||
*/
|
||||
export function useIsMobile() {
|
||||
return readonly(isMobile)
|
||||
return readonly(isMobile);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,4 +52,4 @@ export function useIsMobile() {
|
|||
* Use `composables/useIsMobile` instead.
|
||||
* Defined and exported only for isMobile mixin.
|
||||
*/
|
||||
export const isMobileState = readonly(isMobile)
|
||||
export const isMobileState = readonly(isMobile);
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import {position as caretPosition} from "caret-pos";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { position as caretPosition } from 'caret-pos';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursorPosition, suggestionsData, buffer, emit, log, props) {
|
||||
export default function useSuggestionsPopup(
|
||||
suggestionsPopupElementA,
|
||||
lastCursorPosition,
|
||||
suggestionsData,
|
||||
buffer,
|
||||
emit,
|
||||
log,
|
||||
props,
|
||||
) {
|
||||
const clamp = (val, min, max) => Math.min(max, Math.max(min, val));
|
||||
|
||||
/**
|
||||
* Reference to the SuggestionsPopup DOM element.
|
||||
* @type {Ref<HTMLElement | null>}
|
||||
*/
|
||||
const suggestionsPopupElement = ref(null)
|
||||
const suggestionsPopupElement = ref(null);
|
||||
|
||||
/**
|
||||
* Cancel the suggestions popup by setting the data object to null
|
||||
|
@ -18,9 +26,7 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
};
|
||||
|
||||
const suggestionsPopupVisible = computed(() => {
|
||||
return (
|
||||
suggestionsData.value !== null && !suggestionsData.value.blurred
|
||||
);
|
||||
return suggestionsData.value !== null && !suggestionsData.value.blurred;
|
||||
});
|
||||
|
||||
const filteredSuggestionOptions = computed(() => {
|
||||
|
@ -28,10 +34,8 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
|
||||
return props.suggestionOptions.filter(
|
||||
(option) =>
|
||||
searchText === "" ||
|
||||
option.title
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()),
|
||||
searchText === '' ||
|
||||
option.title.toLowerCase().includes(searchText.toLowerCase()),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -41,7 +45,7 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
*/
|
||||
const handleSuggestionsPopupSelectedEvent = (opt) => {
|
||||
handleSuggestionsPopupSelected(opt.recipe_id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle something selected by click or by `Enter`
|
||||
|
@ -59,9 +63,9 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
|
||||
if (buffer?.value) {
|
||||
buffer.value[suggestionsData.value.fieldIndex] = newValue;
|
||||
emit("input", buffer.value);
|
||||
emit('input', buffer.value);
|
||||
} else {
|
||||
emit("input", newValue);
|
||||
emit('input', newValue);
|
||||
}
|
||||
handleSuggestionsPopupCancel();
|
||||
// set cursor to position after pasted string. Waiting two ticks is necessary for
|
||||
|
@ -82,26 +86,26 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
const handleSuggestionsPopupOpenKeyUp = (e, cursorPos) => {
|
||||
const caretPos = caretPosition(e.target, {
|
||||
customPos: suggestionsData.value.hashPosition - 1,
|
||||
})
|
||||
});
|
||||
|
||||
// Only update the popover position if the line changes (caret pos y changes)
|
||||
if (caretPos.top !== suggestionsData.value.caretPos.top) {
|
||||
suggestionsData.value.caretPos = caretPos
|
||||
suggestionsData.value.caretPos = caretPos;
|
||||
}
|
||||
|
||||
// Cancel suggestion popup on whitespace or caret movement
|
||||
if (
|
||||
[
|
||||
" ",
|
||||
"\t",
|
||||
"#",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"Home",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Escape",
|
||||
' ',
|
||||
'\t',
|
||||
'#',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'Home',
|
||||
'End',
|
||||
'PageUp',
|
||||
'PageDown',
|
||||
'Escape',
|
||||
].includes(e.key)
|
||||
) {
|
||||
handleSuggestionsPopupCancel();
|
||||
|
@ -126,8 +130,8 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
};
|
||||
|
||||
const getClosestListItemIndex = (field) => {
|
||||
const $li = field.closest("li");
|
||||
const $ul = $li?.closest("ul");
|
||||
const $li = field.closest('li');
|
||||
const $ul = $li?.closest('ul');
|
||||
if (!$ul) return null;
|
||||
|
||||
return Array.prototype.indexOf.call($ul.childNodes, $li);
|
||||
|
@ -149,24 +153,21 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
}
|
||||
|
||||
// Only do anything for # key
|
||||
if (e.key !== "#") return;
|
||||
if (e.key !== '#') return;
|
||||
|
||||
// Show the popup only if the # was inserted at the very
|
||||
// beginning of the input or after any whitespace character
|
||||
if (
|
||||
!(
|
||||
cursorPos === 1 ||
|
||||
/\s/.test(field.value.charAt(cursorPos - 2))
|
||||
)
|
||||
!(cursorPos === 1 || /\s/.test(field.value.charAt(cursorPos - 2)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show dialog to select recipe
|
||||
const caretPos = caretPosition(field, { customPos: cursorPos - 1 })
|
||||
const caretPos = caretPosition(field, { customPos: cursorPos - 1 });
|
||||
suggestionsData.value = {
|
||||
field,
|
||||
searchText: "",
|
||||
searchText: '',
|
||||
caretPos,
|
||||
focusIndex: 0,
|
||||
hashPosition: cursorPos,
|
||||
|
@ -190,17 +191,17 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
*/
|
||||
const handleSuggestionsPopupOpenKeyDown = (e) => {
|
||||
// Handle switching the focused option with up/down keys
|
||||
if (["ArrowUp", "ArrowDown"].includes(e.key)) {
|
||||
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
|
||||
// Increment/decrement focuse index based on which key was pressed
|
||||
// and constrain between 0 and length - 1
|
||||
const focusIndex = clamp(
|
||||
suggestionsData.value.focusIndex +
|
||||
{
|
||||
ArrowUp: -1,
|
||||
ArrowDown: +1,
|
||||
}[e.key],
|
||||
{
|
||||
ArrowUp: -1,
|
||||
ArrowDown: +1,
|
||||
}[e.key],
|
||||
0,
|
||||
props.suggestionOptions.length - 1,
|
||||
);
|
||||
|
@ -212,7 +213,7 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
}
|
||||
|
||||
// Handle selecting the current option when enter is pressed
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const { focusIndex } = suggestionsData.value;
|
||||
const selection = filteredSuggestionOptions.value[focusIndex];
|
||||
|
@ -224,9 +225,9 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
* Recover suggestions popup on focus
|
||||
*/
|
||||
const handleSuggestionsPopupFocus = (e) => {
|
||||
log.debug("focus", e, JSON.stringify(suggestionsData.value))
|
||||
log.debug('focus', e, JSON.stringify(suggestionsData.value));
|
||||
if (suggestionsData.value?.blurred) {
|
||||
suggestionsData.value.blurred = false
|
||||
suggestionsData.value.blurred = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -234,7 +235,7 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
* Cancel selection if input gets blurred
|
||||
*/
|
||||
const handleSuggestionsPopupBlur = (e) => {
|
||||
log.debug("blur", e, JSON.stringify(suggestionsData.value))
|
||||
log.debug('blur', e, JSON.stringify(suggestionsData.value));
|
||||
if (!suggestionsPopupVisible.value || !suggestionsPopupElement.value) {
|
||||
return;
|
||||
}
|
||||
|
@ -281,6 +282,6 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
|
|||
handleSuggestionsPopupFocus,
|
||||
handleSuggestionsPopupBlur,
|
||||
handleSuggestionsPopupMouseUp,
|
||||
handleSuggestionsPopupSelectedEvent
|
||||
handleSuggestionsPopupSelectedEvent,
|
||||
};
|
||||
}
|
||||
|
|
18
src/guest.js
18
src/guest.js
|
@ -5,33 +5,33 @@
|
|||
* @license AGPL3 or later
|
||||
*/
|
||||
|
||||
import Vue from "vue"
|
||||
import Vue from 'vue';
|
||||
|
||||
import { useStore } from "./store"
|
||||
import { useStore } from './store';
|
||||
|
||||
import AppInvalidGuest from "./components/AppInvalidGuest.vue"
|
||||
import AppInvalidGuest from './components/AppInvalidGuest.vue';
|
||||
|
||||
// eslint-disable-next-line camelcase,no-undef
|
||||
if (__webpack_use_dev_server__ || false) {
|
||||
// eslint-disable-next-line camelcase,no-undef
|
||||
__webpack_public_path__ = "http://127.0.0.1:3000/apps/cookbook/js/"
|
||||
__webpack_public_path__ = 'http://127.0.0.1:3000/apps/cookbook/js/';
|
||||
}
|
||||
|
||||
// Fetch Nextcloud nonce identifier for dynamic script loading
|
||||
// eslint-disable-next-line camelcase,no-undef
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
__webpack_nonce__ = btoa(OC.requestToken);
|
||||
|
||||
// Also make the injections available in Vue components
|
||||
Vue.prototype.OC = OC
|
||||
Vue.prototype.OC = OC;
|
||||
|
||||
// Pass translation engine to Vue
|
||||
Vue.prototype.t = window.t
|
||||
Vue.prototype.t = window.t;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
// Start the app once document is done loading
|
||||
const App = Vue.extend(AppInvalidGuest)
|
||||
const App = Vue.extend(AppInvalidGuest);
|
||||
new App({
|
||||
store,
|
||||
// router,
|
||||
}).$mount("#content")
|
||||
}).$mount('#content');
|
||||
|
|
|
@ -1,115 +1,115 @@
|
|||
import Vue from "vue"
|
||||
import axios from "@nextcloud/axios"
|
||||
import Vue from 'vue';
|
||||
import axios from '@nextcloud/axios';
|
||||
|
||||
import { generateUrl } from "@nextcloud/router"
|
||||
import { generateUrl } from '@nextcloud/router';
|
||||
|
||||
const instance = axios.create()
|
||||
const instance = axios.create();
|
||||
|
||||
const baseUrl = `${generateUrl("apps/cookbook")}/webapp`
|
||||
const baseUrl = `${generateUrl('apps/cookbook')}/webapp`;
|
||||
|
||||
// Add a debug log for every request
|
||||
instance.interceptors.request.use((config) => {
|
||||
Vue.$log.debug(
|
||||
`[axios] Making "${config.method}" request to "${config.url}"`,
|
||||
config,
|
||||
)
|
||||
const contentType = config.headers["Content-Type"]
|
||||
);
|
||||
const contentType = config.headers['Content-Type'];
|
||||
if (
|
||||
contentType &&
|
||||
!["application/json", "text/json"].includes(contentType)
|
||||
!['application/json', 'text/json'].includes(contentType)
|
||||
) {
|
||||
Vue.$log.warn(
|
||||
`[axios] Request to "${config.url}" is using Content-Type "${contentType}", not JSON`,
|
||||
)
|
||||
);
|
||||
}
|
||||
return config
|
||||
})
|
||||
return config;
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
Vue.$log.debug("[axios] Received response", response)
|
||||
return response
|
||||
Vue.$log.debug('[axios] Received response', response);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
Vue.$log.warn("[axios] Received error", error)
|
||||
return Promise.reject(error)
|
||||
Vue.$log.warn('[axios] Received error', error);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
axios.defaults.headers.common.Accept = "application/json"
|
||||
axios.defaults.headers.common.Accept = 'application/json';
|
||||
|
||||
function createNewRecipe(recipe) {
|
||||
return instance.post(`${baseUrl}/recipes`, recipe)
|
||||
return instance.post(`${baseUrl}/recipes`, recipe);
|
||||
}
|
||||
|
||||
function getRecipe(id) {
|
||||
return instance.get(`${baseUrl}/recipes/${id}`)
|
||||
return instance.get(`${baseUrl}/recipes/${id}`);
|
||||
}
|
||||
|
||||
function getAllRecipes() {
|
||||
return instance.get(`${baseUrl}/recipes`)
|
||||
return instance.get(`${baseUrl}/recipes`);
|
||||
}
|
||||
|
||||
function getAllRecipesOfCategory(categoryName) {
|
||||
return instance.get(`${baseUrl}/category/${categoryName}`)
|
||||
return instance.get(`${baseUrl}/category/${categoryName}`);
|
||||
}
|
||||
|
||||
function getAllRecipesWithTag(tags) {
|
||||
return instance.get(`${baseUrl}/tags/${tags}`)
|
||||
return instance.get(`${baseUrl}/tags/${tags}`);
|
||||
}
|
||||
|
||||
function searchRecipes(search) {
|
||||
return instance.get(`${baseUrl}/search/${search}`)
|
||||
return instance.get(`${baseUrl}/search/${search}`);
|
||||
}
|
||||
|
||||
function updateRecipe(id, recipe) {
|
||||
return instance.put(`${baseUrl}/recipes/${id}`, recipe)
|
||||
return instance.put(`${baseUrl}/recipes/${id}`, recipe);
|
||||
}
|
||||
|
||||
function deleteRecipe(id) {
|
||||
return instance.delete(`${baseUrl}/recipes/${id}`)
|
||||
return instance.delete(`${baseUrl}/recipes/${id}`);
|
||||
}
|
||||
|
||||
function importRecipe(url) {
|
||||
return instance.post(`${baseUrl}/import`, `url=${url}`)
|
||||
return instance.post(`${baseUrl}/import`, `url=${url}`);
|
||||
}
|
||||
|
||||
function getAllCategories() {
|
||||
return instance.get(`${baseUrl}/categories`)
|
||||
return instance.get(`${baseUrl}/categories`);
|
||||
}
|
||||
|
||||
function updateCategoryName(oldName, newName) {
|
||||
return instance.put(`${baseUrl}/category/${encodeURIComponent(oldName)}`, {
|
||||
name: newName,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getAllKeywords() {
|
||||
return instance.get(`${baseUrl}/keywords`)
|
||||
return instance.get(`${baseUrl}/keywords`);
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
return instance.get(`${baseUrl}/config`)
|
||||
return instance.get(`${baseUrl}/config`);
|
||||
}
|
||||
|
||||
function updatePrintImageSetting(enabled) {
|
||||
return instance.post(`${baseUrl}/config`, { print_image: enabled ? 1 : 0 })
|
||||
return instance.post(`${baseUrl}/config`, { print_image: enabled ? 1 : 0 });
|
||||
}
|
||||
|
||||
function updateUpdateInterval(newInterval) {
|
||||
return instance.post(`${baseUrl}/config`, { update_interval: newInterval })
|
||||
return instance.post(`${baseUrl}/config`, { update_interval: newInterval });
|
||||
}
|
||||
|
||||
function updateRecipeDirectory(newDir) {
|
||||
return instance.post(`${baseUrl}/config`, { folder: newDir })
|
||||
return instance.post(`${baseUrl}/config`, { folder: newDir });
|
||||
}
|
||||
|
||||
function updateVisibleInfoBlocks(visibleInfoBlocks) {
|
||||
return instance.post(`${baseUrl}/config`, { visibleInfoBlocks })
|
||||
return instance.post(`${baseUrl}/config`, { visibleInfoBlocks });
|
||||
}
|
||||
|
||||
function reindex() {
|
||||
return instance.post(`${baseUrl}/reindex`)
|
||||
return instance.post(`${baseUrl}/reindex`);
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@ -147,4 +147,4 @@ export default {
|
|||
update: updateVisibleInfoBlocks,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { showSimpleAlertModal } from "cookbook/js/modals"
|
||||
import { showSimpleAlertModal } from 'cookbook/js/modals';
|
||||
|
||||
// Check if two routes point to the same component but have different content
|
||||
function shouldReloadContent(url1, url2) {
|
||||
if (url1 === url2) {
|
||||
return false // Obviously should not if both routes are the same
|
||||
return false; // Obviously should not if both routes are the same
|
||||
}
|
||||
|
||||
const comps1 = url1.split("/")
|
||||
const comps2 = url2.split("/")
|
||||
const comps1 = url1.split('/');
|
||||
const comps2 = url2.split('/');
|
||||
|
||||
if (comps1.length < 2 || comps2.length < 2) {
|
||||
return false // Just a failsafe, this should never happen
|
||||
return false; // Just a failsafe, this should never happen
|
||||
}
|
||||
|
||||
// The route structure is as follows:
|
||||
|
@ -20,47 +20,47 @@ function shouldReloadContent(url1, url2) {
|
|||
// If the items are different, then the router automatically handles
|
||||
// component loading: do not manually reload
|
||||
if (comps1[1] !== comps2[1]) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// If one of the routes is edit and the other is not
|
||||
if (comps1.length !== comps2.length) {
|
||||
// Only reload if changing from edit to create
|
||||
return comps1.pop() === "create" || comps2.pop() === "create"
|
||||
return comps1.pop() === 'create' || comps2.pop() === 'create';
|
||||
}
|
||||
if (comps1.pop() === "create") {
|
||||
if (comps1.pop() === 'create') {
|
||||
// But, if we are moving from create to view, do not reload
|
||||
// the create component
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only options left are that both of the routes are edit or view,
|
||||
// but not identical, or that we're moving from view to create
|
||||
// -> reload view
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the two urls point to the same item instance
|
||||
function isSameItemInstance(url1, url2) {
|
||||
if (url1 === url2) {
|
||||
return true // Obviously true if the routes are the same
|
||||
return true; // Obviously true if the routes are the same
|
||||
}
|
||||
const comps1 = url1.split("/")
|
||||
const comps2 = url2.split("/")
|
||||
const comps1 = url1.split('/');
|
||||
const comps2 = url2.split('/');
|
||||
if (comps1.length < 2 || comps2.length < 2) {
|
||||
return false // Just a failsafe, this should never happen
|
||||
return false; // Just a failsafe, this should never happen
|
||||
}
|
||||
// If the items are different, then the item instance cannot be
|
||||
// the same either
|
||||
if (comps1[1] !== comps2[1]) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
if (comps1.length < 3 || comps2.length < 3) {
|
||||
// ID is the third url component, so can't be the same instance if
|
||||
// either of the urls have less than three components
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
return comps1[2] === comps2[2]
|
||||
return comps1[2] === comps2[2];
|
||||
}
|
||||
|
||||
// A simple function to sanitize HTML tags
|
||||
|
@ -69,13 +69,13 @@ function escapeHTML(text) {
|
|||
/["&'<>]/g,
|
||||
(a) =>
|
||||
({
|
||||
"&": "&",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
})[a],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Fix the decimal separator for languages that use a comma instead of dot
|
||||
|
@ -84,68 +84,68 @@ function fixDecimalSeparator(value, io) {
|
|||
// value is the string value of the number to process
|
||||
// io is either 'i' as in input or 'o' as in output
|
||||
if (!value) {
|
||||
return ""
|
||||
return '';
|
||||
}
|
||||
if (io === "i") {
|
||||
if (io === 'i') {
|
||||
// Check if it's an American number where a comma precedes a dot
|
||||
// e.g. 12,500.25
|
||||
if (value.indexOf(".") > value.indexOf(",")) {
|
||||
return value.replace(",", "")
|
||||
if (value.indexOf('.') > value.indexOf(',')) {
|
||||
return value.replace(',', '');
|
||||
}
|
||||
return value.replace(",", ".")
|
||||
return value.replace(',', '.');
|
||||
}
|
||||
if (io === "o") {
|
||||
return value.toString().replace(".", ",")
|
||||
if (io === 'o') {
|
||||
return value.toString().replace('.', ',');
|
||||
}
|
||||
return ""
|
||||
return '';
|
||||
}
|
||||
|
||||
// This will replace the PHP function nl2br in Vue components
|
||||
// deprecated
|
||||
function nl2br(text) {
|
||||
return text.replace(/\n/g, "<br />")
|
||||
return text.replace(/\n/g, '<br />');
|
||||
}
|
||||
|
||||
// A simple function that converts a MySQL datetime into a timestamp.
|
||||
// deprecated
|
||||
function getTimestamp(date) {
|
||||
if (date) {
|
||||
return new Date(date)
|
||||
return new Date(date);
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
let router
|
||||
let router;
|
||||
|
||||
function useRouter(_router) {
|
||||
router = _router
|
||||
router = _router;
|
||||
}
|
||||
// Push a new URL to the router, essentially navigating to that page.
|
||||
function goTo(url) {
|
||||
router.push(url)
|
||||
router.push(url);
|
||||
}
|
||||
|
||||
// Notify the user if notifications are allowed
|
||||
// deprecated
|
||||
function notify(title, options) {
|
||||
if (!("Notification" in window)) {
|
||||
return
|
||||
if (!('Notification' in window)) {
|
||||
return;
|
||||
}
|
||||
if (Notification.permission === "granted") {
|
||||
if (Notification.permission === 'granted') {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const notification = new Notification(title, options)
|
||||
} else if (Notification.permission !== "denied") {
|
||||
const notification = new Notification(title, options);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission((permission) => {
|
||||
if (!("permission" in Notification)) {
|
||||
Notification.permission = permission
|
||||
if (!('permission' in Notification)) {
|
||||
Notification.permission = permission;
|
||||
}
|
||||
if (permission === "granted") {
|
||||
if (permission === 'granted') {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const notification = new Notification(title, options)
|
||||
const notification = new Notification(title, options);
|
||||
} else {
|
||||
showSimpleAlertModal(title)
|
||||
showSimpleAlertModal(title);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,4 +159,4 @@ export default {
|
|||
useRouter,
|
||||
goTo,
|
||||
notify,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
// TODO: Switch to vuejs3-logger when we switch to Vue 3
|
||||
import VueLogger from "vuejs-logger"
|
||||
import moment from "@nextcloud/moment"
|
||||
import VueLogger from 'vuejs-logger';
|
||||
import moment from '@nextcloud/moment';
|
||||
|
||||
const DEFAULT_LOG_LEVEL = "info"
|
||||
const DEFAULT_LOG_LEVEL = 'info';
|
||||
// How many minutes the logging configuration is valid for
|
||||
const EXPIRY_MINUTES = 30
|
||||
const EXPIRY_MINUTES = 30;
|
||||
// localStorage keys
|
||||
const KEY_ENABLED = "COOKBOOK_LOGGING_ENABLED"
|
||||
const KEY_EXPIRY = "COOKBOOK_LOGGING_EXPIRY"
|
||||
const KEY_LOG_LEVEL = "COOKBOOK_LOGGING_LEVEL"
|
||||
const KEY_ENABLED = 'COOKBOOK_LOGGING_ENABLED';
|
||||
const KEY_EXPIRY = 'COOKBOOK_LOGGING_EXPIRY';
|
||||
const KEY_LOG_LEVEL = 'COOKBOOK_LOGGING_LEVEL';
|
||||
|
||||
// Check if the logging configuration in local storage has expired
|
||||
//
|
||||
|
@ -19,40 +19,40 @@ const KEY_LOG_LEVEL = "COOKBOOK_LOGGING_LEVEL"
|
|||
// logging. We don't want them to have to setup the expiry as well
|
||||
const isExpired = (timestamp) => {
|
||||
if (timestamp === null) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
return moment().isAfter(parseInt(timestamp, 10))
|
||||
}
|
||||
return moment().isAfter(parseInt(timestamp, 10));
|
||||
};
|
||||
|
||||
const isEnabled = () => {
|
||||
const DEFAULT = false
|
||||
const userValue = localStorage.getItem(KEY_ENABLED)
|
||||
const expiry = localStorage.getItem(KEY_EXPIRY)
|
||||
const DEFAULT = false;
|
||||
const userValue = localStorage.getItem(KEY_ENABLED);
|
||||
const expiry = localStorage.getItem(KEY_EXPIRY);
|
||||
|
||||
// Detect the first load after the user has enabled logging
|
||||
// Set the expiry so the logging isn't enabled forever
|
||||
if (userValue !== null && expiry === null) {
|
||||
localStorage.setItem(
|
||||
KEY_EXPIRY,
|
||||
moment().add(EXPIRY_MINUTES, "m").valueOf(),
|
||||
)
|
||||
moment().add(EXPIRY_MINUTES, 'm').valueOf(),
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpired(expiry)) {
|
||||
localStorage.removeItem(KEY_ENABLED)
|
||||
localStorage.removeItem(KEY_EXPIRY)
|
||||
localStorage.removeItem(KEY_ENABLED);
|
||||
localStorage.removeItem(KEY_EXPIRY);
|
||||
|
||||
return DEFAULT
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
// Local storage converts everything to string
|
||||
// Use JSON.parse to transform "false" -> false
|
||||
return JSON.parse(userValue) ?? DEFAULT
|
||||
}
|
||||
return JSON.parse(userValue) ?? DEFAULT;
|
||||
};
|
||||
|
||||
export default function setupLogging(Vue) {
|
||||
const logLevel = localStorage.getItem(KEY_LOG_LEVEL) ?? DEFAULT_LOG_LEVEL
|
||||
const logLevel = localStorage.getItem(KEY_LOG_LEVEL) ?? DEFAULT_LOG_LEVEL;
|
||||
|
||||
Vue.use(VueLogger, {
|
||||
isEnabled: isEnabled(),
|
||||
|
@ -60,13 +60,13 @@ export default function setupLogging(Vue) {
|
|||
stringifyArguments: false,
|
||||
showLogLevel: true,
|
||||
showMethodName: true,
|
||||
separator: "|",
|
||||
separator: '|',
|
||||
showConsoleColors: true,
|
||||
})
|
||||
});
|
||||
|
||||
Vue.$log.info(`Setting up logging with log level ${logLevel}`)
|
||||
Vue.$log.info(`Setting up logging with log level ${logLevel}`);
|
||||
}
|
||||
|
||||
export function enableLogging() {
|
||||
localStorage.setItem(KEY_ENABLED, true)
|
||||
localStorage.setItem(KEY_ENABLED, true);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { create } from "vue-modal-dialogs"
|
||||
import { create } from 'vue-modal-dialogs';
|
||||
|
||||
import SimpleAlertModal from "../components/Modals/SimpleAlertModal.vue"
|
||||
import SimpleConfirmModal from "../components/Modals/SimpleConfirmModal.vue"
|
||||
import SimpleAlertModal from '../components/Modals/SimpleAlertModal.vue';
|
||||
import SimpleConfirmModal from '../components/Modals/SimpleConfirmModal.vue';
|
||||
|
||||
export const showSimpleAlertModal = create(SimpleAlertModal, "content", "title")
|
||||
export const showSimpleAlertModal = create(
|
||||
SimpleAlertModal,
|
||||
'content',
|
||||
'title',
|
||||
);
|
||||
export const showSimpleConfirmModal = create(
|
||||
SimpleConfirmModal,
|
||||
"content",
|
||||
"title",
|
||||
)
|
||||
'content',
|
||||
'title',
|
||||
);
|
||||
|
|
|
@ -1,85 +1,85 @@
|
|||
import api from "cookbook/js/api-interface"
|
||||
import api from 'cookbook/js/api-interface';
|
||||
|
||||
import { generateUrl } from "@nextcloud/router"
|
||||
import { generateUrl } from '@nextcloud/router';
|
||||
|
||||
const baseUrl = generateUrl("apps/cookbook")
|
||||
const baseUrl = generateUrl('apps/cookbook');
|
||||
|
||||
function extractAllRecipeLinkIds(content) {
|
||||
const re = /(?:^|[^#])#r\/([0-9]+)/g
|
||||
let ret = []
|
||||
let matches
|
||||
const re = /(?:^|[^#])#r\/([0-9]+)/g;
|
||||
let ret = [];
|
||||
let matches;
|
||||
for (
|
||||
matches = re.exec(content);
|
||||
matches !== null;
|
||||
matches = re.exec(content)
|
||||
) {
|
||||
ret.push(matches[1])
|
||||
ret.push(matches[1]);
|
||||
}
|
||||
|
||||
// Make the ids unique, see https://stackoverflow.com/a/14438954/882756
|
||||
function onlyUnique(value, index, self) {
|
||||
return self.indexOf(value) === index
|
||||
return self.indexOf(value) === index;
|
||||
}
|
||||
ret = ret.filter(onlyUnique)
|
||||
ret = ret.filter(onlyUnique);
|
||||
|
||||
return ret
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function getRecipesFromLinks(linkIds) {
|
||||
return Promise.all(
|
||||
linkIds.map(async (x) => {
|
||||
let recipe
|
||||
let recipe;
|
||||
try {
|
||||
recipe = await api.recipes.get(x)
|
||||
recipe = await api.recipes.get(x);
|
||||
} catch (ex) {
|
||||
recipe = null
|
||||
recipe = null;
|
||||
}
|
||||
return recipe
|
||||
return recipe;
|
||||
}),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function cleanUpRecipeList(recipes) {
|
||||
return recipes.filter((r) => r !== null).map((x) => x.data)
|
||||
return recipes.filter((r) => r !== null).map((x) => x.data);
|
||||
}
|
||||
|
||||
function getRecipeUrl(id) {
|
||||
return `${baseUrl}/#/recipe/${id}`
|
||||
return `${baseUrl}/#/recipe/${id}`;
|
||||
}
|
||||
|
||||
function insertMarkdownLinks(content, recipes) {
|
||||
let ret = content
|
||||
let ret = content;
|
||||
recipes.forEach((r) => {
|
||||
const { id } = r
|
||||
const { id } = r;
|
||||
|
||||
// Replace link urls in dedicated links (like [this example](#r/123))
|
||||
ret = ret.replace(`](${id})`, `](${getRecipeUrl(id)})`)
|
||||
ret = ret.replace(`](${id})`, `](${getRecipeUrl(id)})`);
|
||||
|
||||
// Replace plain references with recipe name
|
||||
const rePlain = RegExp(
|
||||
`(^|\\s|[,._+&?!-])#r/${id}($|\\s|[,._+&?!-])`,
|
||||
"g",
|
||||
)
|
||||
'g',
|
||||
);
|
||||
// const re = /(^|\s|[,._+&?!-])#r\/(\d+)(?=$|\s|[.,_+&?!-])/g
|
||||
ret = ret.replace(
|
||||
rePlain,
|
||||
`$1[${r.name} (\\#r/${id})](${getRecipeUrl(id)})$2`,
|
||||
)
|
||||
})
|
||||
return ret
|
||||
);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function normalizeNamesMarkdown(content) {
|
||||
// console.log(`Content: ${content}`)
|
||||
const linkIds = extractAllRecipeLinkIds(content)
|
||||
let recipes = await getRecipesFromLinks(linkIds)
|
||||
recipes = cleanUpRecipeList(recipes)
|
||||
const linkIds = extractAllRecipeLinkIds(content);
|
||||
let recipes = await getRecipesFromLinks(linkIds);
|
||||
recipes = cleanUpRecipeList(recipes);
|
||||
// console.log("List of recipes", recipes)
|
||||
|
||||
const markdown = insertMarkdownLinks(content, recipes)
|
||||
const markdown = insertMarkdownLinks(content, recipes);
|
||||
// console.log("Formatted markdown:", markdown)
|
||||
|
||||
return markdown
|
||||
return markdown;
|
||||
}
|
||||
|
||||
export default normalizeNamesMarkdown
|
||||
export default normalizeNamesMarkdown;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
The ingredientFractionRegExp is used to identify fractions in the string.
|
||||
This is used to exclude strings that contain fractions from being valid.
|
||||
*/
|
||||
const fractionRegExp = /^((\d+\s+)?(\d+)\s*\/\s*(\d+)).*/
|
||||
const fractionRegExp = /^((\d+\s+)?(\d+)\s*\/\s*(\d+)).*/;
|
||||
|
||||
function isValidIngredientSyntax(ingredient) {
|
||||
/*
|
||||
|
@ -10,29 +10,29 @@ function isValidIngredientSyntax(ingredient) {
|
|||
possibly followed by a fractional part or a fraction. Then there should be a space
|
||||
and then any sequence of characters.
|
||||
*/
|
||||
const ingredientSyntaxRegExp = /^(?:\d+(?:\.\d+)?(?:\/\d+)?)\s?.*$/
|
||||
const ingredientSyntaxRegExp = /^(?:\d+(?:\.\d+)?(?:\/\d+)?)\s?.*$/;
|
||||
|
||||
/*
|
||||
The ingredientMultipleSeperatorsRegExp is used to check whether the string contains
|
||||
more than one separators (.,) after a number. This is used to exclude strings that
|
||||
contain more than one separator from being valid.
|
||||
*/
|
||||
const ingredientMultipleSeperatorsRegExp = /^-?\d+(?:[.,]\d+){2,}.*/
|
||||
const ingredientMultipleSeperatorsRegExp = /^-?\d+(?:[.,]\d+){2,}.*/;
|
||||
|
||||
return (
|
||||
fractionRegExp.test(ingredient) ||
|
||||
(ingredientSyntaxRegExp.test(ingredient) &&
|
||||
!ingredientMultipleSeperatorsRegExp.test(ingredient))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isIngredientsArrayValid(ingredients) {
|
||||
return ingredients.every(isValidIngredientSyntax)
|
||||
return ingredients.every(isValidIngredientSyntax);
|
||||
}
|
||||
|
||||
function recalculateIngredients(ingredients, currentYield, originalYield) {
|
||||
return ingredients.map((ingredient) => {
|
||||
const matches = ingredient.match(fractionRegExp)
|
||||
const matches = ingredient.match(fractionRegExp);
|
||||
|
||||
if (matches) {
|
||||
const [
|
||||
|
@ -41,42 +41,42 @@ function recalculateIngredients(ingredients, currentYield, originalYield) {
|
|||
wholeNumberPartRaw,
|
||||
numeratorRaw,
|
||||
denominatorRaw,
|
||||
] = matches
|
||||
] = matches;
|
||||
const wholeNumberPart = wholeNumberPartRaw
|
||||
? parseInt(wholeNumberPartRaw, 10)
|
||||
: 0
|
||||
const numerator = parseInt(numeratorRaw, 10)
|
||||
const denominator = parseInt(denominatorRaw, 10)
|
||||
: 0;
|
||||
const numerator = parseInt(numeratorRaw, 10);
|
||||
const denominator = parseInt(denominatorRaw, 10);
|
||||
|
||||
const decimalAmount = wholeNumberPart + numerator / denominator
|
||||
let newAmount = (decimalAmount / originalYield) * currentYield
|
||||
newAmount = newAmount.toFixed(2).replace(/[.]00$/, "")
|
||||
const decimalAmount = wholeNumberPart + numerator / denominator;
|
||||
let newAmount = (decimalAmount / originalYield) * currentYield;
|
||||
newAmount = newAmount.toFixed(2).replace(/[.]00$/, '');
|
||||
|
||||
const newIngredient = ingredient.replace(fractionMatch, newAmount)
|
||||
return newIngredient
|
||||
const newIngredient = ingredient.replace(fractionMatch, newAmount);
|
||||
return newIngredient;
|
||||
}
|
||||
|
||||
if (isValidIngredientSyntax(ingredient)) {
|
||||
const possibleUnit = ingredient
|
||||
.split(" ")[0]
|
||||
.replace(/[^a-zA-Z]/g, "")
|
||||
.split(' ')[0]
|
||||
.replace(/[^a-zA-Z]/g, '');
|
||||
const amount = parseFloat(
|
||||
ingredient.split(" ")[0].replace(",", "."),
|
||||
)
|
||||
const unitAndIngredient = ingredient.split(" ").slice(1).join(" ")
|
||||
ingredient.split(' ')[0].replace(',', '.'),
|
||||
);
|
||||
const unitAndIngredient = ingredient.split(' ').slice(1).join(' ');
|
||||
|
||||
let newAmount = (amount / originalYield) * currentYield
|
||||
newAmount = newAmount.toFixed(2).replace(/[.]00$/, "")
|
||||
let newAmount = (amount / originalYield) * currentYield;
|
||||
newAmount = newAmount.toFixed(2).replace(/[.]00$/, '');
|
||||
|
||||
return `${newAmount}${possibleUnit} ${unitAndIngredient}`
|
||||
return `${newAmount}${possibleUnit} ${unitAndIngredient}`;
|
||||
}
|
||||
|
||||
return ingredient
|
||||
})
|
||||
return ingredient;
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
isValidIngredientSyntax,
|
||||
isIngredientsArrayValid,
|
||||
recalculateIngredients,
|
||||
}
|
||||
};
|
||||
|
|
50
src/main.js
50
src/main.js
|
@ -6,69 +6,69 @@
|
|||
*/
|
||||
|
||||
// Markdown
|
||||
import VueShowdown from "vue-showdown"
|
||||
import VueShowdown from 'vue-showdown';
|
||||
|
||||
import Vue from "vue"
|
||||
import Vue from 'vue';
|
||||
|
||||
import * as ModalDialogs from "vue-modal-dialogs"
|
||||
import * as ModalDialogs from 'vue-modal-dialogs';
|
||||
|
||||
import helpers from "cookbook/js/helper"
|
||||
import setupLogging from "cookbook/js/logging"
|
||||
import helpers from 'cookbook/js/helper';
|
||||
import setupLogging from 'cookbook/js/logging';
|
||||
|
||||
import { linkTo } from "@nextcloud/router"
|
||||
import { linkTo } from '@nextcloud/router';
|
||||
|
||||
import router from "./router"
|
||||
import { useStore } from "./store"
|
||||
import router from './router';
|
||||
import { useStore } from './store';
|
||||
|
||||
import AppMain from "./components/AppMain.vue"
|
||||
import AppMain from './components/AppMain.vue';
|
||||
|
||||
// eslint-disable-next-line camelcase,no-undef
|
||||
if (__webpack_use_dev_server__ || false) {
|
||||
// eslint-disable-next-line camelcase,no-undef
|
||||
__webpack_public_path__ = "http://127.0.0.1:3000/apps/cookbook/js/"
|
||||
__webpack_public_path__ = 'http://127.0.0.1:3000/apps/cookbook/js/';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase,no-undef
|
||||
__webpack_public_path__ = `${linkTo("cookbook", "js")}/`
|
||||
__webpack_public_path__ = `${linkTo('cookbook', 'js')}/`;
|
||||
|
||||
// Fetch Nextcloud nonce identifier for dynamic script loading
|
||||
// eslint-disable-next-line camelcase,no-undef
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
__webpack_nonce__ = btoa(OC.requestToken);
|
||||
|
||||
helpers.useRouter(router)
|
||||
helpers.useRouter(router);
|
||||
|
||||
// A simple function to sanitize HTML tags
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
window.escapeHTML = helpers.escapeHTML
|
||||
window.escapeHTML = helpers.escapeHTML;
|
||||
|
||||
// Also make the injections available in Vue components
|
||||
Vue.prototype.$window = window
|
||||
Vue.prototype.OC = OC
|
||||
Vue.prototype.$window = window;
|
||||
Vue.prototype.OC = OC;
|
||||
|
||||
// Markdown for Vue
|
||||
Vue.use(VueShowdown, {
|
||||
// set default flavor for Markdown
|
||||
flavor: "vanilla",
|
||||
})
|
||||
flavor: 'vanilla',
|
||||
});
|
||||
|
||||
// TODO: Equivalent library for Vue3 when we make that transition:
|
||||
// https://github.com/rlemaigre/vue3-promise-dialog
|
||||
Vue.use(ModalDialogs)
|
||||
Vue.use(ModalDialogs);
|
||||
|
||||
setupLogging(Vue)
|
||||
setupLogging(Vue);
|
||||
|
||||
// Pass translation engine to Vue
|
||||
Vue.prototype.t = window.t
|
||||
Vue.prototype.t = window.t;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
// Start the app once document is done loading
|
||||
Vue.$log.info("Main is done. Creating App.")
|
||||
const App = Vue.extend(AppMain)
|
||||
Vue.$log.info('Main is done. Creating App.');
|
||||
const App = Vue.extend(AppMain);
|
||||
new App({
|
||||
store,
|
||||
router,
|
||||
beforeCreate() {
|
||||
this.$store.commit("initializeStore")
|
||||
this.$store.commit('initializeStore');
|
||||
},
|
||||
}).$mount("#content")
|
||||
}).$mount('#content');
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
* ----------------------
|
||||
* @license AGPL3 or later
|
||||
*/
|
||||
import Vue from "vue"
|
||||
import VueRouter from "vue-router"
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
import Index from "../components/AppIndex.vue"
|
||||
import NotFound from "../components/NotFound.vue"
|
||||
import RecipeView from "../components/RecipeView/RecipeView.vue"
|
||||
import RecipeEdit from "../components/RecipeEdit.vue"
|
||||
import Search from "../components/SearchResults.vue"
|
||||
import Index from '../components/AppIndex.vue';
|
||||
import NotFound from '../components/NotFound.vue';
|
||||
import RecipeView from '../components/RecipeView/RecipeView.vue';
|
||||
import RecipeEdit from '../components/RecipeEdit.vue';
|
||||
import Search from '../components/SearchResults.vue';
|
||||
|
||||
Vue.use(VueRouter)
|
||||
Vue.use(VueRouter);
|
||||
|
||||
// The router will try to match routers in a descending order.
|
||||
// Routes that share the same root, must be listed from the
|
||||
|
@ -26,28 +26,28 @@ Vue.use(VueRouter)
|
|||
const routes = [
|
||||
// Search routes
|
||||
{
|
||||
path: "/category/:value",
|
||||
name: "search-category",
|
||||
path: '/category/:value',
|
||||
name: 'search-category',
|
||||
component: Search,
|
||||
props: { query: "cat" },
|
||||
props: { query: 'cat' },
|
||||
},
|
||||
{
|
||||
path: "/name/:value",
|
||||
name: "search-name",
|
||||
path: '/name/:value',
|
||||
name: 'search-name',
|
||||
component: Search,
|
||||
props: { query: "name" },
|
||||
props: { query: 'name' },
|
||||
},
|
||||
{
|
||||
path: "/search/:value",
|
||||
name: "search-general",
|
||||
path: '/search/:value',
|
||||
name: 'search-general',
|
||||
component: Search,
|
||||
props: { query: "general" },
|
||||
props: { query: 'general' },
|
||||
},
|
||||
{
|
||||
path: "/tags/:value",
|
||||
name: "search-tags",
|
||||
path: '/tags/:value',
|
||||
name: 'search-tags',
|
||||
component: Search,
|
||||
props: { query: "tags" },
|
||||
props: { query: 'tags' },
|
||||
},
|
||||
|
||||
// Recipe routes
|
||||
|
@ -59,17 +59,17 @@ const routes = [
|
|||
// - View: /{item}/:id
|
||||
// - Edit: /{item}/:id/edit
|
||||
// - Create: /{item}/create
|
||||
{ path: "/recipe/create", name: "recipe-create", component: RecipeEdit },
|
||||
{ path: "/recipe/:id/edit", name: "recipe-edit", component: RecipeEdit },
|
||||
{ path: "/recipe/:id", name: "recipe-view", component: RecipeView },
|
||||
{ path: '/recipe/create', name: 'recipe-create', component: RecipeEdit },
|
||||
{ path: '/recipe/:id/edit', name: 'recipe-edit', component: RecipeEdit },
|
||||
{ path: '/recipe/:id', name: 'recipe-view', component: RecipeView },
|
||||
|
||||
// Index is the last defined route
|
||||
{ path: "/", name: "index", component: Index },
|
||||
{ path: '/', name: 'index', component: Index },
|
||||
|
||||
// Anything not matched goes to NotFound
|
||||
{ path: "*", name: "not-found", component: NotFound },
|
||||
]
|
||||
{ path: '*', name: 'not-found', component: NotFound },
|
||||
];
|
||||
|
||||
export default new VueRouter({
|
||||
routes,
|
||||
})
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends: "../../.eslintrc.yml"
|
||||
extends: '../../.eslintrc.yml'
|
||||
|
||||
rules:
|
||||
no-param-reassign:
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
* ----------------------
|
||||
* @license AGPL3 or later
|
||||
*/
|
||||
import Vue from "vue"
|
||||
import Vuex from "vuex"
|
||||
import api from "cookbook/js/api-interface"
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import api from 'cookbook/js/api-interface';
|
||||
|
||||
Vue.use(Vuex)
|
||||
Vue.use(Vuex);
|
||||
|
||||
// We are using the vuex store linking changes within the components to updates in the navigation panel.
|
||||
var store = new Vuex.Store({
|
||||
|
@ -31,7 +31,7 @@ var store = new Vuex.Store({
|
|||
// several independent components
|
||||
recipe: null,
|
||||
// Filter applied to a list of recipes
|
||||
recipeFilters: "",
|
||||
recipeFilters: '',
|
||||
// Loading and saving states to determine which loader icons to show.
|
||||
// State of -1 is reserved for recipe and edit views to be set when the
|
||||
// User loads the app at one of these locations and has to wait for an
|
||||
|
@ -53,68 +53,68 @@ var store = new Vuex.Store({
|
|||
|
||||
mutations: {
|
||||
setConfig(state, { config }) {
|
||||
state.config = config
|
||||
state.config = config;
|
||||
},
|
||||
initializeStore(state) {
|
||||
if (localStorage.getItem("showTagCloudInRecipeList")) {
|
||||
if (localStorage.getItem('showTagCloudInRecipeList')) {
|
||||
state.localSettings.showTagCloudInRecipeList = JSON.parse(
|
||||
localStorage.getItem("showTagCloudInRecipeList"),
|
||||
)
|
||||
localStorage.getItem('showTagCloudInRecipeList'),
|
||||
);
|
||||
} else {
|
||||
state.localSettings.showTagCloudInRecipeList = true
|
||||
state.localSettings.showTagCloudInRecipeList = true;
|
||||
}
|
||||
},
|
||||
setAppNavigationRefreshRequired(state, { b }) {
|
||||
state.appNavigation.refreshRequired = b
|
||||
state.appNavigation.refreshRequired = b;
|
||||
},
|
||||
setAppNavigationVisible(state, { b }) {
|
||||
state.appNavigation.visible = b
|
||||
state.appNavigation.visible = b;
|
||||
},
|
||||
setCategoryUpdating(state, { c }) {
|
||||
state.categoryUpdating = c
|
||||
state.categoryUpdating = c;
|
||||
},
|
||||
setLoadingRecipe(state, { r }) {
|
||||
state.loadingRecipe = r
|
||||
state.loadingRecipe = r;
|
||||
},
|
||||
setPage(state, { p }) {
|
||||
state.page = p
|
||||
state.page = p;
|
||||
},
|
||||
setRecipe(state, { r }) {
|
||||
const rec = JSON.parse(JSON.stringify(r))
|
||||
const rec = JSON.parse(JSON.stringify(r));
|
||||
if (rec === null) {
|
||||
state.recipe = null
|
||||
return
|
||||
state.recipe = null;
|
||||
return;
|
||||
}
|
||||
if ("nutrition" in rec && rec.nutrition instanceof Array) {
|
||||
rec.nutrition = {}
|
||||
if ('nutrition' in rec && rec.nutrition instanceof Array) {
|
||||
rec.nutrition = {};
|
||||
}
|
||||
state.recipe = rec
|
||||
state.recipe = rec;
|
||||
|
||||
// Setting recipe also means that loading/reloading the recipe has finished
|
||||
state.loadingRecipe = 0
|
||||
state.reloadingRecipe = 0
|
||||
state.loadingRecipe = 0;
|
||||
state.reloadingRecipe = 0;
|
||||
},
|
||||
setRecipeCategory(state, { c }) {
|
||||
state.recipe.category = c
|
||||
state.recipe.category = c;
|
||||
},
|
||||
setRecipeFilters(state, { f }) {
|
||||
state.recipeFilters = f;
|
||||
},
|
||||
setReloadingRecipe(state, { r }) {
|
||||
state.reloadingRecipe = r
|
||||
state.reloadingRecipe = r;
|
||||
},
|
||||
setSavingRecipe(state, { b }) {
|
||||
state.savingRecipe = b
|
||||
state.savingRecipe = b;
|
||||
},
|
||||
setShowTagCloudInRecipeList(state, { b }) {
|
||||
localStorage.setItem("showTagCloudInRecipeList", JSON.stringify(b))
|
||||
state.localSettings.showTagCloudInRecipeList = b
|
||||
localStorage.setItem('showTagCloudInRecipeList', JSON.stringify(b));
|
||||
state.localSettings.showTagCloudInRecipeList = b;
|
||||
},
|
||||
setUser(state, { u }) {
|
||||
state.user = u
|
||||
state.user = u;
|
||||
},
|
||||
setUpdatingRecipeDirectory(state, { b }) {
|
||||
state.updatingRecipeDirectory = b
|
||||
state.updatingRecipeDirectory = b;
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -123,8 +123,8 @@ var store = new Vuex.Store({
|
|||
* Read/Update the user settings from the backend
|
||||
*/
|
||||
async refreshConfig(c) {
|
||||
const config = (await api.config.get()).data
|
||||
c.commit("setConfig", { config })
|
||||
const config = (await api.config.get()).data;
|
||||
c.commit('setConfig', { config });
|
||||
},
|
||||
|
||||
/*
|
||||
|
@ -138,114 +138,117 @@ var store = new Vuex.Store({
|
|||
* Create new recipe on the server
|
||||
*/
|
||||
createRecipe(c, { recipe }) {
|
||||
const request = api.recipes.create(recipe)
|
||||
const request = api.recipes.create(recipe);
|
||||
return request.then((v) => {
|
||||
// Refresh navigation to display changes
|
||||
c.dispatch("setAppNavigationRefreshRequired", {
|
||||
c.dispatch('setAppNavigationRefreshRequired', {
|
||||
isRequired: true,
|
||||
})
|
||||
});
|
||||
|
||||
return v
|
||||
})
|
||||
return v;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Delete recipe on the server
|
||||
*/
|
||||
deleteRecipe(c, { id }) {
|
||||
const request = api.recipes.delete(id)
|
||||
const request = api.recipes.delete(id);
|
||||
request.then(() => {
|
||||
// Refresh navigation to display changes
|
||||
c.dispatch("setAppNavigationRefreshRequired", {
|
||||
c.dispatch('setAppNavigationRefreshRequired', {
|
||||
isRequired: true,
|
||||
})
|
||||
})
|
||||
return request
|
||||
});
|
||||
});
|
||||
return request;
|
||||
},
|
||||
setAppNavigationVisible(c, { isVisible }) {
|
||||
c.commit("setAppNavigationVisible", { b: isVisible })
|
||||
c.commit('setAppNavigationVisible', { b: isVisible });
|
||||
},
|
||||
setAppNavigationRefreshRequired(c, { isRequired }) {
|
||||
c.commit("setAppNavigationRefreshRequired", { b: isRequired })
|
||||
c.commit('setAppNavigationRefreshRequired', { b: isRequired });
|
||||
},
|
||||
setLoadingRecipe(c, { recipe }) {
|
||||
c.commit("setLoadingRecipe", { r: parseInt(recipe, 10) })
|
||||
c.commit('setLoadingRecipe', { r: parseInt(recipe, 10) });
|
||||
},
|
||||
setPage(c, { page }) {
|
||||
c.commit("setPage", { p: page })
|
||||
c.commit('setPage', { p: page });
|
||||
},
|
||||
setRecipe(c, { recipe }) {
|
||||
c.commit("setRecipe", { r: recipe })
|
||||
c.commit('setRecipe', { r: recipe });
|
||||
},
|
||||
setRecipeFilters(c, filters) {
|
||||
c.commit("setRecipeFilters", { f: filters })
|
||||
c.commit('setRecipeFilters', { f: filters });
|
||||
},
|
||||
setReloadingRecipe(c, { recipe }) {
|
||||
c.commit("setReloadingRecipe", { r: parseInt(recipe, 10) })
|
||||
c.commit('setReloadingRecipe', { r: parseInt(recipe, 10) });
|
||||
},
|
||||
setSavingRecipe(c, { saving }) {
|
||||
c.commit("setSavingRecipe", { b: saving })
|
||||
c.commit('setSavingRecipe', { b: saving });
|
||||
},
|
||||
setUser(c, { user }) {
|
||||
c.commit("setUser", { u: user })
|
||||
c.commit('setUser', { u: user });
|
||||
},
|
||||
setCategoryUpdating(c, { category }) {
|
||||
c.commit("setCategoryUpdating", { c: category })
|
||||
c.commit('setCategoryUpdating', { c: category });
|
||||
},
|
||||
setShowTagCloudInRecipeList(c, { showTagCloud }) {
|
||||
c.commit("setShowTagCloudInRecipeList", { b: showTagCloud })
|
||||
c.commit('setShowTagCloudInRecipeList', { b: showTagCloud });
|
||||
},
|
||||
updateCategoryName(c, { categoryNames }) {
|
||||
const oldName = categoryNames[0]
|
||||
const newName = categoryNames[1]
|
||||
c.dispatch("setCategoryUpdating", { category: oldName })
|
||||
const oldName = categoryNames[0];
|
||||
const newName = categoryNames[1];
|
||||
c.dispatch('setCategoryUpdating', { category: oldName });
|
||||
|
||||
const request = api.categories.update(oldName, newName)
|
||||
const request = api.categories.update(oldName, newName);
|
||||
|
||||
request
|
||||
.then(() => {
|
||||
if (c.state.recipe && c.state.recipe.recipeCategory === oldName) {
|
||||
c.commit("setRecipeCategory", { c: newName })
|
||||
if (
|
||||
c.state.recipe &&
|
||||
c.state.recipe.recipeCategory === oldName
|
||||
) {
|
||||
c.commit('setRecipeCategory', { c: newName });
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e && e instanceof Error) {
|
||||
throw e
|
||||
throw e;
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// finally
|
||||
c.dispatch("setCategoryUpdating", { category: null })
|
||||
})
|
||||
c.dispatch('setCategoryUpdating', { category: null });
|
||||
});
|
||||
|
||||
return request
|
||||
return request;
|
||||
},
|
||||
updateRecipeDirectory(c, { dir }) {
|
||||
c.commit("setUpdatingRecipeDirectory", { b: true })
|
||||
c.dispatch("setRecipe", { recipe: null })
|
||||
const request = api.config.directory.update(dir)
|
||||
c.commit('setUpdatingRecipeDirectory', { b: true });
|
||||
c.dispatch('setRecipe', { recipe: null });
|
||||
const request = api.config.directory.update(dir);
|
||||
|
||||
request.then(() => {
|
||||
c.dispatch("setAppNavigationRefreshRequired", {
|
||||
c.dispatch('setAppNavigationRefreshRequired', {
|
||||
isRequired: true,
|
||||
})
|
||||
c.commit("setUpdatingRecipeDirectory", { b: false })
|
||||
})
|
||||
return request
|
||||
});
|
||||
c.commit('setUpdatingRecipeDirectory', { b: false });
|
||||
});
|
||||
return request;
|
||||
},
|
||||
/**
|
||||
* Update existing recipe on the server
|
||||
*/
|
||||
updateRecipe(c, { recipe }) {
|
||||
const request = api.recipes.update(recipe.id, recipe)
|
||||
const request = api.recipes.update(recipe.id, recipe);
|
||||
request.then(() => {
|
||||
// Refresh navigation to display changes
|
||||
c.dispatch("setAppNavigationRefreshRequired", {
|
||||
c.dispatch('setAppNavigationRefreshRequired', {
|
||||
isRequired: true,
|
||||
})
|
||||
})
|
||||
return request
|
||||
});
|
||||
});
|
||||
return request;
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const useStore = () => store;
|
||||
|
|
|
@ -3,7 +3,7 @@ const stylelintConfig = require('@nextcloud/stylelint-config')
|
|||
stylelintConfig.extends.push('stylelint-config-idiomatic-order')
|
||||
|
||||
stylelintConfig.rules.indentation = null
|
||||
stylelintConfig.rules['string-quotes'] = 'double'
|
||||
stylelintConfig.rules['string-quotes'] = 'single'
|
||||
stylelintConfig.rules['function-no-unknown'] = [true, {
|
||||
'ignoreFunctions': ["math.div"]
|
||||
}]
|
||||
|
|
Загрузка…
Ссылка в новой задаче