diff --git a/cookbook/serializer.py b/cookbook/serializer.py
index a05fdab18..df6d4c117 100644
--- a/cookbook/serializer.py
+++ b/cookbook/serializer.py
@@ -592,7 +592,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
fields = (
'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
'updated_at', 'full_name')
- read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
+ read_only_fields = ('id', 'label', 'numchild', 'numrecipe', 'parent', 'image')
class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin):
diff --git a/cookbook/views/api.py b/cookbook/views/api.py
index 689256e1b..a524ba2fe 100644
--- a/cookbook/views/api.py
+++ b/cookbook/views/api.py
@@ -411,6 +411,7 @@ class MergeMixin(ViewSetMixin):
description='Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.',
type=int),
OpenApiParameter(name='tree', description='Return all self and children of {obj} with ID [int].', type=int),
+ OpenApiParameter(name='root_tree', description='Return all items belonging to the tree of the given {obj} id', type=int),
]),
move=extend_schema(parameters=[
OpenApiParameter(name="parent", description='The ID of the desired parent of the {obj}.', type=OpenApiTypes.INT,
@@ -423,6 +424,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
def get_queryset(self):
root = self.request.query_params.get('root', None)
tree = self.request.query_params.get('tree', None)
+ root_tree = self.request.query_params.get('root_tree', None)
if root:
if root.isnumeric():
@@ -441,10 +443,23 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self()
except self.model.DoesNotExist:
self.queryset = self.model.objects.none()
+ elif root_tree:
+ if root_tree.isnumeric():
+ try:
+ self.queryset = self.model.objects.get(id=int(root_tree)).get_root().get_descendants_and_self()
+ except self.model.DoesNotExist:
+ self.queryset = self.model.objects.none()
+
else:
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request,
serializer=self.serializer_class, tree=True)
- self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
+
+
+ self.queryset = self.queryset.filter(space=self.request.space)
+ # only order if not root_tree or tree mde because in these modes the sorting is relevant for the client
+ if not root_tree and not tree:
+ self.queryset = self.queryset.order_by(Lower('name').asc())
+
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class,
tree=True)
diff --git a/vue3/src/components/display/ShoppingLineItem.vue b/vue3/src/components/display/ShoppingLineItem.vue
index 188c316a5..dad0e37fc 100644
--- a/vue3/src/components/display/ShoppingLineItem.vue
+++ b/vue3/src/components/display/ShoppingLineItem.vue
@@ -15,8 +15,8 @@
- {{ $n(a.amount) }}
- {{ a.unit.name }}
+ {{ $n(a.amount) }}
+ {{ a.unit.name }}
diff --git a/vue3/src/components/display/ShoppingListView.vue b/vue3/src/components/display/ShoppingListView.vue
index c9ac8934a..88afeb773 100644
--- a/vue3/src/components/display/ShoppingListView.vue
+++ b/vue3/src/components/display/ShoppingListView.vue
@@ -123,6 +123,8 @@
+
+
@@ -180,7 +182,8 @@
-
+
diff --git a/vue3/src/components/inputs/HierarchyEditor.vue b/vue3/src/components/inputs/HierarchyEditor.vue
new file mode 100644
index 000000000..57acc51f9
--- /dev/null
+++ b/vue3/src/components/inputs/HierarchyEditor.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('AddChild')}}
+
+
+
+
+
+ {{$t('Parent')}}
+
+
+
+
+
+
+
+ {{$t('RemoveParent')}}
+
+
+ {{ $t('Edit') }}
+
+
+ {{ $t('Recipes') }}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vue3/src/components/model_editors/FoodEditor.vue b/vue3/src/components/model_editors/FoodEditor.vue
index 01360ad57..d07e89a1f 100644
--- a/vue3/src/components/model_editors/FoodEditor.vue
+++ b/vue3/src/components/model_editors/FoodEditor.vue
@@ -15,6 +15,7 @@
{{ $t('Food') }}
{{ $t('Properties') }}
{{ $t('Conversion') }}
+ {{ $t('Hierarchy') }}
{{ $t('Miscellaneous') }}
@@ -83,7 +84,7 @@
-
+
@@ -97,7 +98,7 @@
-
+
@@ -105,7 +106,19 @@
-
+
+
+
+
+
+
+
+
+
+
+
@@ -117,14 +130,6 @@
-
-
-
-
-
-
@@ -154,6 +159,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import FdcSearchDialog from "@/components/dialogs/FdcSearchDialog.vue";
import {openFdcPage} from "@/utils/fdc.ts";
import {DateTime} from "luxon";
+import HierarchyEditor from "@/components/inputs/HierarchyEditor.vue";
const props = defineProps({
@@ -212,7 +218,7 @@ onMounted(() => {
/**
* component specific state setup logic
*/
-function initializeEditor(){
+function initializeEditor() {
setupState(props.item, props.itemId, {
newItemFunction: () => {
editingObj.value.propertiesFoodAmount = 100
diff --git a/vue3/src/components/model_editors/KeywordEditor.vue b/vue3/src/components/model_editors/KeywordEditor.vue
index 9e5694ee8..333639d17 100644
--- a/vue3/src/components/model_editors/KeywordEditor.vue
+++ b/vue3/src/components/model_editors/KeywordEditor.vue
@@ -9,13 +9,29 @@
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()">
+
+
+
+ {{ $t('Keyword') }}
+ {{ $t('Hierarchy') }}
+
+
+
+
-
+
+
+
-
-
+
+
-
+
+
+
+
+
+
@@ -23,10 +39,11 @@
diff --git a/vue3/src/types/Models.ts b/vue3/src/types/Models.ts
index 9a83278a8..c29ad9521 100644
--- a/vue3/src/types/Models.ts
+++ b/vue3/src/types/Models.ts
@@ -1,6 +1,6 @@
import {
AccessToken,
- ApiApi, Automation, type AutomationTypeEnum, ConnectorConfig, CookLog, CustomFilter,
+ ApiApi, ApiKeywordMoveUpdateRequest, Automation, type AutomationTypeEnum, ConnectorConfig, CookLog, CustomFilter,
Food,
Ingredient,
InviteLink, Keyword,
@@ -85,7 +85,7 @@ type ModelTableHeaders = {
* custom type containing all attributes needed by the generic model system to properly handle all functions
*/
export type Model = {
- name: string,
+ name: EditorSupportedModels,
localizationKey: string,
localizationKeyDescription: string,
icon: string,
@@ -191,6 +191,7 @@ export const TFood = {
isPaginated: true,
isMerge: true,
+ isTree: true,
mergeAutomation: 'FOOD_ALIAS',
toStringKeys: ['name'],
@@ -234,6 +235,7 @@ export const TKeyword = {
isPaginated: true,
isMerge: true,
+ isTree: true,
mergeAutomation: 'KEYWORD_ALIAS',
toStringKeys: ['name'],
@@ -949,6 +951,22 @@ export class GenericModel {
}
}
+ /**
+ * move the given source object so that its parent is the given parentId.
+ * @param source object to change parent for
+ * @param parentId parent id to change the object to or 0 to remove parent
+ */
+ move(source: EditorSupportedTypes, parentId: number) {
+ if (!this.model.isTree) {
+ throw new Error('This model does not support trees!')
+ } else {
+ let moveRequestParams: any = {id: source.id, parent: parentId}
+ moveRequestParams[this.model.name.charAt(0).toLowerCase() + this.model.name.slice(1)] = source
+
+ return this.api[`api${this.model.name}MoveUpdate`](moveRequestParams)
+ }
+ }
+
/**
* gets a label for a specific object instance using the model toStringKeys property
* @param obj obj to get label for