Merge branch 'TandoorRecipes:develop' into cookbookapp-images-import

This commit is contained in:
caffeinated-tech
2025-07-07 21:33:23 +01:00
committed by GitHub
26 changed files with 288 additions and 282 deletions

View File

@@ -6,8 +6,10 @@ class StyleTreeprocessor(Treeprocessor):
def run_processor(self, node):
for child in node:
# if child.tag == "table":
# child.set("class", "markdown-body")
if child.tag == "table":
child.set("class", "markdown-table")
if child.tag == "th" or child.tag == "td":
child.set("class", "markdown-table-cell")
if child.tag == "img":
child.set("class", "img-fluid")
self.run_processor(child)

View File

@@ -341,7 +341,7 @@ class OpenDataImporter:
obj_dict = {
'name': self.data[datatype][k]['name'],
'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']] if self.data[datatype][k]['store_category'] in self.slug_id_cache['category'] else None,
'fdc_id': re.sub(r'\D', '', self.data[datatype][k]['fdc_id']) if self.data[datatype][k]['fdc_id'] != '' else None,
'open_data_slug': k,
'properties_food_unit_id': None,

View File

@@ -641,7 +641,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
class Meta:
model = SupermarketCategory
fields = ('id', 'name', 'description')
fields = ('id', 'name', 'description', 'open_data_slug')
class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
@@ -729,6 +729,7 @@ class RecipeFlatSerializer(WritableNestedModelSerializer):
class Meta:
model = Recipe
fields = ('id', 'name', 'image')
read_only_fields = ('id', 'name', 'image')
class FoodSimpleSerializer(serializers.ModelSerializer):
@@ -1772,7 +1773,7 @@ class AiImportSerializer(serializers.Serializer):
class ExportRequestSerializer(serializers.Serializer):
type = serializers.CharField()
all = serializers.BooleanField(default=False)
recipes = RecipeFlatSerializer(many=True, default=[])
recipes = RecipeSimpleSerializer(many=True, default=[])
custom_filter = CustomFilterSerializer(many=False, default=None, allow_null=True)

View File

@@ -1,7 +0,0 @@
from django.test import utils
from django_scopes import scopes_disabled
# disables scoping error in all queries used inside the test FUNCTIONS
# FIXTURES need to have their own scopes_disabled!!
# This is done by hook pytest_fixture_setup in conftest.py for all non yield fixtures
utils.setup_databases = scopes_disabled()(utils.setup_databases)

View File

@@ -1,75 +0,0 @@
import pytest
from django.contrib import auth
from django.contrib import messages
from django.contrib.messages import get_messages
from django.urls import reverse
from pytest_django.asserts import assertTemplateUsed
from cookbook.models import ConnectorConfig
EDIT_VIEW_NAME = 'edit_connector_config'
@pytest.fixture
def home_assistant_config_obj(a1_s1, space_1):
return ConnectorConfig.objects.create(
name='HomeAssistant 1',
type=ConnectorConfig.HOMEASSISTANT,
token='token',
url='http://localhost:8123/api',
todo_entity='todo.shopping_list',
enabled=True,
created_by=auth.get_user(a1_s1),
space=space_1,
)
def test_edit_connector_config_homeassistant(home_assistant_config_obj: ConnectorConfig, a1_s1, a1_s2):
new_token = '1234_token'
r = a1_s1.post(
reverse(EDIT_VIEW_NAME, args={home_assistant_config_obj.pk}),
{
'name': home_assistant_config_obj.name,
'type': home_assistant_config_obj.type,
'url': home_assistant_config_obj.url,
'update_token': new_token,
'todo_entity': home_assistant_config_obj.todo_entity,
'enabled': home_assistant_config_obj.enabled,
}
)
assert r.status_code == 302
r_messages = [m for m in get_messages(r.wsgi_request)]
assert not any(m.level > messages.SUCCESS for m in r_messages)
home_assistant_config_obj.refresh_from_db()
assert home_assistant_config_obj.token == new_token
r = a1_s2.post(
reverse(EDIT_VIEW_NAME, args={home_assistant_config_obj.pk}),
{
'name': home_assistant_config_obj.name,
'type': home_assistant_config_obj.type,
'url': home_assistant_config_obj.url,
'todo_entity': home_assistant_config_obj.todo_entity,
'update_token': new_token,
'enabled': home_assistant_config_obj.enabled,
}
)
assert r.status_code == 404
@pytest.mark.parametrize(
"arg", [
['a_u', 302],
['g1_s1', 302],
['u1_s1', 302],
['a1_s1', 200],
['g1_s2', 302],
['u1_s2', 302],
['a1_s2', 404],
])
def test_view_permission(arg, request, home_assistant_config_obj):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(EDIT_VIEW_NAME, args={home_assistant_config_obj.pk})).status_code == arg[1]

