overhauld space management and settings system

This commit is contained in:
vabene1111
2025-09-14 08:48:49 +02:00
parent f722d24eaa
commit 4f248afe76
46 changed files with 221 additions and 361 deletions

View File

@@ -368,10 +368,10 @@ class AiLogSerializer(serializers.ModelSerializer):
class SpaceSerializer(WritableNestedModelSerializer): class SpaceSerializer(WritableNestedModelSerializer):
created_by = UserSerializer(read_only=True) created_by = UserSerializer(read_only=True)
user_count = serializers.SerializerMethodField('get_user_count') user_count = serializers.SerializerMethodField('get_user_count', read_only=True)
recipe_count = serializers.SerializerMethodField('get_recipe_count') recipe_count = serializers.SerializerMethodField('get_recipe_count', read_only=True)
file_size_mb = serializers.SerializerMethodField('get_file_size_mb') file_size_mb = serializers.SerializerMethodField('get_file_size_mb', read_only=True)
ai_monthly_credits_used = serializers.SerializerMethodField('get_ai_monthly_credits_used') ai_monthly_credits_used = serializers.SerializerMethodField('get_ai_monthly_credits_used', read_only=True)
ai_default_provider = AiProviderSerializer(required=False, allow_null=True) ai_default_provider = AiProviderSerializer(required=False, allow_null=True)
food_inherit = FoodInheritFieldSerializer(many=True, required=False) food_inherit = FoodInheritFieldSerializer(many=True, required=False)
image = UserFileViewSerializer(required=False, many=False, allow_null=True) image = UserFileViewSerializer(required=False, many=False, allow_null=True)
@@ -413,8 +413,8 @@ class SpaceSerializer(WritableNestedModelSerializer):
name = None name = None
if 'name' in validated_data: if 'name' in validated_data:
name = validated_data['name'] name = validated_data['name']
space = create_space_for_user(self.context['request'].user, name) user_space = create_space_for_user(self.context['request'].user, name)
return space return user_space.space
def update(self, instance, validated_data): def update(self, instance, validated_data):
if 'ai_enabled' in validated_data and not self.context['request'].user.is_superuser: if 'ai_enabled' in validated_data and not self.context['request'].user.is_superuser:

View File

