mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-03 21:37:49 -05:00
moved space stuff to database and reworked invite link backend logic
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
76
vue3/src/components/model_editors/SpaceEditor.vue
Normal file
76
vue3/src/components/model_editors/SpaceEditor.vue
Normal 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>
|
||||
@@ -120,6 +120,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Групирай по",
|
||||
"Hide_Food": "Скриване на храна",
|
||||
"Hide_Keyword": "Скриване на ключови думи",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Agrupat per",
|
||||
"Hide_Food": "Amagar Aliment",
|
||||
"Hide_Keyword": "Amaga les paraules clau",
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Seskupit podle",
|
||||
"Hide_Food": "Skrýt potravinu",
|
||||
"Hide_Keyword": "Skrýt štítky",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupper efter",
|
||||
"Hide_Food": "Skjul mad",
|
||||
"Hide_Keyword": "Skjul nøgleord",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Ομαδοποίηση κατά",
|
||||
"Hide_Food": "Απόκρυψη φαγητού",
|
||||
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Ryhmittely peruste",
|
||||
"Hide_Food": "Piilota Ruoka",
|
||||
"Hide_Keyword": "Piilota avainsana",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "אסוף לפי",
|
||||
"Hide_Food": "הסתר אוכל",
|
||||
"Hide_Keyword": "הסתר מילות מפתח",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupiraj po",
|
||||
"Hide_Food": "Sakrij namirnicu",
|
||||
"Hide_Keyword": "Sakrij ključne riječi",
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Csoportosítva",
|
||||
"Hide_Food": "Alapanyag elrejtése",
|
||||
"Hide_Keyword": "Kulcsszavak elrejtése",
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"Hide_Food": "Թաքցնել սննդամթերքը",
|
||||
"Hide_Keywords": "Թաքցնել բանալի բառը",
|
||||
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը",
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupér",
|
||||
"Hide_Food": "Skjul Matrett",
|
||||
"Hide_Keyword": "Skjul nøkkelord",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupuj według",
|
||||
"Hide_Food": "Ukryj żywność",
|
||||
"Hide_Keyword": "Ukryj słowa kluczowe",
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Agrupar por",
|
||||
"Hide_Food": "Esconder comida",
|
||||
"Hide_Keyword": "",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupat de",
|
||||
"Hide_Food": "Ascunde mâncare",
|
||||
"Hide_Keyword": "Ascunde cuvintele cheie",
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
"GettingStarted": "Начало работы",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Сгруппировать по",
|
||||
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
|
||||
"Headline": "Заголовок",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Gruppera enligt",
|
||||
"Hide_Food": "Dölj livsmedel",
|
||||
"Hide_Keyword": "Dölj nyckelord",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Gruplandırma Ölçütü",
|
||||
"Hide_Food": "Yiyeceği Gizle",
|
||||
"Hide_Keyword": "Anahtar kelimeleri gizle",
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "По Групі",
|
||||
"Hide_Food": "Сховати Їжу",
|
||||
"Hide_Keyword": "",
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "分组",
|
||||
"Hide_Food": "隐藏食物",
|
||||
"Hide_Keyword": "隐藏关键词",
|
||||
|
||||
@@ -221,6 +221,7 @@
|
||||
"GettingStarted": "開始使用",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "分組依據",
|
||||
"HeaderWarning": "警告:變更為標題會刪除數量/單位/食物",
|
||||
"Headline": "標題",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user