Restructure main Vue app invocation to support using BootstrapVue components in Composition API.

This commit is contained in:
Buster Neece 2022-12-21 04:47:12 -06:00
parent 250dafcbb1
commit f20a78ccad
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
5 changed files with 235 additions and 305 deletions

View File

@ -1,39 +1,91 @@
import installPinia from './vendor/pinia';
import gettext from './vendor/gettext';
import {createApp} from "vue";
import {createApp, h} from "vue";
import installBootstrapVue from "./vendor/bootstrapVue";
import installSweetAlert from "./vendor/sweetalert";
import installAxios from "~/vendor/axios";
import useAzuraCast from "~/vendor/azuracast";
import axios from "axios";
import {useEventBus} from "@vueuse/core";
export default function (component, options) {
return function (el, props) {
const vueApp = createApp(
component,
{
...options,
...props
}
);
export default function (component) {
const vueApp = createApp({
render() {
return h(component, this.$appProps)
},
mounted() {
// Workaround to use BootstrapVue toast notifications in Vue 3 composition API.
const notifyBus = useEventBus('notify');
/* Gettext */
if (typeof App.locale !== 'undefined') {
vueApp.config.language = App.locale;
notifyBus.on((event, payload) => {
if (event === 'show') {
this.$bvToast.toast(payload.message, payload.options);
} else if (event === 'hide') {
this.$bvToast.hide(payload.id);
}
});
// Configure some Axios settings that depend on the BootstrapVue $bvToast superglobal.
const handleAxiosError = (error) => {
const {$gettext} = gettext;
let notifyMessage = $gettext('An error occurred and your request could not be completed.');
if (error.response) {
// Request made and server responded
notifyMessage = error.response.data.message;
console.error(notifyMessage);
} else if (error.request) {
// The request was made but no response was received
console.error(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error', error.message);
}
if (typeof this.$notifyError === 'function') {
this.$notifyError(notifyMessage);
}
};
axios.interceptors.request.use((config) => {
return config;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
});
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
});
}
});
vueApp.use(gettext);
/* Gettext */
const {locale} = useAzuraCast();
/* Axios */
installAxios(vueApp);
if (typeof locale !== 'undefined') {
vueApp.config.language = locale;
}
/* Pinia */
installPinia(vueApp);
vueApp.use(gettext);
/* Bootstrap Vue */
installBootstrapVue(vueApp);
/* Axios */
installAxios(vueApp);
/* SweetAlert */
installSweetAlert(vueApp);
/* Pinia */
installPinia(vueApp);
/* Bootstrap Vue */
installBootstrapVue(vueApp);
/* SweetAlert */
installSweetAlert(vueApp);
return function (el, props) {
vueApp.config.globalProperties.$appProps = props;
vueApp.mount(el);
};
}

View File

@ -1,10 +1,10 @@
<template>
<modal-form ref="modal" size="lg" :title="langTitle" :loading="loading" :disable-save-button="v$.form.$invalid"
@submit="submit" @hidden="clearContents">
<modal-form ref="modal" size="lg" :title="$gettext('Configure Backups')" :loading="loading"
:disable-save-button="v$.$invalid" @submit="submit" @hidden="clearContents">
<b-form-fieldset>
<div class="form-row mb-3">
<b-wrapped-form-checkbox class="col-md-12" id="form_edit_backup_enabled"
:field="v$.form.backup_enabled">
:field="v$.backup_enabled">
<template #label>
{{ $gettext('Run Automatic Nightly Backups') }}
</template>
@ -16,8 +16,8 @@
</b-wrapped-form-checkbox>
</div>
<div class="form-row" v-if="v$.form.backup_enabled.$model">
<b-wrapped-form-group class="col-md-6" id="form_backup_time_code" :field="v$.form.backup_time_code">
<div class="form-row" v-if="v$.backup_enabled.$model">
<b-wrapped-form-group class="col-md-6" id="form_backup_time_code" :field="v$.backup_time_code">
<template #label>
{{ $gettext('Scheduled Backup Time') }}
</template>
@ -30,7 +30,7 @@
</b-wrapped-form-group>
<b-wrapped-form-checkbox class="col-md-6" id="form_edit_exclude_media"
:field="v$.form.backup_exclude_media">
:field="v$.backup_exclude_media">
<template #label>
{{ $gettext('Exclude Media from Backup') }}
</template>
@ -41,7 +41,7 @@
</template>
</b-wrapped-form-checkbox>
<b-wrapped-form-group class="col-md-6" id="form_backup_keep_copies" :field="v$.form.backup_keep_copies"
<b-wrapped-form-group class="col-md-6" id="form_backup_keep_copies" :field="v$.backup_keep_copies"
input-type="number" :input-attrs="{min: '0', max: '365'}">
<template #label>
{{ $gettext('Number of Backup Copies to Keep') }}
@ -54,7 +54,7 @@
</b-wrapped-form-group>
<b-wrapped-form-group class="col-md-6" id="edit_form_backup_storage_location"
:field="v$.form.backup_storage_location">
:field="v$.backup_storage_location">
<template #label>
{{ $gettext('Storage Location') }}
</template>
@ -64,7 +64,7 @@
</template>
</b-wrapped-form-group>
<b-wrapped-form-group class="col-md-6" id="edit_form_backup_format" :field="v$.form.backup_format">
<b-wrapped-form-group class="col-md-6" id="edit_form_backup_format" :field="v$.backup_format">
<template #label>
{{ $gettext('Backup Format') }}
</template>
@ -78,9 +78,7 @@
</modal-form>
</template>
<script>
import useVuelidate from "@vuelidate/core";
import CodemirrorTextarea from "~/components/Common/CodemirrorTextarea";
<script setup>
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
import ModalForm from "~/components/Common/ModalForm";
import BFormFieldset from "~/components/Form/BFormFieldset";
@ -88,113 +86,113 @@ import mergeExisting from "~/functions/mergeExisting";
import BWrappedFormCheckbox from "~/components/Form/BWrappedFormCheckbox";
import TimeCode from "~/components/Common/TimeCode";
import objectToFormOptions from "~/functions/objectToFormOptions";
import {computed, ref} from "vue";
import {useAxios} from "~/vendor/axios";
import {useNotify} from "~/vendor/bootstrapVue";
import useVuelidate from "@vuelidate/core";
export default {
name: 'AdminBackupsConfigureModal',
emits: ['relist'],
setup() {
return {v$: useVuelidate()}
},
props: {
settingsUrl: String,
storageLocations: Object
},
components: {
ModalForm,
BFormFieldset,
BWrappedFormGroup,
BWrappedFormCheckbox,
CodemirrorTextarea,
TimeCode
},
data() {
return {
loading: true,
error: null,
form: {},
};
},
validations: {
form: {
'backup_enabled': {},
'backup_time_code': {},
'backup_exclude_media': {},
'backup_keep_copies': {},
'backup_storage_location': {},
'backup_format': {},
const props = defineProps({
settingsUrl: String,
storageLocations: Object
});
const emit = defineEmits(['relist']);
const loading = ref(true);
const error = ref(null);
const form = ref({});
const modal = ref(); // Template Ref
const validations = {
'backup_enabled': {},
'backup_time_code': {},
'backup_exclude_media': {},
'backup_keep_copies': {},
'backup_storage_location': {},
'backup_format': {},
};
const v$ = useVuelidate(validations, form);
const storageLocationOptions = computed(() => {
return objectToFormOptions(props.storageLocations);
});
const formatOptions = computed(() => {
return [
{
value: 'zip',
text: 'Zip',
},
{
value: 'tgz',
text: 'TarGz'
},
{
value: 'tzst',
text: 'ZStd'
}
},
computed: {
langTitle() {
return this.$gettext('Configure Backups');
},
storageLocationOptions() {
return objectToFormOptions(this.storageLocations);
},
formatOptions() {
return [
{
value: 'zip',
text: 'Zip',
},
{
value: 'tgz',
text: 'TarGz'
},
{
value: 'tzst',
text: 'ZStd'
}
];
},
},
methods: {
open() {
this.clearContents();
this.loading = true;
];
});
this.$refs.modal.show();
const clearContents = () => {
v$.value.$reset();
this.axios.get(this.settingsUrl).then((resp) => {
this.form = mergeExisting(this.form, resp.data);
this.loading = false;
}).catch(() => {
this.close();
});
},
clearContents() {
this.v$.$reset();
form.value = {
backup_enabled: false,
backup_time_code: null,
backup_exclude_media: null,
backup_keep_copies: null,
backup_storage_location: null,
backup_format: null,
};
};
this.form = {
backup_enabled: false,
backup_time_code: null,
backup_exclude_media: null,
backup_keep_copies: null,
backup_storage_location: null,
backup_format: null,
};
},
close() {
this.$emit('relist');
this.$refs.modal.hide();
},
submit() {
this.v$.$touch();
if (this.v$.$errors.length > 0) {
return;
}
const {axios} = useAxios();
this.$wrapWithLoading(
this.axios({
method: 'PUT',
url: this.settingsUrl,
data: this.form
})
).then(() => {
this.$notifySuccess();
this.close();
});
}
const close = () => {
emit('relist');
modal.value.hide();
};
const open = () => {
clearContents();
loading.value = true;
modal.value.show();
axios.get(props.settingsUrl).then((resp) => {
form.value = mergeExisting(form.value, resp.data);
loading.value = false;
}).catch(() => {
close();
});
};
const {wrapWithLoading, notifySuccess} = useNotify();
const submit = () => {
v$.value.$touch();
if (v$.value.$errors.length > 0) {
return;
}
wrapWithLoading(
axios({
method: 'PUT',
url: props.settingsUrl,
data: form.value
})
).then(() => {
notifySuccess();
close();
});
}
defineExpose({
open
});
</script>

View File

@ -1,8 +1,8 @@
<template>
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="v$.form.$invalid"
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="v$.$invalid"
@submit="doSubmit" @hidden="clearContents">
<admin-custom-fields-form :form="v$.form" :auto-assign-types="autoAssignTypes">
<admin-custom-fields-form :form="v$" :auto-assign-types="autoAssignTypes">
</admin-custom-fields-form>
</modal-form>
@ -13,14 +13,39 @@ import useVuelidate from "@vuelidate/core";
import {required} from '@vuelidate/validators';
import BaseEditModal from '~/components/Common/BaseEditModal';
import AdminCustomFieldsForm from "~/components/Admin/CustomFields/Form";
import {ref} from "vue";
export default {
name: 'AdminCustomFieldsEditModal',
mixins: [BaseEditModal],
components: {AdminCustomFieldsForm},
setup() {
return {v$: useVuelidate()}
const blankForm = {
'name': '',
'short_name': '',
'auto_assign': ''
};
const form = ref(blankForm);
const resetForm = () => {
form.value = blankForm;
}
const validations = {
'name': {required},
'short_name': {},
'auto_assign': {}
};
const v$ = useVuelidate(validations, form);
return {
form,
resetForm,
v$
};
},
mixins: [BaseEditModal],
props: {
autoAssignTypes: Object
},
@ -31,23 +56,5 @@ export default {
: this.$gettext('Add Custom Field');
}
},
validations() {
return {
form: {
'name': {required},
'short_name': {},
'auto_assign': {}
}
};
},
methods: {
resetForm() {
this.form = {
'name': '',
'short_name': '',
'auto_assign': ''
};
}
}
};
</script>

View File

@ -1,6 +1,5 @@
import axios from "axios";
import VueAxios from "vue-axios";
import gettext from "~/vendor/gettext";
import {inject} from "vue";
import useAzuraCast from "~/vendor/azuracast";
@ -20,45 +19,6 @@ export default function installAxios(vueApp) {
}
vueApp.use(VueAxios, axios);
vueApp.provide('axios', axios);
vueApp.mixin({
mounted() {
const handleAxiosError = (error) => {
const {$gettext} = gettext;
let notifyMessage = $gettext('An error occurred and your request could not be completed.');
if (error.response) {
// Request made and server responded
notifyMessage = error.response.data.message;
console.error(notifyMessage);
} else if (error.request) {
// The request was made but no response was received
console.error(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error', error.message);
}
if (typeof this.$notifyError === 'function') {
this.$notifyError(notifyMessage);
}
};
axios.interceptors.request.use((config) => {
return config;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
});
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
});
}
});
}

View File

@ -1,15 +1,15 @@
import {BootstrapVue} from 'bootstrap-vue';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import {inject} from "vue";
import gettext from "~/vendor/gettext";
import {useEventBus} from "@vueuse/core";
/* Composition API BootstrapVue utilities */
export function useNotify() {
const $bvToast = inject('bvToast');
const {$gettext} = gettext;
const notifyBus = useEventBus('notify');
const notify = function (message = null, options = {}) {
const notify = (message = null, options = {}) => {
if (!!document.hidden) {
return;
}
@ -21,7 +21,10 @@ export function useNotify() {
solid: true
};
$bvToast.toast(message, {...defaults, ...options});
notifyBus.emit('show', {
message: message,
options: {...defaults, ...options}
});
};
const notifyError = (message = null, options = {}) => {
@ -74,7 +77,9 @@ export function useNotify() {
};
const hideLoading = () => {
$bvToast.hide(LOADING_TOAST_ID);
notifyBus.emit('hide', {
id: LOADING_TOAST_ID
});
};
let $isAxiosLoading = false;
@ -109,6 +114,15 @@ export function useNotify() {
};
return {
install(app) {
app.config.globalProperties.$notify = notify;
app.config.globalProperties.$notifyError = notifyError;
app.config.globalProperties.$notifySuccess = notifySuccess;
app.config.globalProperties.$showLoading = showLoading;
app.config.globalProperties.$hideLoading = hideLoading;
app.config.globalProperties.$setLoading = setLoading;
app.config.globalProperties.$wrapWithLoading = wrapWithLoading;
},
notify,
notifyError,
notifySuccess,
@ -121,106 +135,5 @@ export function useNotify() {
export default function installBootstrapVue(vueApp) {
vueApp.use(BootstrapVue);
vueApp.provide('bvToast', vueApp.config.globalProperties.$bvToast);
vueApp.provide('bvModal', vueApp.config.globalProperties.$bvModal);
vueApp.config.globalProperties.$notify = function (message = null, options = {}) {
if (!!document.hidden) {
return;
}
const defaults = {
variant: 'default',
toaster: 'b-toaster-top-right',
autoHideDelay: 3000,
solid: true
};
this.$bvToast.toast(message, {...defaults, ...options});
};
vueApp.config.globalProperties.$notifyError = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('An error occurred and your request could not be completed.');
}
const defaults = {
variant: 'danger',
title: this.$gettext('Error')
};
this.$notify(message, {...defaults, ...options});
return message;
};
vueApp.config.globalProperties.$notifySuccess = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('Changes saved.');
}
const defaults = {
variant: 'success',
title: this.$gettext('Success')
};
this.$notify(message, {...defaults, ...options});
return message;
};
const LOADING_TOAST_ID = 'toast-loading';
vueApp.config.globalProperties.$showLoading = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('Applying changes...');
}
const defaults = {
id: LOADING_TOAST_ID,
variant: 'warning',
title: this.$gettext('Please wait...'),
autoHideDelay: 10000,
isStatus: true
};
this.$notify(message, {...defaults, ...options});
return message;
};
vueApp.config.globalProperties.$hideLoading = function () {
this.$bvToast.hide(LOADING_TOAST_ID);
};
let $isAxiosLoading = false;
let $axiosLoadCount = 0;
vueApp.config.globalProperties.$setLoading = function (isLoading) {
let prevIsLoading = $isAxiosLoading;
if (isLoading) {
$axiosLoadCount++;
$isAxiosLoading = true;
} else if ($axiosLoadCount > 0) {
$axiosLoadCount--;
$isAxiosLoading = ($axiosLoadCount > 0);
}
// Handle state changes
if (!prevIsLoading && $isAxiosLoading) {
this.$showLoading();
} else if (prevIsLoading && !$isAxiosLoading) {
this.$hideLoading();
}
};
vueApp.config.globalProperties.$wrapWithLoading = function (promise) {
this.$setLoading(true);
promise.finally(() => {
this.$setLoading(false);
});
return promise;
};
vueApp.use(useNotify());
};