moved space stuff to database and reworked invite link backend logic

This commit is contained in:
vabene1111
2025-09-11 21:44:40 +02:00
parent ad4b1393dd
commit 723b74509f
46 changed files with 213 additions and 48 deletions

View File

@@ -40,6 +40,9 @@ class ScopeMiddleware:
if request.path.startswith(prefix + '/switch-space/'):
return self.get_response(request)
if request.path.startswith(prefix + '/invite/'):
return self.get_response(request)
# get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point)
user_space = request.user.userspace_set.filter(active=True).first()
@@ -49,6 +52,9 @@ class ScopeMiddleware:
user_space = request.user.userspace_set.filter(active=True).first()
user_space.active = True
user_space.save()
elif 'signup_token' in request.session:
# if user is authenticated, has no space but a signup token (InviteLink) is present, redirect to invite link logic
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
else:
# if user does not yet have a space create one for him
user_space = create_space_for_user(request.user)

View File

@@ -405,6 +405,11 @@ class SpaceSerializer(WritableNestedModelSerializer):
return 0
def create(self, validated_data):
if Space.objects.filter(created_by=self.context['request'].user).count() >= self.context['request'].user.userpreference.max_owned_spaces:
raise serializers.ValidationError(
_('You have the reached the maximum amount of spaces that can be owned by you.') + f' ({self.context['request'].user.userpreference.max_owned_spaces})')
name = None
if 'name' in validated_data:
name = validated_data['name']

View File

@@ -78,10 +78,11 @@ urlpatterns = [
path('setup/', views.setup, name='view_setup'),
path('no-group/', views.no_groups, name='view_no_group'),
path('space-overview/', views.space_overview, name='view_space_overview'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('no-perm/', views.no_perm, name='view_no_perm'),
#path('space-overview/', views.space_overview, name='view_space_overview'),
#path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
#path('no-perm/', views.no_perm, name='view_no_perm'),
path('invite/<slug:token>', views.invite_link, name='view_invite'),
path('invite/<slug:token>/', views.invite_link, name='view_invite'),
path('system/', views.system, name='view_system'),
path('plugin/update/', views.plugin_update, name='view_plugin_update'),

View File

@@ -181,7 +181,10 @@ class StandardFilterModelViewSet(viewsets.ModelViewSet):
queryset = self.queryset
query = self.request.query_params.get('query', None)
if query is not None:
queryset = queryset.filter(name__icontains=query)
try:
queryset = queryset.filter(name__icontains=query)
except FieldError:
pass
updated_at = self.request.query_params.get('updated_at', None)
if updated_at is not None:
@@ -1892,8 +1895,8 @@ class InviteLinkViewSet(LoggingMixin, StandardFilterModelViewSet):
if internal_note is not None:
self.queryset = self.queryset.filter(internal_note=internal_note)
unused = self.request.query_params.get('unused', False)
if unused:
used = self.request.query_params.get('used', False)
if not used:
self.queryset = self.queryset.filter(used_by=None)
if is_space_owner(self.request.user, self.request.space):

View File

@@ -42,6 +42,9 @@ def index(request, path=None, resource=None):
if User.objects.count() < 1 and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS:
return HttpResponseRedirect(reverse_lazy('view_setup'))
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
if request.user.is_authenticated or re.search(r'/recipe/\d+/', request.path[:512]) and request.GET.get('share'):
return render(request, 'frontend/tandoor.html', {})
else:
@@ -98,7 +101,7 @@ def space_overview(request):
max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
ai_enabled=settings.SPACE_AI_ENABLED,
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY,)
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY, )
user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False)
user_space.groups.add(Group.objects.filter(name='admin').get())
@@ -322,7 +325,7 @@ def invite_link(request, token):
try:
token = UUID(token, version=4)
except ValueError:
messages.add_message(request, messages.ERROR, _('Malformed Invite Link supplied!'))
print('Malformed Invite Link supplied!')
return HttpResponseRedirect(reverse('index'))
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
@@ -331,22 +334,17 @@ def invite_link(request, token):
link.used_by = request.user
link.save()
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=False)
if request.user.userspace_set.count() == 1:
user_space.active = True
user_space.save()
UserSpace.objects.filter(user=request.user).update(active=False)
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=True)
user_space.groups.add(link.group)
messages.add_message(request, messages.SUCCESS, _('Successfully joined space.'))
return HttpResponseRedirect(reverse('view_space_overview'))
return HttpResponseRedirect(reverse('index'))
else:
request.session['signup_token'] = str(token)
return HttpResponseRedirect(reverse('account_signup'))
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('view_space_overview'))
return HttpResponseRedirect(reverse('index'))
def report_share_abuse(request, token):