View File

@@ -1,64 +0,0 @@
import pytest
from django.contrib import auth
from django.contrib import messages
from django.contrib.messages import get_messages
from django.urls import reverse
from cookbook.models import Storage
@pytest.fixture
def storage_obj(a1_s1, space_1):
return Storage.objects.create(
name='TestStorage',
method=Storage.DROPBOX,
created_by=auth.get_user(a1_s1),
token='test',
username='test',
password='test',
space=space_1,
)
def test_edit_storage(storage_obj, a1_s1, a1_s2):
r = a1_s1.post(
reverse('edit_storage', args={storage_obj.pk}),
{
'name': 'NewStorage',
'password': '1234_pw',
'token': '1234_token',
'method': Storage.DROPBOX
}
)
storage_obj.refresh_from_db()
assert r.status_code == 302
#r_messages = [m for m in get_messages(r.wsgi_request)]
#assert not any(m.level > messages.SUCCESS for m in r_messages)
#assert storage_obj.password == '1234_pw'
#assert storage_obj.token == '1234_token'
r = a1_s2.post(
reverse('edit_storage', args={storage_obj.pk}),
{
'name': 'NewStorage',
'password': '1234_pw',
'token': '1234_token',
'method': Storage.DROPBOX
}
)
assert r.status_code == 404
@pytest.mark.parametrize("arg", [
['a_u', 302],
['g1_s1', 302],
['u1_s1', 302],
['a1_s1', 302],
['g1_s2', 302],
['u1_s2', 302],
['a1_s2', 404],
])
def test_view_permission(arg, request, storage_obj):
c = request.getfixturevalue(arg[0])
assert c.get(reverse('edit_storage', args={storage_obj.pk})).status_code == arg[1]

View File

