Update Account frontend; auto-reload on edit modal change.

This commit is contained in:
Buster Neece 2022-12-26 05:24:47 -06:00
parent 1023a104f9
commit f32286236c
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
5 changed files with 446 additions and 459 deletions

View File

@ -114,20 +114,20 @@
</b-col>
</b-row>
<account-edit-modal ref="editModal" :user-url="userUrl" :supported-locales="supportedLocales"
@relist="relist"></account-edit-modal>
<account-edit-modal ref="editmodal" :user-url="userUrl" :supported-locales="supportedLocales"
@reload="reload"></account-edit-modal>
<account-change-password-modal ref="changePasswordModal" :change-password-url="changePasswordUrl"
<account-change-password-modal ref="changepasswordmodal" :change-password-url="changePasswordUrl"
@relist="relist"></account-change-password-modal>
<account-two-factor-modal ref="twoFactorModal" :two-factor-url="twoFactorUrl"
<account-two-factor-modal ref="twofactormodal" :two-factor-url="twoFactorUrl"
@relist="relist"></account-two-factor-modal>
<account-api-key-modal ref="apiKeyModal" :create-url="apiKeysApiUrl" @relist="relist"></account-api-key-modal>
<account-api-key-modal ref="apikeymodal" :create-url="apiKeysApiUrl" @relist="relist"></account-api-key-modal>
</div>
</template>
<script>
<script setup>
import Icon from "~/components/Common/Icon";
import DataTable from "~/components/Common/DataTable";
import AccountChangePasswordModal from "./Account/ChangePasswordModal";
@ -137,127 +137,148 @@ import AccountEditModal from "./Account/EditModal";
import Avatar from "~/components/Common/Avatar";
import InfoCard from "~/components/Common/InfoCard";
import EnabledBadge from "~/components/Common/Badges/EnabledBadge.vue";
import {onMounted, ref} from "vue";
import {useTranslate} from "~/vendor/gettext";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
import {useSweetAlert} from "~/vendor/sweetalert";
export default {
name: 'Account',
components: {
EnabledBadge,
AccountEditModal,
AccountTwoFactorModal,
AccountApiKeyModal,
AccountChangePasswordModal,
Icon,
InfoCard,
DataTable,
Avatar
},
props: {
userUrl: String,
changePasswordUrl: String,
twoFactorUrl: String,
apiKeysApiUrl: String,
supportedLocales: Object
},
data() {
return {
userLoading: true,
user: {
name: null,
email: null,
avatar: {
url: null,
service: null,
serviceUrl: null
},
roles: [],
},
securityLoading: true,
security: {
twoFactorEnabled: false,
},
apiKeyFields: [
{
key: 'comment',
isRowHeader: true,
label: this.$gettext('API Key Description/Comments'),
sortable: false
},
{key: 'actions', label: this.$gettext('Actions'), sortable: false, class: 'shrink'}
]
}
},
mounted() {
this.relist();
},
methods: {
relist() {
this.userLoading = true;
this.$wrapWithLoading(
this.axios.get(this.userUrl)
).then((resp) => {
this.user = {
name: resp.data.name,
email: resp.data.email,
roles: resp.data.roles,
avatar: {
url: resp.data.avatar.url_64,
service: resp.data.avatar.service_name,
serviceUrl: resp.data.avatar.service_url
}
};
this.userLoading = false;
});
const props = defineProps({
userUrl: String,
changePasswordUrl: String,
twoFactorUrl: String,
apiKeysApiUrl: String,
supportedLocales: Object
});
this.securityLoading = true;
this.$wrapWithLoading(
this.axios.get(this.twoFactorUrl)
).then((resp) => {
this.security.twoFactorEnabled = resp.data.two_factor_enabled;
this.securityLoading = false;
});
const userLoading = ref(true);
const user = ref({
name: null,
email: null,
avatar: {
url: null,
service: null,
serviceUrl: null
},
roles: [],
});
this.$refs.datatable.relist();
},
doEditProfile() {
this.$refs.editModal.open();
},
doChangePassword() {
this.$refs.changePasswordModal.open();
},
enableTwoFactor() {
this.$refs.twoFactorModal.open();
},
disableTwoFactor() {
this.$confirmDelete({
title: this.$gettext('Disable two-factor authentication?'),
}).then((result) => {
if (result.value) {
this.$wrapWithLoading(
this.axios.delete(this.twoFactorUrl)
).then((resp) => {
this.$notifySuccess(resp.data.message);
this.relist();
});
}
});
},
createApiKey() {
this.$refs.apiKeyModal.create();
},
deleteApiKey(url) {
this.$confirmDelete({
title: this.$gettext('Delete API Key?'),
}).then((result) => {
if (result.value) {
this.$wrapWithLoading(
this.axios.delete(url)
).then((resp) => {
this.$notifySuccess(resp.data.message);
this.relist();
});
}
});
}
const securityLoading = ref(true);
const security = ref({
twoFactorEnabled: false,
});
const {$gettext} = useTranslate();
const apiKeyFields = [
{
key: 'comment',
isRowHeader: true,
label: $gettext('API Key Description/Comments'),
sortable: false
},
{
key: 'actions',
label: $gettext('Actions'),
sortable: false,
class: 'shrink'
}
}
];
const datatable = ref(); // DataTable
const {wrapWithLoading, notifySuccess} = useNotify();
const {axios} = useAxios();
const relist = () => {
userLoading.value = true;
wrapWithLoading(
axios.get(props.userUrl)
).then((resp) => {
user.value = {
name: resp.data.name,
email: resp.data.email,
roles: resp.data.roles,
avatar: {
url: resp.data.avatar.url_64,
service: resp.data.avatar.service_name,
serviceUrl: resp.data.avatar.service_url
}
};
userLoading.value = false;
});
securityLoading.value = true;
wrapWithLoading(
axios.get(props.twoFactorUrl)
).then((resp) => {
security.value.twoFactorEnabled = resp.data.two_factor_enabled;
securityLoading.value = false;
});
datatable.value?.relist();
};
onMounted(relist);
const reload = () => {
location.reload();
};
const editmodal = ref(); // EditModal
const doEditProfile = () => {
editmodal.value?.open();
};
const changepasswordmodal = ref(); // ChangePasswordModal
const doChangePassword = () => {
changepasswordmodal.value?.open();
};
const twofactormodal = ref(); // TwoFactorModal
const enableTwoFactor = () => {
twofactormodal.value?.open();
};
const {confirmDelete} = useSweetAlert();
const disableTwoFactor = () => {
confirmDelete({
title: $gettext('Disable two-factor authentication?'),
}).then((result) => {
if (result.value) {
wrapWithLoading(
axios.delete(props.twoFactorUrl)
).then((resp) => {
notifySuccess(resp.data.message);
relist();
});
}
});
};
const apikeymodal = ref(); // ApiKeyModal
const createApiKey = () => {
apikeymodal.value?.create();
};
const deleteApiKey = (url) => {
confirmDelete({
title: $gettext('Delete API Key?'),
}).then((result) => {
if (result.value) {
wrapWithLoading(
axios.delete(url)
).then((resp) => {
notifySuccess(resp.data.message);
relist();
});
}
});
};
</script>

View File

@ -1,12 +1,12 @@
<template>
<b-modal size="md" centered id="api_keys_modal" ref="modal" :title="langTitle" @hidden="clearContents"
<b-modal size="md" centered id="api_keys_modal" ref="modal" :title="$gettext('Add API Key')" @hidden="clearContents"
no-enforce-focus>
<template #default="slotProps">
<b-alert variant="danger" :show="error != null">{{ error }}</b-alert>
<b-form v-if="newKey === null" class="form vue-form" @submit.prevent="doSubmit">
<b-form-fieldset>
<b-wrapped-form-group id="form_comments" :field="v$.form.comment" autofocus>
<b-wrapped-form-group id="form_comments" :field="v$.comment" autofocus>
<template #label>
{{ $gettext('API Key Description/Comments') }}
</template>
@ -35,89 +35,79 @@
</b-modal>
</template>
<script>
<script setup>
import BFormFieldset from "~/components/Form/BFormFieldset";
import InvisibleSubmitButton from "~/components/Common/InvisibleSubmitButton";
import AccountApiKeyNewKey from "./ApiKeyNewKey";
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
import {required} from '@vuelidate/validators';
import useVuelidate from "@vuelidate/core";
import {ref} from "vue";
import {useVuelidateOnForm} from "~/functions/useVuelidateOnForm";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
export default {
name: 'AccountApiKeyModal',
components: {BFormFieldset, InvisibleSubmitButton, AccountApiKeyNewKey, BWrappedFormGroup},
props: {
createUrl: String
},
setup() {
return {
v$: useVuelidate()
};
},
data() {
return {
error: null,
newKey: null,
form: this.getBlankForm()
}
},
validations: {
form: {
comment: {required}
}
},
computed: {
langTitle() {
return this.$gettext('Add API Key');
}
},
methods: {
create() {
this.resetForm();
this.error = null;
const props = defineProps({
createUrl: String
});
this.$refs.modal.show();
},
getBlankForm() {
return {
comment: ''
};
},
resetForm() {
this.newKey = null;
this.form = this.getBlankForm();
},
doSubmit() {
this.v$.$touch();
if (this.v$.$errors.length > 0) {
return;
}
const emit = defineEmits(['relist']);
this.error = null;
const error = ref(null);
const newKey = ref(null);
this.$wrapWithLoading(
this.axios({
method: 'POST',
url: this.createUrl,
data: this.form
})
).then((resp) => {
this.newKey = resp.data.key;
this.$emit('relist');
}).catch((error) => {
this.error = error.response.data.message;
});
},
close() {
this.$refs.modal.hide();
this.clearContents();
},
clearContents() {
this.v$.$reset();
this.error = null;
this.resetForm();
},
const {form, resetForm, v$} = useVuelidateOnForm(
{
comment: {required}
},
{
comment: ''
}
);
const clearContents = () => {
resetForm();
error.value = null;
newKey.value = null;
};
const modal = ref(); // BModal
const create = () => {
clearContents();
modal.value?.show();
};
const {wrapWithLoading} = useNotify();
const {axios} = useAxios();
const doSubmit = () => {
v$.value.$touch();
if (v$.value.$errors.length > 0) {
return;
}
error.value = null;
wrapWithLoading(
axios({
method: 'POST',
url: props.createUrl,
data: form.value
})
).then((resp) => {
newKey.value = resp.data.key;
emit('relist');
}).catch((error) => {
error.value = error.response.data.message;
});
};
const close = () => {
modal.value?.hide();
};
defineExpose({
create
});
</script>

View File

@ -63,59 +63,60 @@
</b-form-fieldset>
</template>
<script>
<script setup>
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
import BFormFieldset from "~/components/Form/BFormFieldset";
import objectToFormOptions from "~/functions/objectToFormOptions";
import {computed} from "vue";
import {useTranslate} from "~/vendor/gettext";
export default {
name: 'AccountEditForm',
props: {
form: Object,
supportedLocales: Object
},
components: {BFormFieldset, BWrappedFormGroup},
computed: {
localeOptions() {
let localeOptions = objectToFormOptions(this.supportedLocales);
localeOptions.unshift({
text: this.$gettext('Use Browser Default'),
value: 'default'
});
return localeOptions;
const props = defineProps({
form: Object,
supportedLocales: Object
});
const {$gettext} = useTranslate();
const localeOptions = computed(() => {
let localeOptions = objectToFormOptions(props.supportedLocales);
localeOptions.unshift({
text: $gettext('Use Browser Default'),
value: 'default'
});
return localeOptions;
});
const themeOptions = computed(() => {
return [
{
text: $gettext('Prefer System Default'),
value: 'browser'
},
themeOptions() {
return [
{
text: this.$gettext('Prefer System Default'),
value: 'browser'
},
{
text: this.$gettext('Light'),
value: 'light'
},
{
text: this.$gettext('Dark'),
value: 'dark'
}
];
{
text: $gettext('Light'),
value: 'light'
},
show24hourOptions() {
return [
{
text: this.$gettext('Prefer System Default'),
value: null
},
{
text: this.$gettext('12 Hour'),
value: false
},
{
text: this.$gettext('24 Hour'),
value: true
}
];
{
text: $gettext('Dark'),
value: 'dark'
}
}
}
];
});
const show24hourOptions = computed(() => {
return [
{
text: $gettext('Prefer System Default'),
value: null
},
{
text: $gettext('12 Hour'),
value: false
},
{
text: $gettext('24 Hour'),
value: true
}
];
});
</script>

View File

@ -1,121 +1,105 @@
<template>
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="v$.$invalid"
<modal-form ref="modal" :loading="loading" :title="$gettext('Edit Profile')" :error="error"
:disable-save-button="v$.$invalid"
@submit="doSubmit" @hidden="clearContents">
<account-edit-form :form="v$.form" :supported-locales="supportedLocales"></account-edit-form>
<account-edit-form :form="v$" :supported-locales="supportedLocales"></account-edit-form>
</modal-form>
</template>
<script>
<script setup>
import mergeExisting from "~/functions/mergeExisting";
import {email, required} from '@vuelidate/validators';
import useVuelidate from "@vuelidate/core";
import AccountEditForm from "./EditForm.vue";
import ModalForm from "~/components/Common/ModalForm.vue";
import {ref} from "vue";
import {useVuelidateOnForm} from "~/functions/useVuelidateOnForm";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
export default {
name: 'AccountEditModal',
components: {ModalForm, AccountEditForm},
emits: ['relist'],
props: {
userUrl: String,
supportedLocales: Object
const props = defineProps({
userUrl: String,
supportedLocales: Object
});
const emit = defineEmits(['reload']);
const loading = ref(true);
const error = ref(null);
const {form, resetForm, v$} = useVuelidateOnForm(
{
name: {},
email: {required, email},
locale: {required},
theme: {required},
show_24_hour_time: {}
},
setup() {
return {
v$: useVuelidate()
};
},
data() {
return {
loading: true,
error: null,
form: {
...this.getBlankForm(),
}
};
},
validations() {
return {
form: {
name: {},
email: {required, email},
locale: {required},
theme: {required},
show_24_hour_time: {}
}
};
},
computed: {
langTitle() {
return this.$gettext('Edit Profile');
}
},
methods: {
getBlankForm() {
return {
name: '',
email: '',
locale: 'default',
theme: 'browser',
show_24_hour_time: null,
};
},
resetForm() {
this.form = {
...this.getBlankForm()
};
},
open() {
this.resetForm();
this.loading = false;
this.error = null;
this.$refs.modal.show();
this.$wrapWithLoading(
this.axios.get(this.userUrl)
).then((resp) => {
this.form = mergeExisting(this.form, resp.data);
this.loading = false;
}).catch(() => {
this.close();
});
},
doSubmit() {
this.v$.$touch();
if (this.v$.$errors.length > 0) {
return;
}
this.error = null;
this.$wrapWithLoading(
this.axios({
method: 'PUT',
url: this.userUrl,
data: this.form
})
).then(() => {
this.$notifySuccess();
this.$emit('relist');
this.close();
}).catch((error) => {
this.error = error.response.data.message;
});
},
close() {
this.$refs.modal.hide();
},
clearContents() {
this.v$.$reset();
this.loading = false;
this.error = null;
this.resetForm();
},
{
name: '',
email: '',
locale: 'default',
theme: 'browser',
show_24_hour_time: null,
}
}
);
const clearContents = () => {
resetForm();
loading.value = false;
error.value = null;
};
const modal = ref(); // BModal
const close = () => {
modal.value.hide();
};
const {wrapWithLoading, notifySuccess} = useNotify();
const {axios} = useAxios();
const open = () => {
clearContents();
modal.value?.show();
wrapWithLoading(
axios.get(props.userUrl)
).then((resp) => {
form.value = mergeExisting(form.value, resp.data);
loading.value = false;
}).catch(() => {
close();
});
};
const doSubmit = () => {
v$.value.$touch();
if (v$.value.$errors.length > 0) {
return;
}
error.value = null;
wrapWithLoading(
axios({
method: 'PUT',
url: props.userUrl,
data: form.value
})
).then(() => {
notifySuccess();
emit('reload');
close();
}).catch((error) => {
error.value = error.response.data.message;
});
};
defineExpose({
open
});
</script>

View File

@ -1,5 +1,6 @@
<template>
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="v$.$invalid"
<modal-form ref="modal" :loading="loading" :title="$gettext('Enable Two-Factor Authentication')"
:error="error" :disable-save-button="v$.$invalid"
@submit="doSubmit" @hidden="clearContents" no-enforce-focus>
<b-row>
@ -23,7 +24,7 @@
</p>
<b-form-fieldset>
<b-wrapped-form-group id="form_otp" :field="v$.form.otp" autofocus>
<b-wrapped-form-group id="form_otp" :field="v$.otp" autofocus>
<template #label>
{{ $gettext('Code from Authenticator App') }}
</template>
@ -51,116 +52,106 @@
</modal-form>
</template>
<script>
<script setup>
import ModalForm from "~/components/Common/ModalForm";
import CopyToClipboardButton from "~/components/Common/CopyToClipboardButton";
import BFormFieldset from "~/components/Form/BFormFieldset";
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
import {minLength, required} from "@vuelidate/validators";
import useVuelidate from "@vuelidate/core";
import {useVuelidateOnForm} from "~/functions/useVuelidateOnForm";
import {ref} from "vue";
import {useResettableRef} from "~/functions/useResettableRef";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
export default {
name: 'AccountTwoFactorModal',
components: {ModalForm, CopyToClipboardButton, BFormFieldset, BWrappedFormGroup},
emits: ['relist'],
props: {
twoFactorUrl: String
},
setup() {
return {
v$: useVuelidate()
};
},
data() {
return {
loading: true,
error: null,
totp: {
secret: null,
totp_uri: null,
qr_code: null
},
form: {
otp: ''
}
};
},
validations: {
form: {
otp: {
required,
minLength: minLength(6)
}
const props = defineProps({
twoFactorUrl: String
});
const emit = defineEmits(['relist']);
const loading = ref(true);
const error = ref(null);
const {form, resetForm, v$} = useVuelidateOnForm(
{
otp: {
required,
minLength: minLength(6)
}
},
computed: {
langTitle() {
return this.$gettext('Enable Two-Factor Authentication');
}
},
methods: {
resetForm() {
this.totp = {
secret: null,
totp_uri: null,
qr_code: null
};
this.form = {
otp: '',
};
},
open() {
this.resetForm();
this.loading = false;
this.error = null;
this.$refs.modal.show();
this.$wrapWithLoading(
this.axios.put(this.twoFactorUrl)
).then((resp) => {
this.totp = resp.data;
this.loading = false;
}).catch(() => {
this.close();
});
},
doSubmit() {
this.v$.$touch();
if (this.v$.$errors.length > 0) {
return;
}
this.error = null;
this.$wrapWithLoading(
this.axios({
method: 'PUT',
url: this.twoFactorUrl,
data: {
secret: this.totp.secret,
otp: this.form.otp
}
})
).then(() => {
this.$notifySuccess();
this.$emit('relist');
this.close();
}).catch((error) => {
this.error = error.response.data.message;
});
},
close() {
this.$refs.modal.hide();
},
clearContents() {
this.v$.$reset();
this.loading = false;
this.error = null;
this.resetForm();
},
{
otp: ''
}
}
);
const {record: totp, reset: resetTotp} = useResettableRef({
secret: null,
totp_uri: null,
qr_code: null
});
const clearContents = () => {
resetForm();
resetTotp();
loading.value = false;
error.value = null;
};
const modal = ref(); // BModal
const close = () => {
modal.value?.hide();
};
const {wrapWithLoading, notifySuccess} = useNotify();
const {axios} = useAxios();
const open = () => {
clearContents();
loading.value = true;
modal.value?.show();
wrapWithLoading(
axios.put(props.twoFactorUrl)
).then((resp) => {
totp.value = resp.data;
loading.value = false;
}).catch(() => {
close();
});
};
const doSubmit = () => {
v$.value.$touch();
if (v$.value.$errors.length > 0) {
return;
}
error.value = null;
wrapWithLoading(
axios({
method: 'PUT',
url: props.twoFactorUrl,
data: {
secret: totp.value.secret,
otp: form.value.otp
}
})
).then(() => {
notifySuccess();
emit('relist');
close();
}).catch((error) => {
error.value = error.response.data.message;
});
};
defineExpose({
open
});
</script>