View File

@@ -156,13 +156,16 @@ const router = useRouter()
const isPrintMode = useMediaQuery('print')
onMounted(() => {
useUserPreferenceStore()
useUserPreferenceStore().init()
})
/**
* global title update handler, might be overridden by page specific handlers
*/
router.afterEach((to, from) => {
if(to.name != 'WelcomePage' && !useUserPreferenceStore().activeSpace.spaceSetupCompleted && useUserPreferenceStore().activeSpace.createdBy.id! == useUserPreferenceStore().userSettings.user.id!){
router.push({name: 'WelcomePage'})
}
nextTick(() => {
if (to.meta.title) {
title.value = t(to.meta.title)

View File

@@ -0,0 +1,76 @@
<template>
<model-editor-base
:loading="loading"
:dialog="dialog"
@save="saveObject"
@delete="deleteObject"
@close="emit('close'); editingObjChanged = false"
:is-update="isUpdate()"
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()">
<v-card-text>
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
</v-form>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, watch} from "vue";
import {ConnectorConfig, Space} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
const props = defineProps({
item: {type: {} as PropType<Space>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<Space>, required: false, default: {} as Space},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {
setupState,
deleteObject,
saveObject,
isUpdate,
editingObjName,
loading,
editingObj,
editingObjChanged,
modelClass
} = useModelEditorFunctions<Space>('Space', emit)
/**
* watch prop changes and re-initialize editor
* required to embed editor directly into pages and be able to change item from the outside
*/
watch([() => props.item, () => props.itemId], () => {
initializeEditor()
})
// object specific data (for selects/display)
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor(){
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}
</script>
<style scoped>
</style>

View File

@@ -120,6 +120,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",

View File

@@ -117,6 +117,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Групирай по",
"Hide_Food": "Скриване на храна",
"Hide_Keyword": "Скриване на ключови думи",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupat per",
"Hide_Food": "Amagar Aliment",
"Hide_Keyword": "Amaga les paraules clau",

View File

@@ -160,6 +160,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Seskupit podle",
"Hide_Food": "Skrýt potravinu",
"Hide_Keyword": "Skrýt štítky",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupper efter",
"Hide_Food": "Skjul mad",
"Hide_Keyword": "Skjul nøgleord",

View File

@@ -224,6 +224,7 @@
"GettingStarted": "Erste Schritte",
"Global": "Global",
"GlobalHelp": "Globale AI Anbieter können von Nutzern aller Spaces verwendet werden. Sie können nur dich Instanz Admins (Superusers) erstellt und bearbeitet werden.",
"Group": "Gruppe",
"GroupBy": "Gruppieren nach",
"HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht",
"Headline": "Überschrift",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Ομαδοποίηση κατά",
"Hide_Food": "Απόκρυψη φαγητού",
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Getting Started",
"Global": "Global",
"GlobalHelp": "Global AI Providers can be used by users of all spaces. They can only be created and edited by superusers. ",
"Group": "Group",
"GroupBy": "Group By",
"HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food",
"Headline": "Headline",

View File

@@ -215,6 +215,7 @@
"GettingStarted": "Primeros pasos",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar por",
"HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento",
"Headline": "Encabezado",

View File

@@ -158,6 +158,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Ryhmittely peruste",
"Hide_Food": "Piilota Ruoka",
"Hide_Keyword": "Piilota avainsana",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Commencer",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grouper par",
"HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment",
"Headline": "En-tête",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "אסוף לפי",
"Hide_Food": "הסתר אוכל",
"Hide_Keyword": "הסתר מילות מפתח",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupiraj po",
"Hide_Food": "Sakrij namirnicu",
"Hide_Keyword": "Sakrij ključne riječi",

View File

@@ -144,6 +144,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Csoportosítva",
"Hide_Food": "Alapanyag elrejtése",
"Hide_Keyword": "Kulcsszavak elrejtése",

View File

@@ -69,6 +69,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"Hide_Food": "Թաքցնել սննդամթերքը",
"Hide_Keywords": "Թաքցնել բանալի բառը",
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը",

View File

@@ -132,6 +132,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",

View File

@@ -160,6 +160,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Iniziamo",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Raggruppa per",
"HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento",
"Headline": "Intestazione",

View File

@@ -146,6 +146,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",

View File

@@ -152,6 +152,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupér",
"Hide_Food": "Skjul Matrett",
"Hide_Keyword": "Skjul nøkkelord",

View File

@@ -223,6 +223,7 @@
"GettingStarted": "Aan de slag",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Groepeer per",
"HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel",
"Headline": "Koptekst",

View File

@@ -187,6 +187,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupuj według",
"Hide_Food": "Ukryj żywność",
"Hide_Keyword": "Ukryj słowa kluczowe",

View File

@@ -132,6 +132,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar por",
"Hide_Food": "Esconder comida",
"Hide_Keyword": "",

View File

@@ -221,6 +221,7 @@
"GettingStarted": "Começando",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar Por",
"HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento",
"Headline": "Título",

View File

@@ -139,6 +139,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupat de",
"Hide_Food": "Ascunde mâncare",
"Hide_Keyword": "Ascunde cuvintele cheie",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Начало работы",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Сгруппировать по",
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
"Headline": "Заголовок",

View File

@@ -222,6 +222,7 @@
"GettingStarted": "Začetek",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Združi po",
"HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano",
"Headline": "Glavni naslov",

View File

@@ -198,6 +198,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Gruppera enligt",
"Hide_Food": "Dölj livsmedel",
"Hide_Keyword": "Dölj nyckelord",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Gruplandırma Ölçütü",
"Hide_Food": "Yiyeceği Gizle",
"Hide_Keyword": "Anahtar kelimeleri gizle",

View File

@@ -142,6 +142,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "По Групі",
"Hide_Food": "Сховати Їжу",
"Hide_Keyword": "",

View File

@@ -161,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "分组",
"Hide_Food": "隐藏食物",
"Hide_Keyword": "隐藏关键词",

View File

@@ -221,6 +221,7 @@
"GettingStarted": "開始使用",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "分組依據",
"HeaderWarning": "警告:變更為標題會刪除數量/單位/食物",
"Headline": "標題",

View File

@@ -35,6 +35,17 @@
<database-model-col model="MealType"></database-model-col>
</v-row>
<v-row>
<v-col>
<h2>{{ $t('Space') }}</h2>
</v-col>
</v-row>
<v-row dense>
<database-model-col model="Space"></database-model-col>
<database-model-col model="UserSpace"></database-model-col>
<database-model-col model="InviteLink"></database-model-col>
</v-row>
<template v-if="useUserPreferenceStore().activeSpace.aiEnabled">
<v-row>
<v-col>

View File

@@ -43,7 +43,7 @@
</v-row>
<v-row>
<v-col>
<v-text-field prepend-inner-icon="$search" :label="$t('Search')" v-model="query" clearable></v-text-field>
<v-text-field prepend-inner-icon="$search" :label="$t('Search')" v-model="query" v-if="!genericModel.model.disableSearch" clearable></v-text-field>
<v-data-table-server
v-model="selectedItems"
@@ -82,13 +82,16 @@
<v-chip label v-if="item.space == null" color="success">{{ $t('Global') }}</v-chip>
<v-chip label v-else color="info">{{ $t('Space') }}</v-chip>
</template>
<template v-slot:item.groups="{ item }" v-if="genericModel.model.name == 'UserSpace'">
{{item.groups.flatMap((x: Group) => x.name).join(', ')}}
</template>
<template v-slot:item.action="{ item }">
<v-btn class="float-right" icon="$menu" variant="plain">
<v-icon icon="$menu"></v-icon>
<v-menu activator="parent" close-on-content-click>
<v-list density="compact">
<v-list-item prepend-icon="$edit" :to="{name: 'ModelEditPage', params: {model: model, id: item.id}}"
v-if="!genericModel.model.disableCreate && !genericModel.model.disableUpdate && !genericModel.model.disableDelete">
v-if="!(genericModel.model.disableCreate && genericModel.model.disableUpdate && genericModel.model.disableDelete)">
{{ $t('Edit') }}
</v-list-item>
<v-list-item prepend-icon="fa-solid fa-arrows-to-dot" v-if="genericModel.model.isMerge" link>
@@ -144,7 +147,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelMergeDialog from "@/components/dialogs/ModelMergeDialog.vue";
import {VDataTableUpdateOptions} from "@/vuetify";
import SyncDialog from "@/components/dialogs/SyncDialog.vue";
import {ApiApi, ApiRecipeListRequest, RecipeImport} from "@/openapi";
import {ApiApi, ApiRecipeListRequest, Group, RecipeImport} from "@/openapi";
import {useTitle} from "@vueuse/core";
import RecipeShareDialog from "@/components/dialogs/RecipeShareDialog.vue";
import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";

View File

@@ -64,8 +64,6 @@ import SearchPage from "@/pages/SearchPage.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useRouter} from "vue-router";
const router = useRouter()
const totalRecipes = ref(-1)
onMounted(() => {
@@ -74,10 +72,6 @@ onMounted(() => {
api.apiRecipeList({pageSize: 1}).then((r) => {
totalRecipes.value = r.count
})
if (!useUserPreferenceStore().activeSpace.spaceSetupCompleted) {
router.push({name: 'WelcomePage'})
}
})
</script>

View File

@@ -17,7 +17,7 @@
<v-stepper-window>
<v-stepper-window-item value="1">
<v-card flat>
<v-card-title class="text-h4">{{ $t('WelcometoTandoor') }}</v-card-title>
<v-card-title class="text-h4">{{ $t('WelcometoTandoor') }} <span class="text-tandoor">{{useUserPreferenceStore().userSettings.user.displayName}}</span></v-card-title>
<v-card-text v-if="space">
<p class="text-subtitle-1 mb-4">{{ $t('WelcomeSettingsHelp') }}</p>

View File

@@ -67,7 +67,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
function loadUserSettings() {
console.log('loading user settings from DB')
let api = new ApiApi()
api.apiUserPreferenceList().then(r => {
return api.apiUserPreferenceList().then(r => {
if (r.length == 1) {
userSettings.value = r[0]
isAuthenticated.value = true
@@ -104,7 +104,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/
function loadServerSettings() {
let api = new ApiApi()
api.apiServerSettingsCurrentRetrieve().then(r => {
return api.apiServerSettingsCurrentRetrieve().then(r => {
serverSettings.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -116,7 +116,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/
function loadActiveSpace() {
let api = new ApiApi()
api.apiSpaceCurrentRetrieve().then(r => {
return api.apiSpaceCurrentRetrieve().then(r => {
activeSpace.value = r
}).catch(err => {
if (err.response.status != 403) {
@@ -130,7 +130,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/
function loadUserSpaces() {
let api = new ApiApi()
api.apiUserSpaceList().then(r => {
return api.apiUserSpaceList().then(r => {
userSpaces.value = r.results
}).catch(err => {
if (err.response.status != 403) {
@@ -146,7 +146,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
function loadSpaces() {
let api = new ApiApi()
api.apiSpaceList().then(r => {
return api.apiSpaceList().then(r => {
spaces.value = r.results
}).catch(err => {
if (err.response.status != 403) {
@@ -162,9 +162,10 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
let api = new ApiApi()
api.apiSwitchActiveSpaceRetrieve({spaceId: space.id!}).then(r => {
loadActiveSpace()
router.push({name: 'StartPage'}).then(() => {
location.reload()
loadActiveSpace().then(() => {
router.push({name: 'StartPage'}).then(() => {
location.reload()
})
})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -223,15 +224,20 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
}
}
// always load settings on first initialization of store
loadUserSettings()
loadServerSettings()
loadActiveSpace()
loadUserSpaces()
loadSpaces()
updateTheme()
function init() {
const promises = [] as Promise<any>[]
promises.push(loadUserSettings())
promises.push(loadServerSettings())
promises.push(loadActiveSpace())
promises.push(loadUserSpaces())
promises.push(loadSpaces())
updateTheme()
return Promise.allSettled(promises)
}
return {
init,
deviceSettings,
userSettings,
serverSettings,

View File

@@ -1,13 +1,13 @@
import {
AccessToken, AiLog, AiProvider,
ApiApi, ApiKeywordMoveUpdateRequest, Automation, type AutomationTypeEnum, ConnectorConfig, CookLog, CustomFilter,
Food,
Food, FoodInheritField,
Ingredient,
InviteLink, Keyword,
MealPlan,
MealType,
Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingListEntry,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingListEntry, Space,
Step,
Supermarket,
SupermarketCategory, Sync, SyncLog,
@@ -101,6 +101,7 @@ export type Model = {
disableCreate?: boolean | undefined,
disableUpdate?: boolean | undefined,
disableDelete?: boolean | undefined,
disableSearch?: boolean | undefined,
// disable showing this model as an option in the ModelListPage
disableListView?: boolean | undefined,
@@ -148,6 +149,8 @@ export type EditorSupportedModels =
| 'SearchFields'
| 'AiProvider'
| 'AiLog'
| 'Space'
| 'FoodInheritField'
// used to type methods/parameters in conjunction with configuration type
export type EditorSupportedTypes =
@@ -184,6 +187,8 @@ export type EditorSupportedTypes =
| SearchFields
| AiProvider
| AiLog
| Space
| FoodInheritField
export const TFood = {
name: 'Food',
@@ -655,7 +660,8 @@ export const TUserSpace = {
disableCreate: true,
tableHeaders: [
{title: 'User', key: 'user'},
{title: 'User', key: 'user.displayName'},
{title: 'Group', key: 'groups'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
@@ -669,19 +675,39 @@ export const TInviteLink = {
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/InviteLinkEditor.vue`)),
disableListView: true,
disableSearch: true,
isPaginated: true,
toStringKeys: ['email', 'role'],
tableHeaders: [
{title: 'Email', key: 'email'},
{title: 'Role', key: 'group'},
{title: 'Role', key: 'group.name'},
{title: 'Valid Until', key: 'validUntil'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TInviteLink)
export const TSpace = {
name: 'Space',
localizationKey: 'Space',
localizationKeyDescription: 'SpaceHelp',
icon: 'fa-solid fa-hard-drive',
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/SpaceEditor.vue`)),
disableDelete: true,
isPaginated: true,
toStringKeys: ['name'],
tableHeaders: [
{title: 'Name', key: 'name'},
{title: 'Owner', key: 'createdBy.displayName'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TSpace)
export const TStorage = {
name: 'Storage',
localizationKey: 'Storage',