@@ -1,24 +0,0 @@
import pytest
from django.contrib import auth
from django.urls import reverse
from cookbook.forms import ImportExportBase
from cookbook.models import ExportLog
@pytest.fixture
def obj_1(space_1, u1_s1):
return ExportLog.objects.create(type=ImportExportBase.DEFAULT, running=False, created_by=auth.get_user(u1_s1), space=space_1, exported_recipes=10, total_recipes=10)
@pytest.mark.parametrize("arg", [
['a_u', 302],
['g1_s1', 302],
['u1_s1', 200],
['a1_s1', 200],
['u1_s2', 404],
['a1_s2', 404],
])
def test_export_file_cache(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
assert c.get(reverse('view_export_file', args=[obj_1.pk])).status_code == arg[1]

View File

@@ -120,7 +120,7 @@ urlpatterns = [
path('api-token-auth/', CustomAuthToken.as_view()),
path('offline/', views.offline, name='view_offline'),
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript')), name='service_worker'),
#path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript')), name='service_worker'),
path('manifest.json', views.web_manifest, name='web_manifest'),
]

View File

@@ -83,6 +83,8 @@ def get_integration(request, export_type):
return Rezeptsuitede(request, export_type)
if export_type == ImportExportBase.GOURMET:
return Gourmet(request, export_type)
@group_required('user')
def export_file(request, pk):
el = get_object_or_404(ExportLog, pk=pk, space=request.space)
@@ -92,7 +94,7 @@ def export_file(request, pk):
if cacheData is None:
el.possibly_not_expired = False
el.save()
return render(request, 'export_response.html', {'pk': pk})
return JsonResponse({'msg': 'Export Expired or not found'}, status=404)
response = HttpResponse(cacheData['file'], content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="' + cacheData['filename'] + '"'

View File

@@ -20,7 +20,7 @@ UNINSTALL_MIDDLEWARE = [
]
UNINSTALL_INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.sites', 'django.contrib.staticfiles', 'corsheaders', 'django_cleanup.apps.CleanupConfig', 'django_js_reverse', 'hcaptcha']
'django.contrib.messages', 'django.contrib.sites', 'django.contrib.staticfiles', 'corsheaders', 'django_cleanup.apps.CleanupConfig', 'hcaptcha']
# disable extras not needed for testing
for x in UNINSTALL_MIDDLEWARE:

View File

@@ -1,12 +1,12 @@
Django==4.2.22
cryptography===44.0.1
cryptography===45.0.5
django-annoying==0.10.6
django-cleanup==9.0.0
django-crispy-forms==2.4
crispy-bootstrap4==2025.6
djangorestframework==3.15.2
drf-spectacular==0.27.1
drf-spectacular-sidecar==2024.2.1
drf-spectacular-sidecar==2025.7.1
drf-writable-nested==0.7.2
django-oauth-toolkit==2.4.0
django-debug-toolbar==4.3.0
@@ -40,7 +40,7 @@ django-hCaptcha==0.2.0
python-ldap==3.4.4
django-auth-ldap==4.6.0
pyppeteer==2.0.0
pytubefix==8.13.1
pytubefix==9.2.2
aiohttp==3.10.11
inflection==0.5.1
redis==5.2.1
@@ -48,17 +48,17 @@ requests-oauthlib==2.0.0
pyjwt==2.10.1
python3-openid==3.2.0
python3-saml==1.16.0
django-vite==3.0.3
django-vite==3.1.0
litellm==1.64.1
# Development
pytest==8.0.0
pytest-django==4.10.0
pytest-cov===5.0.0
pytest-factoryboy==2.7.0
pytest==8.4.1
pytest-django==4.11.0
pytest-cov===6.0.0
pytest-factoryboy==2.8.0
pytest-html==4.1.1
pytest-asyncio==0.23.5
pytest-xdist==3.6.1
pytest-asyncio==0.25.3
pytest-xdist==3.7.0
autopep8==2.3.2
flake8==7.3.0
yapf==0.40.2

View File

@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vite build --emptyOutDir",
"preview": "vite preview"
},
"dependencies": {
@@ -18,32 +18,32 @@
"pinia": "^3.0.2",
"vue": "^3.5.13",
"vue-draggable-plus": "^0.6.0",
"vue-i18n": "^11.1.3",
"vue-i18n": "^11.1.7",
"vue-router": "^4.5.0",
"vue-simple-calendar": "7.1.0",
"vuedraggable": "^4.1.0",
"@types/sortablejs": "^1.15.8",
"vuetify": "^3.8.10"
"vuetify": "^3.8.12"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@tsconfig/node22": "^22.0.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3",
"@types/node": "^24.0.8",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"jsdom": "^26.1.0",
"typescript": "^5.8.3",
"vite": "6.3.5",
"vite-plugin-pwa": "^1.0.0",
"vite-plugin-pwa": "^1.0.1",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0",
"workbox-background-sync": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^6.2.4",
"workbox-background-sync": "^7.3.0",
"workbox-expiration": "^7.3.0",
"workbox-navigation-preload": "^7.3.0",
"workbox-precaching": "^7.3.0",
"workbox-routing": "^7.3.0",
"workbox-strategies": "^7.3.0",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8"
}

View File

@@ -0,0 +1,52 @@
<template>
<v-dialog height="70vh" activator="parent">
<v-card>
<v-closable-card-title v-model="dialog" :title="$t('Help')" icon="fa-solid fa-question"></v-closable-card-title>
<v-divider></v-divider>
<v-card-text class="pa-0">
<v-layout style="height: 100%">
<v-navigation-drawer style="height: calc(100% + 0px)">
<v-list-item>
<v-text-field density="compact" variant="outlined" class="pt-2 pb-2" :label="$t('Search')" hide-details clearable ></v-text-field>
</v-list-item>
<v-divider></v-divider>
<v-list-item link title="Start" @click="window = 'start'"></v-list-item>
<v-list-item link title="Test" @click="window = 'test'"></v-list-item>
</v-navigation-drawer>
<v-main>
<v-container>
<v-window v-model="window">
<v-window-item value="start">
Start
</v-window-item>
<v-window-item value="test">
Test
</v-window-item>
</v-window>
</v-container>
</v-main>
</v-layout>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import {ref} from "vue";
const dialog = ref(false)
const window = ref('start')
</script>
<style scoped>
</style>

View File

