mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 04:10:06 -05:00
space settings
This commit is contained in:
@@ -37,7 +37,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
|
|||||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||||
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig)
|
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig)
|
||||||
from cookbook.templatetags.custom_tags import markdown
|
from cookbook.templatetags.custom_tags import markdown
|
||||||
from recipes.settings import AWS_ENABLED, MEDIA_URL
|
from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST
|
||||||
|
|
||||||
|
|
||||||
class WritableNestedModelSerializer(WNMS):
|
class WritableNestedModelSerializer(WNMS):
|
||||||
@@ -1338,7 +1338,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
|
|||||||
validated_data['space'] = self.context['request'].space
|
validated_data['space'] = self.context['request'].space
|
||||||
obj = super().create(validated_data)
|
obj = super().create(validated_data)
|
||||||
|
|
||||||
if obj.email:
|
if obj.email and EMAIL_HOST is not '':
|
||||||
try:
|
try:
|
||||||
if InviteLink.objects.filter(space=self.context['request'].space,
|
if InviteLink.objects.filter(space=self.context['request'].space,
|
||||||
created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
|
created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
|
||||||
|
|||||||
@@ -4,15 +4,13 @@
|
|||||||
<v-divider class="mb-3"></v-divider>
|
<v-divider class="mb-3"></v-divider>
|
||||||
|
|
||||||
<v-data-table :items="spaceUserSpaces" :headers="userTableHeaders" density="compact" :hide-default-footer="spaceUserSpaces.length < 10">
|
<v-data-table :items="spaceUserSpaces" :headers="userTableHeaders" density="compact" :hide-default-footer="spaceUserSpaces.length < 10">
|
||||||
|
|
||||||
<template #item.groups="{item}">
|
<template #item.groups="{item}">
|
||||||
<span v-for="g in item.groups">{{ g.name }}</span>
|
<span v-for="g in item.groups">{{ g.name }} </span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #item.edit="{item}">
|
<template #item.edit="{item}">
|
||||||
<v-btn icon="$edit" color="edit" size="small" variant="tonal" @click="spaceUserEditDialogUserSpace = Object.assign({}, item); spaceUserEditDialogState = true"></v-btn>
|
<v-btn icon="$edit" color="edit" size="small" variant="tonal" @click="spaceUserEditDialogUserSpace = Object.assign({}, item); spaceUserEditDialogState = true"></v-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
|
|
||||||
<v-dialog v-model="spaceUserEditDialogState" max-width="400px">
|
<v-dialog v-model="spaceUserEditDialogState" max-width="400px">
|
||||||
@@ -21,7 +19,7 @@
|
|||||||
<v-card-subtitle>{{ $t('Created') }} {{ DateTime.fromJSDate(spaceUserEditDialogUserSpace.createdAt).toLocaleString(DateTime.DATETIME_MED) }}</v-card-subtitle>
|
<v-card-subtitle>{{ $t('Created') }} {{ DateTime.fromJSDate(spaceUserEditDialogUserSpace.createdAt).toLocaleString(DateTime.DATETIME_MED) }}</v-card-subtitle>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form>
|
<v-form>
|
||||||
<v-select :items="groups" item-value="id" item-title="name" v-model="spaceUserEditDialogUserSpace.groups"></v-select>
|
<v-select :items="groups" item-value="id" item-title="name" v-model="spaceUserEditDialogUserSpace.groups" multiple return-object></v-select>
|
||||||
</v-form>
|
</v-form>
|
||||||
<div v-if="spaceUserEditDialogUserSpace.internalNote">
|
<div v-if="spaceUserEditDialogUserSpace.internalNote">
|
||||||
<p>{{ $t('Note') }}</p>
|
<p>{{ $t('Note') }}</p>
|
||||||
@@ -35,11 +33,14 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<p class="text-h6 mt-3">{{ $t('Invites') }} <v-btn size="small" class="float-right" prepend-icon="$create" color="create" @click="inviteLinkDialogObject = {} as InviteLink;inviteLinkDialogState = true">{{$t('New')}}</v-btn></p>
|
<p class="text-h6 mt-3">{{ $t('Invites') }}
|
||||||
|
<v-btn size="small" class="float-right" prepend-icon="$create" color="create" @click="inviteLinkDialogObject = {} as InviteLink;inviteLinkDialogState = true">{{ $t('New') }}</v-btn>
|
||||||
|
</p>
|
||||||
<v-divider class="mb-3"></v-divider>
|
<v-divider class="mb-3"></v-divider>
|
||||||
|
|
||||||
<v-data-table :items="spaceInviteLinks" :headers="inviteTableHeaders" density="compact" :hide-default-footer="spaceInviteLinks.length < 10">
|
<v-data-table :items="spaceInviteLinks" :headers="inviteTableHeaders" density="compact" :hide-default-footer="spaceInviteLinks.length < 10">
|
||||||
<template #item.edit="{item}">
|
<template #item.edit="{item}">
|
||||||
|
<v-btn icon="$copy" color="success" size="small" variant="tonal" @click="copyInviteLink(item)"></v-btn>
|
||||||
<v-btn icon="$edit" color="edit" size="small" variant="tonal" @click="inviteLinkDialogObject = Object.assign({}, item); inviteLinkDialogState = true"></v-btn>
|
<v-btn icon="$edit" color="edit" size="small" variant="tonal" @click="inviteLinkDialogObject = Object.assign({}, item); inviteLinkDialogState = true"></v-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -60,7 +61,11 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-btn color="cancel" @click="inviteLinkDialogState = false">{{ $t('Cancel') }}</v-btn>
|
<v-btn color="cancel" @click="inviteLinkDialogState = false">{{ $t('Cancel') }}</v-btn>
|
||||||
<v-btn color="save" prepend-icon="$save" @click="createInviteLink(inviteLinkDialogObject)">{{ $t('Create') }}</v-btn>
|
<v-btn color="save" prepend-icon="$save" @click="saveInviteLink(inviteLinkDialogObject)" :loading="inviteLinkDialogLoading">
|
||||||
|
<span v-if="inviteLinkDialogObject.id == undefined">{{ $t('Create') }}</span>
|
||||||
|
<span v-if="inviteLinkDialogObject.id != undefined">{{ $t('Update') }}</span>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="delete" prepend-icon="$delete" @click="deleteInviteLink(inviteLinkDialogObject)" v-if="inviteLinkDialogObject.id != undefined">{{ $t('Delete') }}</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
@@ -77,7 +82,8 @@ import {ApiApi, Group, InviteLink, UserSpace} from "@/openapi";
|
|||||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {DateTime} from "luxon";
|
import {DateTime} from "luxon";
|
||||||
import {VDateInput} from 'vuetify/labs/VDateInput' //TODO remove once component is out of labs
|
import {VDateInput} from 'vuetify/labs/VDateInput'
|
||||||
|
import {useClipboard} from "@vueuse/core"; //TODO remove once component is out of labs
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
|
|
||||||
@@ -90,6 +96,7 @@ const spaceUserEditDialogUserSpace = ref({} as UserSpace)
|
|||||||
|
|
||||||
const inviteLinkDialogState = ref(false)
|
const inviteLinkDialogState = ref(false)
|
||||||
const inviteLinkDialogObject = ref({} as InviteLink)
|
const inviteLinkDialogObject = ref({} as InviteLink)
|
||||||
|
const inviteLinkDialogLoading = ref(false)
|
||||||
|
|
||||||
const userTableHeaders = [
|
const userTableHeaders = [
|
||||||
{title: t('Username'), key: 'user.username'},
|
{title: t('Username'), key: 'user.username'},
|
||||||
@@ -126,26 +133,76 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update user space in DB and list on client
|
||||||
|
* @param userSpace UserSpace object to update
|
||||||
|
*/
|
||||||
function updateUserSpace(userSpace: UserSpace) {
|
function updateUserSpace(userSpace: UserSpace) {
|
||||||
const api = new ApiApi()
|
const api = new ApiApi()
|
||||||
|
|
||||||
api.apiUserSpacePartialUpdate({id: userSpace.id!, patchedUserSpace: userSpace}).then(r => {
|
api.apiUserSpacePartialUpdate({id: userSpace.id!, patchedUserSpace: userSpace}).then(r => {
|
||||||
|
spaceUserSpaces.value.splice(spaceUserSpaces.value.indexOf(userSpace), 1, r)
|
||||||
|
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createInviteLink(inviteLink: InviteLink){
|
/**
|
||||||
|
* create or update the invite link, refresh invite link list on client
|
||||||
|
* @param inviteLink InviteLink object to update
|
||||||
|
*/
|
||||||
|
function saveInviteLink(inviteLink: InviteLink) {
|
||||||
const api = new ApiApi()
|
const api = new ApiApi()
|
||||||
|
|
||||||
|
inviteLinkDialogLoading.value = true
|
||||||
|
|
||||||
|
if (inviteLink.id == undefined) {
|
||||||
api.apiInviteLinkCreate({inviteLink: inviteLink}).then(r => {
|
api.apiInviteLinkCreate({inviteLink: inviteLink}).then(r => {
|
||||||
inviteLinkDialogState.value = false
|
inviteLinkDialogState.value = false
|
||||||
spaceInviteLinks.value.push(r)
|
spaceInviteLinks.value.push(r)
|
||||||
useMessageStore().addPreparedMessage(PreparedMessage.CREATE_SUCCESS)
|
useMessageStore().addPreparedMessage(PreparedMessage.CREATE_SUCCESS)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
|
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
|
||||||
|
}).finally(() => {
|
||||||
|
inviteLinkDialogLoading.value = false
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
api.apiInviteLinkUpdate({inviteLink: inviteLink, id: inviteLink.id}).then(r => {
|
||||||
|
inviteLinkDialogState.value = false
|
||||||
|
spaceInviteLinks.value.splice(spaceInviteLinks.value.indexOf(inviteLink), 1, r)
|
||||||
|
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS)
|
||||||
|
}).catch(err => {
|
||||||
|
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||||
|
}).finally(() => {
|
||||||
|
inviteLinkDialogLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete invite link from database and client
|
||||||
|
* @param inviteLink InviteLink object to delete
|
||||||
|
*/
|
||||||
|
function deleteInviteLink(inviteLink: InviteLink) {
|
||||||
|
const api = new ApiApi()
|
||||||
|
api.apiInviteLinkDestroy({id: inviteLink.id}).then(r => {
|
||||||
|
inviteLinkDialogState.value = false
|
||||||
|
spaceInviteLinks.value.splice(spaceInviteLinks.value.indexOf(inviteLink) - 1, 1)
|
||||||
|
useMessageStore().addPreparedMessage(PreparedMessage.DELETE_SUCCESS)
|
||||||
|
}).catch(err => {
|
||||||
|
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* copy invite link with url to clipboard
|
||||||
|
* @param inviteLink InviteLink object to copy
|
||||||
|
*/
|
||||||
|
function copyInviteLink(inviteLink: InviteLink) {
|
||||||
|
const {copy} = useClipboard()
|
||||||
|
copy(`${location.protocol}//${location.host}/invite/${inviteLink.uuid}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="6" v-for="s in spaces">
|
<v-col cols="6" v-for="s in spaces">
|
||||||
<v-card>
|
<v-card @click="useUserPreferenceStore().switchSpace(s)">
|
||||||
<v-img height="200px" :src="recipeDefaultImage" :alt="$t('Image')"> </v-img>
|
<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-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-subtitle>{{ $t('created_by') }} {{ s.createdBy.displayName }} </v-card-subtitle>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export enum ErrorMessageType {
|
|||||||
export enum PreparedMessage {
|
export enum PreparedMessage {
|
||||||
UPDATE_SUCCESS = 'UPDATE_SUCCESS',
|
UPDATE_SUCCESS = 'UPDATE_SUCCESS',
|
||||||
CREATE_SUCCESS = 'CREATE_SUCCESS',
|
CREATE_SUCCESS = 'CREATE_SUCCESS',
|
||||||
|
DELETE_SUCCESS = 'DELETE_SUCCESS',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,6 +94,7 @@ export const useMessageStore = defineStore('message_store', () => {
|
|||||||
if (preparedMessage == PreparedMessage.UPDATE_SUCCESS) {
|
if (preparedMessage == PreparedMessage.UPDATE_SUCCESS) {
|
||||||
addMessage(MessageType.SUCCESS, 'Updated Successfully', 7000, {}) // TODO localize and make more useful ?
|
addMessage(MessageType.SUCCESS, 'Updated Successfully', 7000, {}) // TODO localize and make more useful ?
|
||||||
addMessage(MessageType.SUCCESS, 'Created Successfully', 7000, {})
|
addMessage(MessageType.SUCCESS, 'Created Successfully', 7000, {})
|
||||||
|
addMessage(MessageType.SUCCESS, 'Deleted Successfully', 7000, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,28 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load data for currently active space
|
||||||
|
*/
|
||||||
function loadActiveSpace(){
|
function loadActiveSpace(){
|
||||||
let api = new ApiApi()
|
let api = new ApiApi()
|
||||||
api.apiSpaceCurrentRetrieve().then(r => {
|
api.apiSpaceCurrentRetrieve().then(r => {
|
||||||
activeSpace.value = r
|
activeSpace.value = r
|
||||||
|
}).catch(err => {
|
||||||
|
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* switch to the given space
|
||||||
|
*/
|
||||||
|
function switchSpace(space: Space){
|
||||||
|
let api = new ApiApi()
|
||||||
|
|
||||||
|
api.apiSwitchActiveSpaceRetrieve({spaceId: space.id}).then(r => {
|
||||||
|
loadActiveSpace()
|
||||||
|
}).catch(err => {
|
||||||
|
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +94,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
|
|||||||
// always load active space on first initialization of store
|
// always load active space on first initialization of store
|
||||||
loadActiveSpace()
|
loadActiveSpace()
|
||||||
|
|
||||||
return {deviceSettings, userSettings, activeSpace, loadUserSettings, updateUserSettings}
|
return {deviceSettings, userSettings, activeSpace, loadUserSettings, updateUserSettings, switchSpace}
|
||||||
})
|
})
|
||||||
|
|
||||||
// enable hot reload for store
|
// enable hot reload for store
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export default createVuetify({
|
|||||||
edit: 'fa-solid fa-pencil',
|
edit: 'fa-solid fa-pencil',
|
||||||
create: 'fa-solid fa-circle-plus',
|
create: 'fa-solid fa-circle-plus',
|
||||||
search: 'fa-solid fa-magnifying-glass',
|
search: 'fa-solid fa-magnifying-glass',
|
||||||
|
copy: 'fa-solid fa-copy',
|
||||||
settings: 'fa-solid fa-sliders',
|
settings: 'fa-solid fa-sliders',
|
||||||
spaces: 'fa-solid fa-database',
|
spaces: 'fa-solid fa-database',
|
||||||
shopping: 'fa-solid fa-cart-shopping',
|
shopping: 'fa-solid fa-cart-shopping',
|
||||||
|
|||||||
Reference in New Issue
Block a user