Merge branch 'feature/multi-tennancy' into develop

This commit is contained in:
vabene1111
2022-06-13 20:20:49 +02:00
56 changed files with 4868 additions and 12920 deletions

View File

@ -0,0 +1,287 @@
<template>
<div id="app">
<div class="row mt-2">
<div class="col col-12">
<div v-if="space !== undefined">
<h6><i class="fas fa-book"></i> {{ $t('Recipes') }}</h6>
<b-progress height="1.5rem" :max="space.max_recipes" variant="success" :striped="true">
<b-progress-bar :value="space.recipe_count" class="text-dark font-weight-bold">
{{ space.recipe_count }} /
<template v-if="space.max_recipes === 0"></template>
<template v-else>{{ space.max_recipes }}</template>
</b-progress-bar>
</b-progress>
<h6 class="mt-2"><i class="fas fa-users"></i> {{ $t('Users') }}</h6>
<b-progress height="1.5rem" :max="space.max_users" variant="success" :striped="true">
<b-progress-bar :value="space.user_count" class="text-dark font-weight-bold">
{{ space.user_count }} /
<template v-if="space.max_users === 0"></template>
<template v-else>{{ space.max_users }}</template>
</b-progress-bar>
</b-progress>
<h6 class="mt-2"><i class="fas fa-file"></i> {{ $t('Files') }}</h6>
<b-progress height="1.5rem" :max="space.max_file_storage_mb" variant="success" :striped="true">
<b-progress-bar :value="space.file_size_mb" class="text-dark font-weight-bold">
{{ space.file_size_mb }} /
<template v-if="space.max_file_storage_mb === 0"></template>
<template v-else>{{ space.max_file_storage_mb }}</template>
</b-progress-bar>
</b-progress>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col col-12">
<div v-if="user_spaces !== undefined">
<h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Users') }}</h4>
<table class="table">
<thead>
<tr>
<th>{{ $t('User') }}</th>
<th>{{ $t('Groups') }}</th>
<th></th>
</tr>
</thead>
<tr v-for="us in user_spaces" :key="us.id">
<td>{{ us.user.username }}</td>
<td>
<generic-multiselect
class="input-group-text m-0 p-0"
@change="us.groups = $event.val; updateUserSpace(us)"
label="name"
:initial_selection="us.groups"
:model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10"
:multiple="true"
/>
</td>
<td>
<button class="btn btn-danger" @click="deleteUserSpace(us)">{{ $t('Delete') }}</button>
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col col-12">
<div v-if="invite_links !== undefined">
<h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Invites') }}</h4>
<table class="table">
<thead>
<tr>
<th>#</th>
<th>{{ $t('Email') }}</th>
<th>{{ $t('Group') }}</th>
<th></th>
<th></th>
</tr>
</thead>
<tr v-for="il in active_invite_links" :key="il.id">
<td>{{ il.id }}</td>
<td>{{ il.email }}</td>
<td>
<generic-multiselect
class="input-group-text m-0 p-0"
@change="il.group = $event.val;"
label="name"
:initial_single_selection="il.group"
:model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10"
:multiple="false"
/>
</td>
<td><input type="date" v-model="il.valid_until" class="form-control"></td>
<td>
<b-dropdown no-caret right>
<template #button-content>
<i class="fas fa-ellipsis-v"></i>
</template>
<!-- <b-dropdown-item>-->
<!-- <i class="fas fa-share-alt"></i>-->
<!-- </b-dropdown-item>-->
<b-dropdown-item @click="copyToClipboard(il, true)">
<i class="fas fa-link"></i> {{ $t('Copy Link') }}
</b-dropdown-item>
<b-dropdown-item @click="copyToClipboard(il, false)">
<i class="far fa-clipboard"></i> {{ $t('Copy Token') }}
</b-dropdown-item>
<b-dropdown-item @click="deleteInviteLink(il)">
<i class="fas fa-trash-alt"></i> {{ $t('Delete') }}
</b-dropdown-item>
</b-dropdown>
</td>
</tr>
</table>
<b-button variant="primary" @click="show_invite_create = true">{{ $t('Create') }}</b-button>
</div>
</div>
</div>
<div class="row mt-4" v-if="space !== undefined">
<div class="col col-12">
<h4 class="mt-2"><i class="fas fa-cogs"></i> {{ $t('Settings') }}</h4>
<label>{{ $t('Message') }}</label>
<b-form-textarea v-model="space.message"></b-form-textarea>
<b-form-checkbox v-model="space.show_facet_count"> Facet Count</b-form-checkbox>
<span class="text-muted small">{{ $t('facet_count_info') }}</span><br/>
<label>{{ $t('FoodInherit') }}</label>
<generic-multiselect :initial_selection="space.food_inherit"
:model="Models.FOOD_INHERIT_FIELDS"
@change="space.food_inherit = $event.val;">
</generic-multiselect>
<span class="text-muted small">{{ $t('food_inherit_info') }}</span><br/>
<a class="btn btn-success" @click="updateSpace()">{{ $t('Update') }}</a><br/>
<a class="btn btn-warning mt-1" @click="resetInheritance()">{{ $t('reset_food_inheritance') }}</a><br/>
<span class="text-muted small">{{ $t('reset_food_inheritance_info') }}</span>
</div>
</div>
<div class="row mt-4">
<div class="col col-12">
<h4 class="mt-2"><i class="fas fa-trash"></i> {{ $t('Delete') }}</h4>
{{ $t('warning_space_delete') }}
<br/>
<a class="btn btn-danger" :href="resolveDjangoUrl('delete_space', ACTIVE_SPACE_ID)">{{
$t('Delete')
}}</a>
</div>
</div>
<br/>
<br/>
<generic-modal-form :model="Models.INVITE_LINK" :action="Actions.CREATE" :show="show_invite_create"
@finish-action="show_invite_create = false; loadInviteLinks()"/>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiMixin, resolveDjangoUrl, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils"
import {ApiApiFactory} from "@/utils/openapi/api.ts"
import GenericMultiselect from "@/components/GenericMultiselect";
import GenericModalForm from "@/components/Modals/GenericModalForm";
import axios from "axios";
Vue.use(BootstrapVue)
export default {
name: "SupermarketView",
mixins: [ResolveUrlMixin, ToastMixin, ApiMixin],
components: {GenericMultiselect, GenericModalForm},
data() {
return {
ACTIVE_SPACE_ID: window.ACTIVE_SPACE_ID,
space: undefined,
user_spaces: [],
invite_links: [],
show_invite_create: false
}
},
computed: {
active_invite_links: function () {
return this.invite_links.filter(il => il.used_by === null)
},
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiFactory = new ApiApiFactory()
apiFactory.retrieveSpace(this.ACTIVE_SPACE_ID).then(r => {
this.space = r.data
})
apiFactory.listUserSpaces().then(r => {
this.user_spaces = r.data
})
this.loadInviteLinks()
},
methods: {
copyToClipboard: function (inviteLink, link) {
let content = inviteLink.uuid
if (link) {
content = localStorage.BASE_PATH + this.resolveDjangoUrl('view_invite', inviteLink.uuid)
}
navigator.clipboard.writeText(content)
},
loadInviteLinks: function () {
let apiFactory = new ApiApiFactory()
apiFactory.listInviteLinks().then(r => {
this.invite_links = r.data
})
},
updateSpace: function () {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateSpace(this.ACTIVE_SPACE_ID, this.space).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
updateUserSpace: function (userSpace) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserSpace(userSpace.id, userSpace).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
deleteUserSpace: function (userSpace) {
if (confirm(this.$t('confirm_delete', {object: this.$t("User")}))) {
let apiFactory = new ApiApiFactory()
apiFactory.destroyUserSpace(userSpace.id).then(r => {
this.user_spaces = this.user_spaces.filter(u => u !== userSpace)
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
})
}
},
deleteInviteLink: function (inviteLink) {
let apiFactory = new ApiApiFactory()
apiFactory.destroyInviteLink(inviteLink.id).then(r => {
this.invite_links = this.invite_links.filter(i => i !== inviteLink)
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
})
},
resetInheritance: function () {
axios.get(resolveDjangoUrl('api_reset_food_inheritance')).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
},
}
</script>
<style></style>

View File

@ -0,0 +1,18 @@
import Vue from 'vue'
import App from './SpaceManageView.vue'
import i18n from '@/i18n'
Vue.config.productionTip = false
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
let publicPath = localStorage.STATIC_URL + 'vue/'
if (process.env.NODE_ENV === 'development') {
publicPath = 'http://localhost:8080/'
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
new Vue({
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -0,0 +1,36 @@
<template>
<div>
<b-form-group v-bind:label="label" class="mb-3">
<b-form-input v-model="new_value" type="date" ></b-form-input>
<em v-if="help" class="small text-muted">{{ help }}</em>
<small v-if="subtitle" class="text-muted">{{ subtitle }}</small>
</b-form-group>
</div>
</template>
<script>
export default {
name: "DateInput",
props: {
field: { type: String, default: "You Forgot To Set Field Name" },
label: { type: String, default: "Text Field" },
value: { type: String, default: "" },
help: { type: String, default: undefined },
subtitle: { type: String, default: undefined },
},
data() {
return {
new_value: undefined,
}
},
mounted() {
this.new_value = this.value
},
watch: {
new_value: function () {
this.$root.$emit("change", this.field, this.new_value)
},
},
methods: {},
}
</script>

View File

@ -14,6 +14,7 @@
<emoji-input v-if="visibleCondition(f, 'emoji')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
<file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
<date-input v-if="visibleCondition(f, 'date')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help" :subtitle="f.subtitle" />
</div>
<template v-slot:modal-footer>
<div class="row w-100">
@ -42,6 +43,7 @@ import { ApiMixin, StandardToasts, ToastMixin, getUserPreference } from "@/utils
import CheckboxInput from "@/components/Modals/CheckboxInput"
import LookupInput from "@/components/Modals/LookupInput"
import TextInput from "@/components/Modals/TextInput"
import DateInput from "@/components/Modals/DateInput"
import EmojiInput from "@/components/Modals/EmojiInput"
import ChoiceInput from "@/components/Modals/ChoiceInput"
import FileInput from "@/components/Modals/FileInput"
@ -50,7 +52,7 @@ import HelpBadge from "@/components/Badges/Help"
export default {
name: "GenericModalForm",
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge },
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge,DateInput },
mixins: [ApiMixin, ToastMixin],
props: {
model: { required: true, type: Object },

View File

@ -14,6 +14,9 @@
"success_moving_resource": "Successfully moved a resource!",
"success_merging_resource": "Successfully merged a resource!",
"file_upload_disabled": "File upload is not enabled for your space.",
"warning_space_delete": "You can delete your space including all recipes, shopping lists, meal plans and whatever else you have created. This cannot be undone! Are you sure you want to do this ?",
"food_inherit_info": "Fields on food that should be inherited by default.",
"facet_count_info": "Show recipe counts on search filters.",
"step_time_minutes": "Step time in minutes",
"confirm_delete": "Are you sure you want to delete this {object}?",
"import_running": "Import running, please wait!",
@ -125,6 +128,8 @@
"Move": "Move",
"Merge": "Merge",
"Parent": "Parent",
"Copy Link": "Copy Link",
"Copy Token": "Copy Token",
"delete_confirmation": "Are you sure that you want to delete {source}?",
"move_confirmation": "Move <i>{child}</i> to parent <i>{parent}</i>",
"merge_confirmation": "Replace <i>{source}</i> with <i>{target}</i>",
@ -262,6 +267,8 @@
"New_Cookbook": "New cookbook",
"Hide_Keyword": "Hide keywords",
"Clear": "Clear",
"Users": "Users",
"Invites": "Invites",
"err_move_self": "Cannot move item to itself",
"nothing": "Nothing to do",
"err_merge_self": "Cannot merge item with itself",
@ -340,6 +347,7 @@
"filter": "Filter",
"Website": "Website",
"App": "App",
"Message": "Message",
"Bookmarklet": "Bookmarklet",
"click_image_import": "Click the image you want to import for this recipe",
"no_more_images_found": "No additional images found on Website.",
@ -351,7 +359,9 @@
"search_create_help_text": "Create a new recipe directly in Tandoor.",
"warning_duplicate_filter": "Warning: Due to technical limitations having multiple filters of the same combination (and/or/not) might yield unexpected results.",
"reset_children": "Reset Child Inheritance",
"reset_children_help": "Overwrite all children with values from inherited fields. Inheritted fields of children will be set to Inherit Fields unless Children Inherit Fields is set.",
"reset_children_help": "Overwrite all children with values from inherited fields. Inherited fields of children will be set to Inherit Fields unless Children Inherit Fields is set.",
"reset_food_inheritance": "Reset Inheritance",
"reset_food_inheritance_info": "Reset all foods to default inherited fields and their parent values.",
"substitute_help": "Substitutes are considered when searching for recipes that can be made with onhand ingredients.",
"substitute_siblings_help": "All food that share a parent of this food are considered substitutes.",
"substitute_children_help": "All food that are children of this food are considered substitutes.",
@ -360,7 +370,7 @@
"SubstituteOnHand": "You have a substitute on hand.",
"ChildInheritFields": "Children Inherit Fields",
"ChildInheritFields_help": "Children will inherit these fields by default.",
"InheritFields_help": "The values of these fields will be inheritted from parent (Exception: blank shopping categories are not inheritted)",
"InheritFields_help": "The values of these fields will be inherited from parent (Exception: blank shopping categories are not inherited)",
"last_viewed": "Last Viewed",
"created_on": "Created On",
"updatedon": "Updated On",
@ -412,5 +422,6 @@
"Warning_Delete_Supermarket_Category": "Deleting a supermarket category will also delete all relations to foods. Are you sure?",
"New_Supermarket": "Create new supermarket",
"New_Supermarket_Category": "Create new supermarket category",
"Are_You_Sure": "Are you sure?"
"Are_You_Sure": "Are you sure?",
"Valid Until": "Valid Until"
}

View File

@ -23,7 +23,7 @@ export class Models {
false: undefined,
},
},
tree: { default: undefined },
tree: {default: undefined},
},
},
delete: {
@ -50,7 +50,7 @@ export class Models {
type: "lookup",
field: "target",
list: "self",
sticky_options: [{ id: 0, name: "tree_root" }],
sticky_options: [{id: 0, name: "tree_root"}],
},
},
},
@ -71,7 +71,7 @@ export class Models {
food_onhand: true,
shopping: true,
},
tags: [{ field: "supermarket_category", label: "name", color: "info" }],
tags: [{field: "supermarket_category", label: "name", color: "info"}],
// REQUIRED: unordered array of fields that can be set during create
create: {
// if not defined partialUpdate will use the same parameters, prepending 'id'
@ -159,7 +159,7 @@ export class Models {
field: "substitute_siblings",
label: "substitute_siblings", // form.label always translated in utils.getForm()
help_text: "substitute_siblings_help", // form.help_text always translated
condition: { field: "parent", value: true, condition: "field_exists" },
condition: {field: "parent", value: true, condition: "field_exists"},
},
substitute_children: {
form_field: true,
@ -168,7 +168,7 @@ export class Models {
field: "substitute_children",
label: "substitute_children",
help_text: "substitute_children_help",
condition: { field: "numchild", value: 0, condition: "gt" },
condition: {field: "numchild", value: 0, condition: "gt"},
},
inherit_fields: {
form_field: true,
@ -178,7 +178,7 @@ export class Models {
field: "inherit_fields",
list: "FOOD_INHERIT_FIELDS",
label: "InheritFields",
condition: { field: "food_children_exist", value: true, condition: "preference_equals" },
condition: {field: "food_children_exist", value: true, condition: "preference_equals"},
help_text: "InheritFields_help",
},
child_inherit_fields: {
@ -189,7 +189,7 @@ export class Models {
field: "child_inherit_fields",
list: "FOOD_INHERIT_FIELDS",
label: "ChildInheritFields", // form.label always translated in utils.getForm()
condition: { field: "numchild", value: 0, condition: "gt" },
condition: {field: "numchild", value: 0, condition: "gt"},
help_text: "ChildInheritFields_help", // form.help_text always translated
},
reset_inherit: {
@ -199,7 +199,7 @@ export class Models {
field: "reset_inherit",
label: "reset_children",
help_text: "reset_children_help",
condition: { field: "numchild", value: 0, condition: "gt" },
condition: {field: "numchild", value: 0, condition: "gt"},
},
form_function: "FoodCreateDefault",
},
@ -399,7 +399,7 @@ export class Models {
static SUPERMARKET = {
name: "Supermarket",
apiName: "Supermarket",
ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }],
ordered_tags: [{field: "category_to_supermarket", label: "category::name", color: "info"}],
create: {
params: [["name", "description", "category_to_supermarket"]],
form: {
@ -469,9 +469,9 @@ export class Models {
form_field: true,
type: "choice",
options: [
{ value: "FOOD_ALIAS", text: "Food_Alias" },
{ value: "UNIT_ALIAS", text: "Unit_Alias" },
{ value: "KEYWORD_ALIAS", text: "Keyword_Alias" },
{value: "FOOD_ALIAS", text: "Food_Alias"},
{value: "UNIT_ALIAS", text: "Unit_Alias"},
{value: "KEYWORD_ALIAS", text: "Keyword_Alias"},
],
field: "type",
label: "Type",
@ -663,12 +663,53 @@ export class Models {
},
},
}
static INVITE_LINK = {
name: "InviteLink",
apiName: "InviteLink",
paginated: false,
create: {
params: [["email", "group", "valid_until"]],
form: {
email: {
form_field: true,
type: "text",
field: "email",
label: "Email",
placeholder: "",
},
group: {
form_field: true,
type: "lookup",
field: "group",
list: "GROUP",
list_label: "name",
label: "Group",
placeholder: "",
},
valid_until: {
form_field: true,
type: "date",
field: "valid_until",
label: "Valid Until",
placeholder: "",
},
},
},
}
static USER = {
name: "User",
apiName: "User",
paginated: false,
}
static GROUP = {
name: "Group",
apiName: "Group",
paginated: false,
}
static STEP = {
name: "Step",
apiName: "Step",
@ -694,7 +735,7 @@ export class Actions {
},
],
},
ok_label: { function: "translate", phrase: "Save" },
ok_label: {function: "translate", phrase: "Save"},
},
}
static UPDATE = {
@ -729,7 +770,7 @@ export class Actions {
},
],
},
ok_label: { function: "translate", phrase: "Delete" },
ok_label: {function: "translate", phrase: "Delete"},
instruction: {
form_field: true,
type: "instruction",
@ -756,17 +797,17 @@ export class Actions {
suffix: "s",
params: ["query", "page", "pageSize", "options"],
config: {
query: { default: undefined },
page: { default: 1 },
pageSize: { default: 25 },
query: {default: undefined},
page: {default: 1},
pageSize: {default: 25},
},
}
static MERGE = {
function: "merge",
params: ["source", "target"],
config: {
source: { type: "string" },
target: { type: "string" },
source: {type: "string"},
target: {type: "string"},
},
form: {
title: {
@ -781,7 +822,7 @@ export class Actions {
},
],
},
ok_label: { function: "translate", phrase: "Merge" },
ok_label: {function: "translate", phrase: "Merge"},
instruction: {
form_field: true,
type: "instruction",
@ -815,8 +856,8 @@ export class Actions {
function: "move",
params: ["source", "target"],
config: {
source: { type: "string" },
target: { type: "string" },
source: {type: "string"},
target: {type: "string"},
},
form: {
title: {
@ -831,7 +872,7 @@ export class Actions {
},
],
},
ok_label: { function: "translate", phrase: "Move" },
ok_label: {function: "translate", phrase: "Move"},
instruction: {
form_field: true,
type: "instruction",

File diff suppressed because it is too large Load Diff