@@ -52,4 +52,38 @@ p, ol, ul, li {
margin: revert;
}
/* css classes needed to render markdown blockquotes */
blockquote {
background: rgb(200,200,200,0.2);
border-left: 4px solid #ccc;
margin: 1.5em 10px;
padding: .5em 10px;
quotes: none;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: .1em;
margin-right: .25em;
vertical-align: -.4em;
}
blockquote p {
display: inline;
}
.markdown-table {
border: 1px solid;
border-collapse: collapse;
}
.markdown-table-cell {
border: 1px solid;
border-collapse: collapse;
padding: 8px;
}
</style>

View File

@@ -79,6 +79,14 @@
<v-window v-model="currentTab">
<v-window-item value="shopping">
<v-container>
<v-row class="pa-0" dense>
<v-col class="pa-0">
<v-chip-group v-model="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket" v-if="supermarkets.length > 0" >
<v-chip v-for="s in supermarkets" :value="s" :key="s.id" label density="compact" variant="outlined" color="primary">{{s.name}}</v-chip>
</v-chip-group>
</v-col>
</v-row>
<v-row>
<v-col>
<v-alert v-if="useShoppingStore().hasFailedItems()" color="warning" class="mb-2">
@@ -226,20 +234,18 @@
import {computed, onMounted, ref} from "vue";
import {useShoppingStore} from "@/stores/ShoppingStore";
import {ApiApi, IngredientString, ResponseError, ShoppingListEntry, ShoppingListRecipe, Supermarket} from "@/openapi";
import {ApiApi, ResponseError, ShoppingListEntry, ShoppingListRecipe, Supermarket} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import ShoppingLineItem from "@/components/display/ShoppingLineItem.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import ShoppingLineItemDialog from "@/components/dialogs/ShoppingLineItemDialog.vue";
import {IShoppingListCategory, IShoppingListFood, ShoppingGroupingOptions} from "@/types/Shopping";
import {ShoppingGroupingOptions} from "@/types/Shopping";
import {useI18n} from "vue-i18n";
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue";
import SupermarketEditor from "@/components/model_editors/SupermarketEditor.vue";
import DeleteConfirmDialog from "@/components/dialogs/DeleteConfirmDialog.vue";
import ShoppingListEntryInput from "@/components/inputs/ShoppingListEntryInput.vue";
import {DateTime} from "luxon";
import MealPlanEditor from "@/components/model_editors/MealPlanEditor.vue";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import {onBeforeRouteLeave} from "vue-router";
import {isShoppingCategoryVisible} from "@/utils/logic_utils.ts";
@@ -248,6 +254,7 @@ import ShoppingExportDialog from "@/components/dialogs/ShoppingExportDialog.vue"
const {t} = useI18n()
const currentTab = ref("shopping")
const supermarkets = ref([] as Supermarket[])
/**
* VSelect items for shopping list grouping options with localized names
@@ -279,6 +286,8 @@ onMounted(() => {
}
})
}
loadSupermarkets()
})
/**
@@ -339,6 +348,20 @@ function deleteListRecipe(slr: ShoppingListRecipe) {
})
}
/**
* load a list of supermarkets
*/
function loadSupermarkets(){
let api = new ApiApi()
api.apiSupermarketList().then(r => {
supermarkets.value = r.results
// TODO either recursive or add a "favorite" attribute to supermarkets for them to display at all
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
</script>
<style scoped>

View File

@@ -2,7 +2,7 @@
<v-progress-linear :model-value="timerProgress" color="primary" height="5"></v-progress-linear>
<v-alert :color="timerColor" class="rounded-0" variant="tonal">
<v-alert-title><i class="fas fa-stopwatch mr-1"></i> {{ Duration.fromMillis(durationSeconds * 1000).toFormat('hh:mm:ss') }}</v-alert-title>
{{$t('FinishedAt')}} {{ DateTime.now().plus({'seconds': durationSeconds}).toLocaleString(DateTime.TIME_SIMPLE) }}
{{$t('FinishedAt')}} {{ formatFinishTime(durationSeconds) }}
<template #close>
<v-btn-group divided>
<v-btn width="40" @click="changeTimer(-60)"><i class="fas fa-minus"></i>1</v-btn>
@@ -76,8 +76,26 @@ function stopTimer() {
emit('stop')
}
/**
* formats a future time based on a duration in seconds from now
* displays the time in "hh:mm" format and adds a "+Xd" suffix if the target day differs from today
*/
function formatFinishTime(durationSeconds: number): string {
const now = DateTime.now()
const target = now.plus({ seconds: durationSeconds })
let timeString = target.toLocaleString(DateTime.TIME_SIMPLE)
const daysDifference = Math.floor(
target.startOf('day').diff(now.startOf('day'), 'days').days
)
if (daysDifference >= 1) {
const label = daysDifference === 1 ? t('Day') : t('Days');
timeString += ` +${daysDifference} ${label}`;
}
return timeString
}
</script>
<style scoped>
</style>
</style>

View File

@@ -59,7 +59,7 @@
<v-label>{{ $t('Ingredients') }}</v-label>
<div v-if="!mobile">
<vue-draggable v-model="step.ingredients" handle=".drag-handle" :on-sort="sortIngredients" :empty-insert-threshold="25" group="ingredients">
<v-row v-for="(ingredient, index) in step.ingredients" dense>
<v-row v-for="(ingredient, index) in step.ingredients" :key="ingredient.id" dense>
<v-col cols="2" v-if="!ingredient.isHeader">
<v-input hide-details>
<template #prepend>
@@ -118,7 +118,7 @@
<v-list v-if="mobile">
<vue-draggable v-model="step.ingredients" handle=".drag-handle" :on-sort="sortIngredients" group="ingredients" empty-insert-threshold="25">
<v-list-item v-for="(ingredient, index) in step.ingredients" border @click="editingIngredientIndex = index; dialogIngredientEditor = true">
<v-list-item v-for="(ingredient, index) in step.ingredients" :key="ingredient.id" border @click="editingIngredientIndex = index; dialogIngredientEditor = true">
<ingredient-string :ingredient="ingredient"></ingredient-string>
<template #append>
<v-icon icon="$dragHandle" class="drag-handle"></v-icon>
@@ -344,6 +344,7 @@ function insertAndFocusIngredient() {
step.value.ingredients.push(ingredient)
nextTick(() => {
sortIngredients()
if (mobile.value) {
editingIngredientIndex.value = step.value.ingredients.length - 1
dialogIngredientEditor.value = true

File diff suppressed because one or more lines are too long

View File

@@ -14,6 +14,7 @@
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
<v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea>
<v-text-field :label="$t('Open_Data_Slug')" :hint="$t('open_data_help_text')" persistent-hint v-model="editingObj.openDataSlug" disabled></v-text-field>
</v-form>
</v-card-text>

View File

@@ -19,12 +19,12 @@ import {
CustomFilterFromJSONTyped,
CustomFilterToJSON,
} from './CustomFilter';
import type { RecipeFlat } from './RecipeFlat';
import type { RecipeSimple } from './RecipeSimple';
import {
RecipeFlatFromJSON,
RecipeFlatFromJSONTyped,
RecipeFlatToJSON,
} from './RecipeFlat';
RecipeSimpleFromJSON,
RecipeSimpleFromJSONTyped,
RecipeSimpleToJSON,
} from './RecipeSimple';
/**
*
@@ -46,10 +46,10 @@ export interface ExportRequest {
all?: boolean;
/**
*
* @type {Array<RecipeFlat>}
* @type {Array<RecipeSimple>}
* @memberof ExportRequest
*/
recipes?: Array<RecipeFlat>;
recipes?: Array<RecipeSimple>;
/**
*
* @type {CustomFilter}
@@ -78,7 +78,7 @@ export function ExportRequestFromJSONTyped(json: any, ignoreDiscriminator: boole
'type': json['type'],
'all': json['all'] == null ? undefined : json['all'],
'recipes': json['recipes'] == null ? undefined : ((json['recipes'] as Array<any>).map(RecipeFlatFromJSON)),
'recipes': json['recipes'] == null ? undefined : ((json['recipes'] as Array<any>).map(RecipeSimpleFromJSON)),
'customFilter': json['custom_filter'] == null ? undefined : CustomFilterFromJSON(json['custom_filter']),
};
}
@@ -91,7 +91,7 @@ export function ExportRequestToJSON(value?: ExportRequest | null): any {
'type': value['type'],
'all': value['all'],
'recipes': value['recipes'] == null ? undefined : ((value['recipes'] as Array<any>).map(RecipeFlatToJSON)),
'recipes': value['recipes'] == null ? undefined : ((value['recipes'] as Array<any>).map(RecipeSimpleToJSON)),
'custom_filter': CustomFilterToJSON(value['customFilter']),
};
}

View File

@@ -71,6 +71,12 @@ export interface PatchedSupermarketCategory {
* @memberof PatchedSupermarketCategory
*/
description?: string;
/**
*
* @type {string}
* @memberof PatchedSupermarketCategory
*/
openDataSlug?: string;
}
/**
@@ -93,6 +99,7 @@ export function PatchedSupermarketCategoryFromJSONTyped(json: any, ignoreDiscrim
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'openDataSlug': json['open_data_slug'] == null ? undefined : json['open_data_slug'],
};
}
@@ -105,6 +112,7 @@ export function PatchedSupermarketCategoryToJSON(value?: PatchedSupermarketCateg
'id': value['id'],
'name': value['name'],
'description': value['description'],
'open_data_slug': value['openDataSlug'],
};
}

View File

@@ -30,13 +30,13 @@ export interface RecipeFlat {
* @type {string}
* @memberof RecipeFlat
*/
name: string;
readonly name: string;
/**
*
* @type {string}
* @memberof RecipeFlat
*/
image?: string;
readonly image: string | null;
}
/**
@@ -44,6 +44,7 @@ export interface RecipeFlat {
*/
export function instanceOfRecipeFlat(value: object): value is RecipeFlat {
if (!('name' in value) || value['name'] === undefined) return false;
if (!('image' in value) || value['image'] === undefined) return false;
return true;
}
@@ -59,19 +60,17 @@ export function RecipeFlatFromJSONTyped(json: any, ignoreDiscriminator: boolean)
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'],
'image': json['image'] == null ? undefined : json['image'],
'image': json['image'],
};
}
export function RecipeFlatToJSON(value?: RecipeFlat | null): any {
export function RecipeFlatToJSON(value?: Omit<RecipeFlat, 'name'|'image'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'image': value['image'],
};
}

View File

@@ -71,6 +71,12 @@ export interface SupermarketCategory {
* @memberof SupermarketCategory
*/
description?: string;
/**
*
* @type {string}
* @memberof SupermarketCategory
*/
openDataSlug?: string;
}
/**
@@ -94,6 +100,7 @@ export function SupermarketCategoryFromJSONTyped(json: any, ignoreDiscriminator:
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'],
'description': json['description'] == null ? undefined : json['description'],
'openDataSlug': json['open_data_slug'] == null ? undefined : json['open_data_slug'],
};
}
@@ -106,6 +113,7 @@ export function SupermarketCategoryToJSON(value?: SupermarketCategory | null): a
'id': value['id'],
'name': value['name'],
'description': value['description'],
'open_data_slug': value['openDataSlug'],
};
}

View File

@@ -14,6 +14,11 @@
</v-col>
</v-row>
<v-btn>
Test Dialog
<help-dialog></help-dialog>
</v-btn>
<v-row>
<v-col>
<v-expansion-panels>
@@ -90,6 +95,7 @@
import {TKeyword, TRecipe, TUnit, TUserSpace} from "@/types/Models";
import {useUserPreferenceStore} from "../stores/UserPreferenceStore";
import HelpDialog from "@/components/dialogs/HelpDialog.vue";
</script>
<style scoped>

View File

@@ -168,7 +168,7 @@
<v-stepper-actions>
<template #prev>
<v-btn @click="stepper = 'type'">{{ $t('Back') }}</v-btn>
<v-btn @click="stepper = 'type'; importResponse = {}">{{ $t('Back') }}</v-btn>
</template>
<template #next>
<v-btn @click="loadRecipeFromUrl({url: importUrl})" v-if="importType == 'url'" :disabled="importUrl == ''" :loading="loading">{{

View File

@@ -959,26 +959,26 @@
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz#8249de9b7e22fcb3ceb5e66090c30a1d5492b81a"
integrity sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==
"@intlify/core-base@11.1.6":
version "11.1.6"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.6.tgz#87986c85e45a973e810296b046a77c0ae88f8130"
integrity sha512-gfMLnoWGiQkA1BwK6Qbrog/e3I6Lnkhqk08XObJb0lMq6sLG1Ggl2MazVaMfGnv/E1Td8pCS5UwR54Ys+fOxmQ==
"@intlify/core-base@11.1.7":
version "11.1.7"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.7.tgz#497280e4774011cf0d42eaedb20e9cd4594c0a3f"
integrity sha512-gYiGnQeJVp3kNBeXQ73m1uFOak0ry4av8pn+IkEWigyyPWEMGzB+xFeQdmGMFn49V+oox6294oGVff8bYOhtOw==
dependencies:
"@intlify/message-compiler" "11.1.6"
"@intlify/shared" "11.1.6"
"@intlify/message-compiler" "11.1.7"
"@intlify/shared" "11.1.7"
"@intlify/message-compiler@11.1.6":
version "11.1.6"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.6.tgz#65a86f9c7b030b4c2d7faa4ab4f9104adbd1fdbf"
integrity sha512-w0LYo5sqgQZF3vEmjLlx+5PYk5EEiB+uigsBkka/DKoAIH2c5xlXcjAxhTgSw35Vrck+GOGriahFsfbHL+ZjPw==
"@intlify/message-compiler@11.1.7":
version "11.1.7"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.7.tgz#047ba659cfd34b0f630dddf73c3f9224bd3af7f8"
integrity sha512-0ezkep1AT30NyuKj8QbRlmvMORCCRlOIIu9v8RNU8SwDjjTiFCZzczCORMns2mCH4HZ1nXgrfkKzYUbfjNRmng==
dependencies:
"@intlify/shared" "11.1.6"
"@intlify/shared" "11.1.7"
source-map-js "^1.0.2"
"@intlify/shared@11.1.6":
version "11.1.6"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.6.tgz#57887e58559c229773da792a97506e16d41042fa"
integrity sha512-G1Pe4UILhiGOItuehRW+Pk9/NlnRaMFsdnhZ1fwBjiHvrzitmPNZdLx7Eo3GPfRrsk1mdkilZSfgH8SnM419vA==
"@intlify/shared@11.1.7":
version "11.1.7"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.7.tgz#54e60d52b73fb25019e2689d6531a54928b40194"
integrity sha512-4yZeMt2Aa/7n5Ehy4KalUlvt3iRLcg1tq9IBVfOgkyWFArN4oygn6WxgGIFibP3svpaH8DarbNaottq+p0gUZQ==
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.8"
@@ -1020,6 +1020,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@rolldown/pluginutils@1.0.0-beta.19":
version "1.0.0-beta.19"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz#fc3b95145a8e7a3bf92754269d8e4f40eea8a244"
integrity sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==
"@rollup/plugin-babel@^5.2.0":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
@@ -1213,20 +1218,13 @@
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.6.2.tgz#be6536931801f437eafcb9c0f6d6781f72308041"
integrity sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==
"@types/node@*":
version "24.0.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.0.3.tgz#f935910f3eece3a3a2f8be86b96ba833dc286cab"
integrity sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==
"@types/node@*", "@types/node@^24.0.8":
version "24.0.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.0.8.tgz#98f50977fe76ab78d02fc3bcb7f6c3b5f79a363c"
integrity sha512-WytNrFSgWO/esSH9NbpWUfTMGQwCGIKfCmNlmFDNiI5gGhgMmEA+V1AEvKLeBNvvtBnailJtkrEa2OIISwrVAA==
dependencies:
undici-types "~7.8.0"
"@types/node@^22.14.1":
version "22.15.32"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.32.tgz#c301cc2275b535a5e54bb81d516b1d2e9afe06e5"
integrity sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==
dependencies:
undici-types "~6.21.0"
"@types/resolve@1.20.2":
version "1.20.2"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
@@ -1252,10 +1250,12 @@
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
"@vitejs/plugin-vue@^5.2.3":
version "5.2.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
"@vitejs/plugin-vue@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz#3f8c3cdeb709d9646770eebead1babe6409bf059"
integrity sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ==
dependencies:
"@rolldown/pluginutils" "1.0.0-beta.19"
"@volar/language-core@2.4.14", "@volar/language-core@~2.4.11":
version "2.4.14"
@@ -3259,11 +3259,6 @@ unbox-primitive@^1.1.0:
has-symbols "^1.1.0"
which-boxed-primitive "^1.1.1"
undici-types@~6.21.0:
version "6.21.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
undici-types@~7.8.0:
version "7.8.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294"
@@ -3322,10 +3317,10 @@ update-browserslist-db@^1.1.3:
escalade "^3.2.0"
picocolors "^1.1.1"
vite-plugin-pwa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-1.0.0.tgz#bd0ee81e71851bd45cae2f4aec90e52e9833152d"
integrity sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==
vite-plugin-pwa@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-1.0.1.tgz#fec0cc65ffd592bb9ca8374176bd9a2aeaafe97b"
integrity sha512-STyUomQbydj7vGamtgQYIJI0YsUZ3T4pJLGBQDQPhzMse6aGSncmEN21OV35PrFsmCvmtiH+Nu1JS1ke4RqBjQ==
dependencies:
debug "^4.3.6"
pretty-bytes "^6.1.1"
@@ -3368,13 +3363,13 @@ vue-draggable-plus@^0.6.0:
dependencies:
"@types/sortablejs" "^1.15.8"
vue-i18n@^11.1.3:
version "11.1.6"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.6.tgz#7a9d4933da4a3fc5e4d6bd4b8c4c9a7d6c18af23"
integrity sha512-+IbsW/sTZHj7U1w0rPOYJbuSB0/7DeO1nvUo3BxvO20OQgHs+ukJ3QeLqvoUA6DiLk+8SA9+djRmKC9+FC6cAg==
vue-i18n@^11.1.7:
version "11.1.7"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.7.tgz#a26c0224d1311ac89b82ff6d0ee45f68b5099237"
integrity sha512-CDrU7Cmyh1AxJjerQmipV9nVa//exVBdhTcWGlbfcDCN8bKp/uAe7Le6IoN4//5emIikbsSKe9Uofmf/xXkhOA==
dependencies:
"@intlify/core-base" "11.1.6"
"@intlify/shared" "11.1.6"
"@intlify/core-base" "11.1.7"
"@intlify/shared" "11.1.7"
"@vue/devtools-api" "^6.5.0"
vue-router@^4.5.0:
@@ -3417,10 +3412,10 @@ vuedraggable@^4.1.0:
dependencies:
sortablejs "1.14.0"
vuetify@^3.8.10:
version "3.8.10"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.8.10.tgz#73a33f900a9ad483e6c73b9b5a05851988e3e513"
integrity sha512-3BETdCGh3eB1cV5+dA+L5CYi62Q/Jb09H512GImeYzMHd2R+LntO0F5pNCqVB4KoxymQ4Jej3Q0J6AYmf0KD8w==
vuetify@^3.8.12:
version "3.8.12"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.8.12.tgz#7c433b8b036011bb0a800f08f5a53d61067eeae8"
integrity sha512-XRX/yRel/V5rlas12ovujVCo8RDb/NwICyef/DVYzybqbYz/UGHZd23sN5q1zw0h9jUN8httXI6ytWN7OFugmA==
w3c-xmlserializer@^5.0.0:
version "5.0.0"
@@ -3521,7 +3516,7 @@ which-typed-array@^1.1.16, which-typed-array@^1.1.19:
gopd "^1.2.0"
has-tostringtag "^1.0.2"
workbox-background-sync@7.3.0:
workbox-background-sync@7.3.0, workbox-background-sync@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz#b6340731a8d5b42b9e75a8a87c8806928e6e6303"
integrity sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==
@@ -3591,7 +3586,7 @@ workbox-core@7.3.0:
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.3.0.tgz#f24fb92041a0b7482fe2dd856544aaa9fa105248"
integrity sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==
workbox-expiration@7.3.0:
workbox-expiration@7.3.0, workbox-expiration@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.3.0.tgz#2c1ee1fdada34aa7e7474f706d5429c914bd10d2"
integrity sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==
@@ -3609,14 +3604,14 @@ workbox-google-analytics@7.3.0:
workbox-routing "7.3.0"
workbox-strategies "7.3.0"
workbox-navigation-preload@7.3.0:
workbox-navigation-preload@7.3.0, workbox-navigation-preload@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz#9d54693b9179d5175e66af5ef9a92d1b7cf3e605"
integrity sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==
dependencies:
workbox-core "7.3.0"
workbox-precaching@7.3.0:
workbox-precaching@7.3.0, workbox-precaching@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.3.0.tgz#a84663d69efdb334f25c04dba0a72ed3391c4da8"
integrity sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==
@@ -3644,14 +3639,14 @@ workbox-recipes@7.3.0:
workbox-routing "7.3.0"
workbox-strategies "7.3.0"
workbox-routing@7.3.0:
workbox-routing@7.3.0, workbox-routing@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.3.0.tgz#fc86296bc1155c112ee2c16b3180853586c30208"
integrity sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==
dependencies:
workbox-core "7.3.0"
workbox-strategies@7.3.0:
workbox-strategies@7.3.0, workbox-strategies@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.3.0.tgz#bb1530f205806895aacdea3639e6cf6bfb3a6cb0"
integrity sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==