@@ -548,7 +548,7 @@ class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
serializer_class = SpaceSerializer serializer_class = SpaceSerializer
permission_classes = [IsReadOnlyDRF & CustomIsGuest | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope] permission_classes = [IsReadOnlyDRF & CustomIsGuest | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination pagination_class = DefaultPagination
http_method_names = ['get', 'post', 'patch'] http_method_names = ['get', 'post', 'put', 'patch']
def get_queryset(self): def get_queryset(self):
return self.queryset.filter( return self.queryset.filter(

View File

@@ -29,8 +29,6 @@ let routes = [
{path: 'meal-plan', component: () => import("@/components/settings/MealPlanSettings.vue"), name: 'MealPlanSettings', meta: {title: 'Settings'}}, {path: 'meal-plan', component: () => import("@/components/settings/MealPlanSettings.vue"), name: 'MealPlanSettings', meta: {title: 'Settings'}},
{path: 'search', component: () => import("@/components/settings/SearchSettings.vue"), name: 'SearchSettings', meta: {title: 'Settings'}}, {path: 'search', component: () => import("@/components/settings/SearchSettings.vue"), name: 'SearchSettings', meta: {title: 'Settings'}},
{path: 'space', component: () => import("@/components/settings/SpaceSettings.vue"), name: 'SpaceSettings', meta: {title: 'Settings'}}, {path: 'space', component: () => import("@/components/settings/SpaceSettings.vue"), name: 'SpaceSettings', meta: {title: 'Settings'}},
{path: 'space-members', component: () => import("@/components/settings/SpaceMemberSettings.vue"), name: 'SpaceMemberSettings', meta: {title: 'Settings'}},
{path: 'user-space', component: () => import("@/components/settings/UserSpaceSettings.vue"), name: 'UserSpaceSettings', meta: {title: 'Settings'}},
{path: 'open-data-import', component: () => import("@/components/settings/OpenDataImportSettings.vue"), name: 'OpenDataImportSettings', meta: {title: 'Settings'}}, {path: 'open-data-import', component: () => import("@/components/settings/OpenDataImportSettings.vue"), name: 'OpenDataImportSettings', meta: {title: 'Settings'}},
{path: 'export', component: () => import("@/components/settings/ExportDataSettings.vue"), name: 'ExportDataSettings', meta: {title: 'Settings'}}, {path: 'export', component: () => import("@/components/settings/ExportDataSettings.vue"), name: 'ExportDataSettings', meta: {title: 'Settings'}},
{path: 'api', component: () => import("@/components/settings/ApiSettings.vue"), name: 'ApiSettings', meta: {title: 'Settings'}}, {path: 'api', component: () => import("@/components/settings/ApiSettings.vue"), name: 'ApiSettings', meta: {title: 'Settings'}},

View File

@@ -0,0 +1,72 @@
<template>
<v-row v-if="props.space.name != undefined">
<v-col cols="12" md="4">
<v-card :to="{name: 'SearchPage'}">
<v-card-title><i class="fa-solid fa-book"></i> {{ $t('Recipes') }}</v-card-title>
<v-card-text>{{ $n(props.space.recipeCount) }} / {{ props.space.maxRecipes == 0 ? '∞' : $n(props.space.maxRecipes) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveRecipeLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.recipeCount / props.space.maxRecipes) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card :to="{name: 'ModelListPage', params: {model: 'UserSpace'}}">
<v-card-title><i class="fa-solid fa-users"></i> {{ $t('Users') }}</v-card-title>
<v-card-text>{{ $n(props.space.userCount) }} / {{ props.space.maxUsers == 0 ? '∞' : $n(props.space.maxUsers) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveUserLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.userCount / props.space.maxUsers) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card :to="{name: 'ModelListPage', params: {model: 'UserFile'}}">
<v-card-title><i class="fa-solid fa-file"></i> {{ $t('Files') }}</v-card-title>
<v-card-text v-if="props.space.maxFileStorageMb > -1">{{ $n(Math.round(props.space.fileSizeMb)) }} /
{{ props.space.maxFileStorageMb == 0 ? '' : $n(props.space.maxFileStorageMb) }}
MB
</v-card-text>
<v-card-text v-if="props.space.maxFileStorageMb == -1">{{ $t('file_upload_disabled') }}</v-card-text>
<v-progress-linear v-if="props.space.maxFileStorageMb > -1" :color="isSpaceAboveStorageLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.fileSizeMb / props.space.maxFileStorageMb) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
<v-card-title><i class="fa-solid hand-holding-dollar"></i> {{ $t('MonthlyCredits') }}</v-card-title>
<v-card-text>{{ $n(props.space.aiMonthlyCreditsUsed) }} / {{ $n(props.space.aiCreditsMonthly) }} {{ $t('Credits') }}
</v-card-text>
<v-progress-linear :model-value="props.space.aiMonthlyCreditsUsed" :max="props.space.aiCreditsMonthly" height="10"
></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
<v-card-title><i class="fa-solid hand-holding-dollar"></i> {{ $t('AiCreditsBalance') }}</v-card-title>
<v-card-text>{{ $n(props.space.aiCreditsBalance) }} {{ $t('Credits') }}
</v-card-text>
<v-progress-linear height="10"
></v-progress-linear>
</v-card>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import {PropType} from "vue";
import {Space} from "@/openapi";
import {isSpaceAboveRecipeLimit, isSpaceAboveStorageLimit, isSpaceAboveUserLimit} from "@/utils/logic_utils.ts";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const props = defineProps({
space: {type: {} as PropType<Space>, required: true},
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,47 @@
<template>
<v-alert color="primary" variant="tonal" v-if="useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://tandoor.dev/manage" target="_blank">{{ $t('ManageSubscription') }}</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextHosted') }}</p>
</v-alert>
<v-alert color="primary" variant="tonal" v-if="!useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://github.com/sponsors/vabene1111" target="_blank"><i class="fa-brands fa-github"></i> GitHub Sponsors
</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextSelfhosted') }}</p>
</v-alert>
</template>
<script setup lang="ts">
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
</script>
<style scoped>
</style>

View File

@@ -27,7 +27,9 @@
<user-file-field v-model="editingObj.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field> <user-file-field v-model="editingObj.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field>
<v-textarea v-model="editingObj.message" :label="$t('Message')"></v-textarea> <v-textarea v-model="editingObj.message" :label="$t('Message')" clearable></v-textarea>
<space-limits-info :space="editingObj" :show-thank-you="false"></space-limits-info>
</v-form> </v-form>
</v-tabs-window-item> </v-tabs-window-item>
@@ -81,13 +83,14 @@
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue"; import {onMounted, PropType, ref, watch} from "vue";
import {ConnectorConfig, Space} from "@/openapi"; import {ApiApi, ConnectorConfig, Space} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue"; import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions"; import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import UserFileField from "@/components/inputs/UserFileField.vue"; import UserFileField from "@/components/inputs/UserFileField.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue"; import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts"; import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import editor from "mavon-editor"; import editor from "mavon-editor";
import SpaceLimitsInfo from "@/components/display/SpaceLimitsInfo.vue";
const props = defineProps({ const props = defineProps({
item: {type: {} as PropType<Space>, required: false, default: null}, item: {type: {} as PropType<Space>, required: false, default: null},

View File

@@ -2,7 +2,10 @@
<v-form> <v-form>
<p class="text-h6">{{ $t('Profile') }}</p> <p class="text-h6">{{ $t('Profile') }}</p>
<v-divider class="mb-3"></v-divider> <v-divider class="mb-3"></v-divider>
<v-text-field :label="$t('Username')" v-model="user.username" disabled :hint="$t('theUsernameCannotBeChanged')" persistent-hint></v-text-field>
<thank-you-note></thank-you-note>
<v-text-field class="mt-3" :label="$t('Username')" v-model="user.username" disabled :hint="$t('theUsernameCannotBeChanged')" persistent-hint></v-text-field>
<!-- <v-label>Avatar</v-label><br/>--> <!-- <v-label>Avatar</v-label><br/>-->
<!-- <v-avatar class="mt-3 mb-3" style="height: 10vh; width: 10vh" color="info">V</v-avatar> Feature coming in a future Version of Tandoor.--> <!-- <v-avatar class="mt-3 mb-3" style="height: 10vh; width: 10vh" color="info">V</v-avatar> Feature coming in a future Version of Tandoor.-->
@@ -39,6 +42,7 @@ import {ApiApi, User} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore"; import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore"; import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useDjangoUrls} from "@/composables/useDjangoUrls"; import {useDjangoUrls} from "@/composables/useDjangoUrls";
import ThankYouNote from "@/components/display/ThankYouNote.vue";
const {getDjangoUrl} = useDjangoUrls() const {getDjangoUrl} = useDjangoUrls()

View File

@@ -1,122 +0,0 @@
<template>
<v-form>
<p class="text-h6">{{ $t('SpaceMembers') }}</p>
<v-divider></v-divider>
<p class="text-subtitle-2">{{ $t('SpaceMemberHelp') }}</p>
<v-data-table :items="spaceUserSpaces" :headers="userTableHeaders" density="compact" :hide-default-footer="spaceUserSpaces.length < 10" class="mt-3">
<template #item.groups="{item}">
<span v-for="g in item.groups">{{ g.name }}&nbsp;</span>
</template>
<template #item.edit="{item}">
<v-btn color="edit" size="small" v-if="item.user.id != useUserPreferenceStore().activeSpace.createdBy.id">
<v-icon icon="$edit"></v-icon>
<model-edit-dialog model="UserSpace" :item="item" @delete="deleteUserSpace(item)" class="mt-2"></model-edit-dialog>
</v-btn>
<v-chip color="edit" v-else>{{ $t('Owner') }}</v-chip>
</template>
</v-data-table>
<p class="text-h6 mt-3">{{ $t('Invites') }}
<v-btn size="small" class="float-right" prepend-icon="$create" color="create">
{{ $t('New') }}
<model-edit-dialog model="InviteLink" @delete="deleteInviteLink" @create="item => spaceInviteLinks.push(item)" class="mt-2"></model-edit-dialog>
</v-btn>
</p>
<v-divider class="mb-3"></v-divider>
<v-data-table :items="spaceInviteLinks" :headers="inviteTableHeaders" density="compact" :hide-default-footer="spaceInviteLinks.length < 10">
<template #item.reusable="{item}">
<v-icon icon="fa-solid fa-check" color="success" v-if="item.reusable"></v-icon>
<v-icon icon="fa-solid fa-times" color="error" v-if="!item.reusable"></v-icon>
</template>
<template #item.edit="{item}">
<btn-copy size="small" :copy-value="inviteLinkUrl(item)" class="me-1"></btn-copy>
<v-btn color="edit" size="small">
<v-icon icon="$edit"></v-icon>
<model-edit-dialog model="InviteLink" :item="item" @delete="deleteInviteLink(item)" class="mt-2"></model-edit-dialog>
</v-btn>
</template>
</v-data-table>
</v-form>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {ApiApi, InviteLink, UserSpace} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import {useDjangoUrls} from "@/composables/useDjangoUrls.ts";
const {t} = useI18n()
const spaceUserSpaces = ref([] as UserSpace[])
const spaceInviteLinks = ref([] as InviteLink[])
const userTableHeaders = [
{title: t('Username'), key: 'user.username'},
{title: t('Role'), key: 'groups'},
{title: t('Edit'), key: 'edit', align: 'end'},
]
const inviteTableHeaders = [
{title: 'ID', key: 'id'},
{title: t('Email'), key: 'email'},
{title: t('Role'), key: 'group.name'},
{title: t('Reusable'), key: 'reusable'},
{title: t('Edit'), key: 'edit', align: 'end'},
]
onMounted(() => {
const api = new ApiApi()
api.apiUserSpaceList().then(r => {
spaceUserSpaces.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
api.apiInviteLinkList({unused: true}).then(r => {
spaceInviteLinks.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
/**
* delete userspace from client list (database handled by editor)
* @param userSpace UserSpace object that was deleted
*/
function deleteUserSpace(userSpace: UserSpace) {
spaceUserSpaces.value.splice(spaceUserSpaces.value.indexOf(userSpace) - 1, 1)
}
/**
* delete invite link from client list (database handled by editor)
* @param inviteLink InviteLink object that was deleted
*/
function deleteInviteLink(inviteLink: InviteLink) {
spaceInviteLinks.value.splice(spaceInviteLinks.value.indexOf(inviteLink) - 1, 1)
}
/**
* returns url for invite link
* @param inviteLink InviteLink object to create url for
*/
function inviteLinkUrl(inviteLink: InviteLink) {
return useDjangoUrls().getDjangoUrl(`/invite/${inviteLink.uuid}`)
}
</script>
<style scoped>
</style>

View File

@@ -3,172 +3,17 @@
<p class="text-h6">{{ useUserPreferenceStore().activeSpace.name }}</p> <p class="text-h6">{{ useUserPreferenceStore().activeSpace.name }}</p>
<v-divider class="mb-3"></v-divider> <v-divider class="mb-3"></v-divider>
<v-row v-if="space.name != undefined"> <space-editor :item-id="useUserPreferenceStore().activeSpace.id!"></space-editor>
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-book"></i> {{ $t('Recipes') }}</v-card-title>
<v-card-text>{{ $n(space.recipeCount) }} / {{ space.maxRecipes == 0 ? '∞' : $n(space.maxRecipes) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveRecipeLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.recipeCount / space.maxRecipes) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-users"></i> {{ $t('Users') }}</v-card-title>
<v-card-text>{{ $n(space.userCount) }} / {{ space.maxUsers == 0 ? '∞' : $n(space.maxUsers) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveUserLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.userCount / space.maxUsers) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-file"></i> {{ $t('Files') }}</v-card-title>
<v-card-text v-if="space.maxFileStorageMb > -1">{{ $n(Math.round(space.fileSizeMb)) }} / {{ space.maxFileStorageMb == 0 ? '' : $n(space.maxFileStorageMb) }}
MB
</v-card-text>
<v-card-text v-if="space.maxFileStorageMb == -1">{{ $t('file_upload_disabled') }}</v-card-text>
<v-progress-linear v-if="space.maxFileStorageMb > -1" :color="isSpaceAboveStorageLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.fileSizeMb / space.maxFileStorageMb) * 100"></v-progress-linear>
</v-card>
</v-col>
</v-row>
<v-divider class="mt-3 mb-3"></v-divider>
<v-alert color="primary" variant="tonal" v-if="useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://tandoor.dev/manage" target="_blank">{{ $t('ManageSubscription') }}</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextHosted') }}</p>
</v-alert>
<v-alert color="primary" variant="tonal" v-if="!useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://github.com/sponsors/vabene1111" target="_blank"><i class="fa-brands fa-github"></i> GitHub Sponsors
</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextSelfhosted') }}</p>
</v-alert>
<p class="text-h6 mt-2">{{ $t('Settings') }}</p>
<v-divider class="mb-2"></v-divider>
<v-text-field v-model="space.name" :label="$t('Name')"></v-text-field>
<user-file-field v-model="space.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field>
<v-textarea v-model="space.message" :label="$t('Message')"></v-textarea>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<p class="text-h6 mt-2">{{ $t('AI') }}</p>
<v-divider class="mb-2"></v-divider>
<p class="text-disabled font-italic text-body-2">
<span v-if="useUserPreferenceStore().serverSettings.hosted">
{{ $t('AISettingsHostedHelp') }}
</span>
<span v-else>
{{ $t('SettingsOnlySuperuser') }}
</span>
</p>
<v-checkbox v-model="space.aiEnabled" :label="$t('Enabled')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser" hide-details></v-checkbox>
<template v-if="space.aiEnabled">
<model-select model="AiProvider" :label="$t('Default')" v-model="space.aiDefaultProvider"></model-select>
<v-number-input v-model="space.aiCreditsMonthly" :precision="2" :label="$t('MonthlyCredits')"
:disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
<v-number-input v-model="space.aiCreditsBalance" :precision="4" :label="$t('AiCreditsBalance')"
:disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
</template>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<v-divider class="mt-4 mb-2"></v-divider>
<h2>{{ $t('Cosmetic') }}</h2>
<span>{{ $t('Space_Cosmetic_Settings') }}</span>
<v-label class="mt-4">{{ $t('Nav_Color') }}</v-label>
<v-color-picker v-model="space.navBgColor" class="mb-4" mode="hex" :modes="['hex']" show-swatches
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
<v-btn class="mb-4" @click="space.navBgColor = ''">{{ $t('Reset') }}</v-btn>
<user-file-field v-model="space.navLogo" :label="$t('Logo')" :hint="$t('CustomNavLogoHelp')" persistent-hint></user-file-field>
<user-file-field v-model="space.logoColor32" :label="$t('Logo') + ' 32x32px'"></user-file-field>
<user-file-field v-model="space.logoColor128" :label="$t('Logo') + ' 128x128px'"></user-file-field>
<user-file-field v-model="space.logoColor144" :label="$t('Logo') + ' 144x144px'"></user-file-field>
<user-file-field v-model="space.logoColor180" :label="$t('Logo') + ' 180x180px'"></user-file-field>
<user-file-field v-model="space.logoColor192" :label="$t('Logo') + ' 192x192px'"></user-file-field>
<user-file-field v-model="space.logoColor512" :label="$t('Logo') + ' 512x512px'"></user-file-field>
<user-file-field v-model="space.logoColorSvg" :label="$t('Logo') + ' SVG'"></user-file-field>
<user-file-field v-model="space.customSpaceTheme" :label="$t('CustomTheme') + ' CSS'"></user-file-field>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
</v-form> </v-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {onMounted, ref} from "vue";
import {ApiApi, Space} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import UserFileField from "@/components/inputs/UserFileField.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {isSpaceAboveRecipeLimit, isSpaceAboveStorageLimit, isSpaceAboveUserLimit} from "@/utils/logic_utils";
const space = ref({} as Space)
onMounted(() => {
loadSpace()
})
function loadSpace() {
let api = new ApiApi()
api.apiSpaceCurrentRetrieve().then(r => {
space.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
function updateSpace() {
let api = new ApiApi()
api.apiSpacePartialUpdate({id: space.value.id, patchedSpace: space.value}).then(r => {
space.value = r
useUserPreferenceStore().activeSpace = Object.assign({}, space.value)
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS, space.value)
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
})
}
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import SpaceLimitsInfo from "@/components/display/SpaceLimitsInfo.vue";
import SpaceEditor from "@/components/model_editors/SpaceEditor.vue";
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,66 +0,0 @@
<template>
<v-row>
<v-col>
<p class="text-h6">
{{ $t('YourSpaces') }}
<v-btn color="create" prepend-icon="$add" class="float-right" size="small" @click="createNewSpace()">{{ $t('New') }}</v-btn>
</p>
<v-divider></v-divider>
</v-col>
</v-row>
<v-row>
<v-col cols="6" v-for="s in spaces" :key="s.id">
<v-card @click="useUserPreferenceStore().switchSpace(s)">
<v-img height="200px" cover :src="(s.image !== undefined) ? s.image?.preview : recipeDefaultImage" :alt="$t('Image')"></v-img>
<v-card-title>{{ s.name }}
<v-chip variant="tonal" density="compact" color="error" v-if="s.id == useUserPreferenceStore().activeSpace.id">{{ $t('active') }}</v-chip>
</v-card-title>
<v-card-subtitle>{{ $t('created_by') }} {{ s.createdBy.displayName }}</v-card-subtitle>
</v-card>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {ApiApi, type FoodInheritField, Space} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import recipeDefaultImage from '../../assets/recipe_no_image.svg'
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useDjangoUrls} from "@/composables/useDjangoUrls";
const {getDjangoUrl} = useDjangoUrls()
const spaces = ref([] as Space[])
onMounted(() => {
loadSpaces()
})
function loadSpaces() {
const api = new ApiApi()
api.apiSpaceList().then(r => {
spaces.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
function createNewSpace() {
let api = new ApiApi()
api.apiSpaceCreate({space: {} as Space}).then(r => {
spaces.value.push(r)
}).catch(err => {
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
})
}
</script>
<style scoped>
</style>

View File

@@ -288,6 +288,7 @@
"Show_as_header": "", "Show_as_header": "",
"Single": "", "Single": "",
"Size": "", "Size": "",
"Skip": "",
"Sort_by_new": "", "Sort_by_new": "",
"Space": "", "Space": "",
"SpaceHelp": "", "SpaceHelp": "",

View File

@@ -281,6 +281,7 @@
"Show_as_header": "Показване като заглавка", "Show_as_header": "Показване като заглавка",
"Single": "Единичен", "Single": "Единичен",
"Size": "Размер", "Size": "Размер",
"Skip": "",
"Sort_by_new": "Сортиране по ново", "Sort_by_new": "Сортиране по ново",
"Space": "", "Space": "",
"SpaceHelp": "", "SpaceHelp": "",

View File

@@ -365,6 +365,7 @@
"Show_as_header": "Mostreu com a títol", "Show_as_header": "Mostreu com a títol",
"Single": "Únic/a", "Single": "Únic/a",
"Size": "Mida", "Size": "Mida",
"Skip": "",
"Social_Authentication": "Identificació amb Xarxes Socials", "Social_Authentication": "Identificació amb Xarxes Socials",
"Sort_by_new": "Ordenar a partir del més nou", "Sort_by_new": "Ordenar a partir del més nou",
"Space": "", "Space": "",

View File

@@ -360,6 +360,7 @@
"Show_as_header": "Nastav jako nadpis", "Show_as_header": "Nastav jako nadpis",
"Single": "Jednoduchý", "Single": "Jednoduchý",
"Size": "Velikost", "Size": "Velikost",
"Skip": "",
"Social_Authentication": "Přihlašování pomocí účtů sociálních sítí", "Social_Authentication": "Přihlašování pomocí účtů sociálních sítí",
"Sort_by_new": "Seřadit od nejnovějšího", "Sort_by_new": "Seřadit od nejnovějšího",
"Space": "", "Space": "",

View File

@@ -365,6 +365,7 @@
"Show_as_header": "Vis som rubrik", "Show_as_header": "Vis som rubrik",
"Single": "Enkel", "Single": "Enkel",
"Size": "Størrelse", "Size": "Størrelse",
"Skip": "",
"Social_Authentication": "Social authenticering", "Social_Authentication": "Social authenticering",
"Sort_by_new": "Sorter efter nylige", "Sort_by_new": "Sorter efter nylige",
"Space": "", "Space": "",

View File

@@ -503,6 +503,7 @@
"Show_as_header": "Als Überschrift", "Show_as_header": "Als Überschrift",
"Single": "Einzeln", "Single": "Einzeln",
"Size": "Größe", "Size": "Größe",
"Skip": "Überspringen",
"Social_Authentication": "Login über Drittanbieter", "Social_Authentication": "Login über Drittanbieter",
"Sort_by_new": "Nach Neueste sortieren", "Sort_by_new": "Nach Neueste sortieren",
"Source": "Quelle", "Source": "Quelle",

View File

@@ -365,6 +365,7 @@
"Show_as_header": "Εμφάνιση ως κεφαλίδα", "Show_as_header": "Εμφάνιση ως κεφαλίδα",
"Single": "Ενικός", "Single": "Ενικός",
"Size": "Μέγεθος", "Size": "Μέγεθος",
"Skip": "",
"Social_Authentication": "Ταυτοποίηση μέσω κοινωνικών δικτύων", "Social_Authentication": "Ταυτοποίηση μέσω κοινωνικών δικτύων",
"Sort_by_new": "Ταξινόμηση κατά νέο", "Sort_by_new": "Ταξινόμηση κατά νέο",
"Space": "", "Space": "",

View File

@@ -501,6 +501,7 @@
"Show_as_header": "Show as header", "Show_as_header": "Show as header",
"Single": "Single", "Single": "Single",
"Size": "Size", "Size": "Size",
"Skip": "Skip",
"Social_Authentication": "Social Authentication", "Social_Authentication": "Social Authentication",
"Sort_by_new": "Sort by new", "Sort_by_new": "Sort by new",
"Source": "Source", "Source": "Source",

View File

@@ -484,6 +484,7 @@
"Show_as_header": "Mostrar como encabezado", "Show_as_header": "Mostrar como encabezado",
"Single": "Simple", "Single": "Simple",
"Size": "Tamaño", "Size": "Tamaño",
"Skip": "",
"Social_Authentication": "Autenticación Social", "Social_Authentication": "Autenticación Social",
"Sort_by_new": "Ordenar por novedades", "Sort_by_new": "Ordenar por novedades",
"SourceImportHelp": "Importar JSON en formato schema.org/recipe o páginas HTML con recetas en formato JSON+LD o microdatos.", "SourceImportHelp": "Importar JSON en formato schema.org/recipe o páginas HTML con recetas en formato JSON+LD o microdatos.",

View File

@@ -353,6 +353,7 @@
"Show_as_header": "Näytä otsikkona", "Show_as_header": "Näytä otsikkona",
"Single": "Yksittäinen", "Single": "Yksittäinen",
"Size": "Koko", "Size": "Koko",
"Skip": "",
"Social_Authentication": "Sosiaalinen Todennus", "Social_Authentication": "Sosiaalinen Todennus",
"Sort_by_new": "Lajittele uusien mukaan", "Sort_by_new": "Lajittele uusien mukaan",
"Space": "", "Space": "",

View File

@@ -498,6 +498,7 @@
"Show_as_header": "Montrer comme en-tête", "Show_as_header": "Montrer comme en-tête",
"Single": "Unique", "Single": "Unique",
"Size": "Taille", "Size": "Taille",
"Skip": "",
"Social_Authentication": "Authentification Sociale", "Social_Authentication": "Authentification Sociale",
"Sort_by_new": "Trier par nouveautés", "Sort_by_new": "Trier par nouveautés",
"Source": "Source", "Source": "Source",

View File

@@ -365,6 +365,7 @@
"Show_as_header": "הצג בתור כותרת", "Show_as_header": "הצג בתור כותרת",
"Single": "בודד", "Single": "בודד",
"Size": "גודל", "Size": "גודל",
"Skip": "",
"Social_Authentication": "אימות חברתי", "Social_Authentication": "אימות חברתי",
"Sort_by_new": "סדר ע\"י חדש", "Sort_by_new": "סדר ע\"י חדש",
"Space": "", "Space": "",

View File

@@ -365,6 +365,7 @@
"Show_as_header": "Prikaži kao zaglavlje", "Show_as_header": "Prikaži kao zaglavlje",
"Single": "Jedna", "Single": "Jedna",
"Size": "Veličina", "Size": "Veličina",
"Skip": "",
"Social_Authentication": "Autentifikacija putem društvenih mreža", "Social_Authentication": "Autentifikacija putem društvenih mreža",
"Sort_by_new": "Poredaj po novom", "Sort_by_new": "Poredaj po novom",
"Space": "", "Space": "",

View File

@@ -333,6 +333,7 @@
"Show_as_header": "Megjelenítés címként", "Show_as_header": "Megjelenítés címként",
"Single": "Egyetlen", "Single": "Egyetlen",
"Size": "Méret", "Size": "Méret",
"Skip": "",
"Sort_by_new": "Rendezés legújabbak szerint", "Sort_by_new": "Rendezés legújabbak szerint",
"Space": "", "Space": "",
"SpaceHelp": "", "SpaceHelp": "",

View File

@@ -150,6 +150,7 @@
"Shopping_list": "Գնումների ցուցակ", "Shopping_list": "Գնումների ցուցակ",
"Show_as_header": "Ցույց տալ որպես խորագիր", "Show_as_header": "Ցույց տալ որպես խորագիր",
"Size": "", "Size": "",
"Skip": "",
"Sort_by_new": "Տեսակավորել ըստ նորերի", "Sort_by_new": "Տեսակավորել ըստ նորերի",
"Space": "", "Space": "",
"SpaceHelp": "", "SpaceHelp": "",

View File

@@ -309,6 +309,7 @@
"Show_as_header": "Tampilkan sebagai tajuk", "Show_as_header": "Tampilkan sebagai tajuk",
"Single": "", "Single": "",
"Size": "Ukuran", "Size": "Ukuran",
"Skip": "",
"Social_Authentication": "", "Social_Authentication": "",
"Sort_by_new": "Urutkan berdasarkan baru", "Sort_by_new": "Urutkan berdasarkan baru",
"Space": "", "Space": "",

View File

@@ -363,6 +363,7 @@
"Show_as_header": "", "Show_as_header": "",
"Single": "", "Single": "",
"Size": "", "Size": "",
"Skip": "",
"Social_Authentication": "", "Social_Authentication": "",
"Sort_by_new": "", "Sort_by_new": "",
"Space": "", "Space": "",

View File

@@ -500,6 +500,7 @@
"Show_as_header": "Mostra come intestazione", "Show_as_header": "Mostra come intestazione",
"Single": "Singolo", "Single": "Singolo",
"Size": "Dimensione", "Size": "Dimensione",
"Skip": "",
"Social_Authentication": "Autenticazione social", "Social_Authentication": "Autenticazione social",
"Sort_by_new": "Prima i nuovi", "Sort_by_new": "Prima i nuovi",
"Source": "Fonte", "Source": "Fonte",

View File

@@ -337,6 +337,7 @@
"Show_as_header": "Rodyti kaip antraštę", "Show_as_header": "Rodyti kaip antraštę",
"Single": "", "Single": "",
"Size": "", "Size": "",
"Skip": "",
"Social_Authentication": "", "Social_Authentication": "",
"Sort_by_new": "Rūšiuoti pagal naujumą", "Sort_by_new": "Rūšiuoti pagal naujumą",
"Space": "", "Space": "",

View File

@@ -365,6 +365,7 @@
"Show_as_header": "", "Show_as_header": "",
"Single": "", "Single": "",
"Size": "", "Size": "",
"Skip": "",
"Social_Authentication": "", "Social_Authentication": "",
"Sort_by_new": "", "Sort_by_new": "",
"Space": "", "Space": "",

View File

@@ -347,6 +347,7 @@
"Show_as_header": "Vis som overskrift", "Show_as_header": "Vis som overskrift",
"Single": "", "Single": "",
"Size": "Størrelse", "Size": "Størrelse",
"Skip": "",
"Social_Authentication": "", "Social_Authentication": "",
"Sort_by_new": "Sorter etter nyest", "Sort_by_new": "Sorter etter nyest",
"Space": "", "Space": "",

View File

@@ -501,6 +501,7 @@
"Show_as_header": "Toon als koptekst", "Show_as_header": "Toon als koptekst",
"Single": "Enkele", "Single": "Enkele",
"Size": "Grootte", "Size": "Grootte",
"Skip": "",
"Social_Authentication": "Authenticeren met sociale media-account", "Social_Authentication": "Authenticeren met sociale media-account",
"Sort_by_new": "Sorteer op nieuw", "Sort_by_new": "Sorteer op nieuw",
"Source": "Bron", "Source": "Bron",

View File

@@ -391,6 +391,7 @@
"Show_as_header": "Pokaż jako nagłówek", "Show_as_header": "Pokaż jako nagłówek",
"Single": "Pojedynczy", "Single": "Pojedynczy",
"Size": "Rozmiar", "Size": "Rozmiar",
"Skip": "",
"Social_Authentication": "Uwierzytelnianie społecznościowe", "Social_Authentication": "Uwierzytelnianie społecznościowe",
"Sort_by_new": "Sortuj według nowych", "Sort_by_new": "Sortuj według nowych",
"Space": "", "Space": "",

View File

@@ -299,6 +299,7 @@
"Show_Week_Numbers": "Mostrar números das semanas?", "Show_Week_Numbers": "Mostrar números das semanas?",
"Show_as_header": "Mostrar como cabeçalho", "Show_as_header": "Mostrar como cabeçalho",
"Size": "Tamanho", "Size": "Tamanho",
"Skip": "",
"Sort_by_new": "Ordenar por mais recente", "Sort_by_new": "Ordenar por mais recente",
"Space": "", "Space": "",
"SpaceHelp": "", "SpaceHelp": "",

View File

@@ -439,6 +439,7 @@
"Show_as_header": "Mostrar como título", "Show_as_header": "Mostrar como título",
"Single": "Simples", "Single": "Simples",
"Size": "Tamanho", "Size": "Tamanho",
"Skip": "",
"Social_Authentication": "Autenticação social", "Social_Authentication": "Autenticação social",
"Sort_by_new": "Ordenar por novos", "Sort_by_new": "Ordenar por novos",
"Space": "", "Space": "",

View File

@@ -321,6 +321,7 @@
"Show_as_header": "Afișare ca antet", "Show_as_header": "Afișare ca antet",
"Single": "Singur", "Single": "Singur",
"Size": "Marime", "Size": "Marime",
"Skip": "",
"Social_Authentication": "Autentificare socială", "Social_Authentication": "Autentificare socială",
"Sort_by_new": "Sortare după nou", "Sort_by_new": "Sortare după nou",
"Space": "", "Space": "",

View File

@@ -498,6 +498,7 @@
"Show_as_header": "Показывать как заголовок", "Show_as_header": "Показывать как заголовок",
"Single": "Одиночный", "Single": "Одиночный",
"Size": "Размер", "Size": "Размер",
"Skip": "",
"Social_Authentication": "Социальная аутентификация", "Social_Authentication": "Социальная аутентификация",
"Sort_by_new": "Сортировка по новизне", "Sort_by_new": "Сортировка по новизне",
"Source": "Источник", "Source": "Источник",

View File

@@ -500,6 +500,7 @@
"Show_as_header": "Prikaži kot glavo", "Show_as_header": "Prikaži kot glavo",
"Single": "Ena", "Single": "Ena",
"Size": "Velikost", "Size": "Velikost",
"Skip": "",
"Social_Authentication": "Socialna avtentikacija", "Social_Authentication": "Socialna avtentikacija",
"Sort_by_new": "Razvrsti po novih", "Sort_by_new": "Razvrsti po novih",
"Source": "Vir", "Source": "Vir",

View File

@@ -402,6 +402,7 @@
"Show_as_header": "Visa som rubrik", "Show_as_header": "Visa som rubrik",
"Single": "Enstaka", "Single": "Enstaka",
"Size": "Storlek", "Size": "Storlek",
"Skip": "",
"Social_Authentication": "Social autentisering", "Social_Authentication": "Social autentisering",
"Sort_by_new": "Sortera efter ny", "Sort_by_new": "Sortera efter ny",
"Space": "", "Space": "",

View File

@@ -365,6 +365,7 @@
"Show_as_header": "Başlık olarak göster", "Show_as_header": "Başlık olarak göster",
"Single": "Tek", "Single": "Tek",
"Size": "Boyut", "Size": "Boyut",
"Skip": "",
"Social_Authentication": "Sosyal Kimlik Doğrulama", "Social_Authentication": "Sosyal Kimlik Doğrulama",
"Sort_by_new": "Yeniye göre sırala", "Sort_by_new": "Yeniye göre sırala",
"Space": "", "Space": "",

View File

@@ -323,6 +323,7 @@
"Show_as_header": "Показати як заголовок", "Show_as_header": "Показати як заголовок",
"Single": "", "Single": "",
"Size": "Розмір", "Size": "Розмір",
"Skip": "",
"Sort_by_new": "Сортувати за новими", "Sort_by_new": "Сортувати за новими",
"Space": "", "Space": "",
"SpaceHelp": "", "SpaceHelp": "",

View File

@@ -365,6 +365,7 @@
"Show_as_header": "显示标题", "Show_as_header": "显示标题",
"Single": "单个", "Single": "单个",
"Size": "大小", "Size": "大小",
"Skip": "",
"Social_Authentication": "社交认证", "Social_Authentication": "社交认证",
"Sort_by_new": "按新旧排序", "Sort_by_new": "按新旧排序",
"Space": "", "Space": "",

View File

@@ -499,6 +499,7 @@
"Show_as_header": "顯示為標題", "Show_as_header": "顯示為標題",
"Single": "單一", "Single": "單一",
"Size": "大小", "Size": "大小",
"Skip": "",
"Social_Authentication": "社交認證", "Social_Authentication": "社交認證",
"Sort_by_new": "按最新排序", "Sort_by_new": "按最新排序",
"Source": "來源", "Source": "來源",

View File

@@ -1803,6 +1803,11 @@ export interface ApiSpaceRetrieveRequest {
id: number; id: number;
} }
export interface ApiSpaceUpdateRequest {
id: number;
space?: Omit<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'>;
}
export interface ApiStepCreateRequest { export interface ApiStepCreateRequest {
step: Omit<Step, 'instructionsMarkdown'|'stepRecipeData'|'numrecipe'>; step: Omit<Step, 'instructionsMarkdown'|'stepRecipeData'|'numrecipe'>;
} }
@@ -13265,6 +13270,46 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value(); return await response.value();
} }
/**
* logs request counts to redis cache total/per user/
*/
async apiSpaceUpdateRaw(requestParameters: ApiSpaceUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Space>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiSpaceUpdate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/space/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PUT',
headers: headerParameters,
query: queryParameters,
body: SpaceToJSON(requestParameters['space']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => SpaceFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiSpaceUpdate(requestParameters: ApiSpaceUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Space> {
const response = await this.apiSpaceUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/** /**
* logs request counts to redis cache total/per user/ * logs request counts to redis cache total/per user/
*/ */

View File

@@ -13,9 +13,7 @@
<v-list-item :to="{name: 'SearchSettings'}" prepend-icon="$search">{{ $t('Search') }}</v-list-item> <v-list-item :to="{name: 'SearchSettings'}" prepend-icon="$search">{{ $t('Search') }}</v-list-item>
<v-divider></v-divider> <v-divider></v-divider>
<v-list-subheader>Space</v-list-subheader> <v-list-subheader>Space</v-list-subheader>
<v-list-item :to="{name: 'UserSpaceSettings'}" prepend-icon="$spaces">{{ $t('YourSpaces') }}</v-list-item>
<v-list-item :to="{name: 'SpaceSettings'}" prepend-icon="$settings">{{ $t('SpaceSettings') }}</v-list-item> <v-list-item :to="{name: 'SpaceSettings'}" prepend-icon="$settings">{{ $t('SpaceSettings') }}</v-list-item>
<v-list-item :to="{name: 'SpaceMemberSettings'}" prepend-icon="fa-solid fa-users">{{ $t('SpaceMembers') }}</v-list-item>
<v-list-item :to="{name: 'OpenDataImportSettings'}" prepend-icon="fa-solid fa-cloud-arrow-down">{{ $t('Open_Data_Import') }}</v-list-item> <v-list-item :to="{name: 'OpenDataImportSettings'}" prepend-icon="fa-solid fa-cloud-arrow-down">{{ $t('Open_Data_Import') }}</v-list-item>
<v-list-item :to="{name: 'ExportDataSettings'}" prepend-icon="fa-solid fa-file-export">{{ $t('Export') }}</v-list-item> <v-list-item :to="{name: 'ExportDataSettings'}" prepend-icon="fa-solid fa-file-export">{{ $t('Export') }}</v-list-item>
<v-divider></v-divider> <v-divider></v-divider>

View File

@@ -39,6 +39,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
</template> </template>
<template #next> <template #next>
<v-btn @click="finishWelcome()" color="warning" class="me-2" :loading="loading">{{ $t('Skip') }}</v-btn>
<v-btn @click="updateSpaceAndUserSettings()" :loading="loading" color="success">{{ $t('Next') }}</v-btn> <v-btn @click="updateSpaceAndUserSettings()" :loading="loading" color="success">{{ $t('Next') }}</v-btn>
</template> </template>
</v-stepper-actions> </v-stepper-actions>
@@ -194,8 +195,10 @@ onMounted(() => {
function finishWelcome(target: RouteLocationRaw = {name: 'StartPage'}) { function finishWelcome(target: RouteLocationRaw = {name: 'StartPage'}) {
if (space.value) { if (space.value) {
space.value.spaceSetupCompleted = true space.value.spaceSetupCompleted = true
loading.value = true
updateSpace().then(() => { updateSpace().then(() => {
router.push(target) router.push(target)
loading.value = false
}) })
} else { } else {
useMessageStore().addMessage(MessageType.ERROR, "Space not loaded yet", 5000) useMessageStore().addMessage(MessageType.ERROR, "Space not loaded yet", 5000)