Update components that use v-model values to Vue3 "Futureproof" them.

This commit is contained in:
Buster Neece 2022-12-16 21:43:03 -06:00
parent 2798e926e4
commit 6cb6236fa7
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
12 changed files with 505 additions and 330 deletions

View File

@ -1,10 +1,9 @@
import axios from 'axios';
import VueAxios from 'vue-axios';
import usePinia from './vendor/pinia';
import installPinia from './vendor/pinia';
import gettext from './vendor/gettext';
import {createApp} from "vue";
import useBootstrapVue from "./vendor/bootstrapVue";
import useSweetAlert from "./vendor/sweetalert";
import installBootstrapVue from "./vendor/bootstrapVue";
import installSweetAlert from "./vendor/sweetalert";
import installAxios from "~/vendor/axios";
export default function (component, options) {
return function (el, props) {
@ -24,61 +23,16 @@ export default function (component, options) {
vueApp.use(gettext);
/* Axios */
// Configure auto-CSRF on requests
if (typeof App.api_csrf !== 'undefined') {
axios.defaults.headers.common['X-API-CSRF'] = App.api_csrf;
}
vueApp.use(VueAxios, 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);
});
}
});
installAxios(vueApp);
/* Pinia */
usePinia(vueApp);
installPinia(vueApp);
/* Bootstrap Vue */
useBootstrapVue(vueApp);
installBootstrapVue(vueApp);
/* SweetAlert */
useSweetAlert(vueApp);
installSweetAlert(vueApp);
vueApp.mount(el);
};

View File

@ -2,38 +2,50 @@
<b-input v-bind="$attrs" type="time" v-model="timeCode" pattern="[0-9]{2}:[0-9]{2}" placeholder="13:45"></b-input>
</template>
<script>
<script setup>
import {computed} from "vue";
import _ from 'lodash';
export default {
props: ['value'],
computed: {
timeCode: {
get () {
return this.parseTimeCode(this.value);
},
set (newValue) {
this.$emit('input', this.convertToTimeCode(newValue));
}
}
},
methods: {
parseTimeCode (timeCode) {
if (timeCode !== '' && timeCode !== null) {
timeCode = _.padStart(timeCode, 4, '0');
return timeCode.substr(0, 2) + ':' + timeCode.substr(2);
}
const props = defineProps({
modelValue: String
});
return null;
},
convertToTimeCode (time) {
if (_.isEmpty(time)) {
return null;
}
const emit = defineEmits(['update:modelValue']);
let timeParts = time.split(':');
return (100 * parseInt(timeParts[0], 10)) + parseInt(timeParts[1], 10);
}
const parseTimeCode = (timeCode) => {
if (timeCode !== '' && timeCode !== null) {
timeCode = _.padStart(timeCode, 4, '0');
return timeCode.substring(0, 2) + ':' + timeCode.substring(2);
}
return null;
}
const convertToTimeCode = (time) => {
if (_.isEmpty(time)) {
return null;
}
let timeParts = time.split(':');
return (100 * parseInt(timeParts[0], 10)) + parseInt(timeParts[1], 10);
}
const timeCode = computed({
get: () => {
return parseTimeCode(props.modelValue);
},
set: (newValue) => {
emit('update:modelValue', convertToTimeCode(newValue));
}
});
</script>
<script>
export default {
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
};
</script>

View File

@ -13,36 +13,34 @@
</div>
</template>
<script>
<script setup>
import Icon from "~/components/Common/Icon";
import {onMounted, ref} from "vue";
import {get, set, useVModel} from "@vueuse/core";
const props = defineProps({
modelValue: String
});
const emit = defineEmits(['update:modelValue']);
const initial = ref(75);
onMounted(() => {
set(initial, props.modelValue);
});
const volume = useVModel(props, 'modelValue', emit);
const reset = () => {
set(volume, get(initial));
}
</script>
<script>
export default {
components: {Icon},
name: "VolumeSlider",
emits: "input",
props: ['value'],
data() {
return {
initial: 75
}
},
mounted() {
this.initial = this.value;
},
computed: {
volume: {
get() {
return this.value
},
set(newValue) {
this.$emit('input', newValue);
}
}
},
methods: {
reset() {
this.volume = this.initial;
}
model: {
prop: 'modelValue',
event: 'update:modelValue'
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<b-tab :title="langTitle">
<b-tab :title="$gettext('Intro')">
<b-form-group>
<b-form-row>
<b-form-group class="col-md-6" label-for="intro_file">
@ -7,10 +7,12 @@
{{ $gettext('Select Intro File') }}
</template>
<template #description>
{{ $gettext('This introduction file should exactly match the bitrate and format of the mount point itself.') }}
{{
$gettext('This introduction file should exactly match the bitrate and format of the mount point itself.')
}}
</template>
<flow-upload :target-url="targetUrl" :valid-mime-types="acceptMimeTypes"
<flow-upload :target-url="targetUrl" :valid-mime-types="['audio/*']"
@success="onFileSuccess"></flow-upload>
</b-form-group>
@ -38,56 +40,57 @@
</b-tab>
</template>
<script>
<script setup>
import FlowUpload from '~/components/Common/FlowUpload';
export default {
name: 'MountFormIntro',
components: {FlowUpload},
props: {
value: Object,
recordHasIntro: Boolean,
editIntroUrl: String,
newIntroUrl: String
},
data() {
return {
hasIntro: this.recordHasIntro,
acceptMimeTypes: ['audio/*']
};
},
watch: {
recordHasIntro(newValue) {
this.hasIntro = newValue;
}
},
computed: {
langTitle() {
return this.$gettext('Intro');
},
targetUrl() {
return (this.editIntroUrl)
? this.editIntroUrl
: this.newIntroUrl;
}
},
methods: {
onFileSuccess(file, message) {
this.hasIntro = true;
if (!this.editIntroUrl) {
this.$emit('input', message);
}
},
deleteIntro() {
if (this.editIntroUrl) {
this.axios.delete(this.editIntroUrl).then(() => {
this.hasIntro = false;
});
} else {
this.hasIntro = false;
this.$emit('input', null);
}
}
import {computed, toRef} from "vue";
import {useAxios} from "~/vendor/axios";
import {set} from "@vueuse/core";
const props = defineProps({
modelValue: Object,
recordHasIntro: Boolean,
editIntroUrl: String,
newIntroUrl: String
});
const emit = defineEmits(['update:modelValue']);
const hasIntro = toRef(props, 'recordHasIntro');
const targetUrl = computed(() => {
return (props.editIntroUrl)
? props.editIntroUrl
: props.newIntroUrl;
});
const onFileSuccess = (file, message) => {
set(hasIntro, true);
if (!props.editIntroUrl) {
emit('update:modelValue', message);
}
};
const {axios} = useAxios();
const deleteIntro = () => {
if (props.editIntroUrl) {
axios.delete(props.editIntroUrl).then(() => {
set(hasIntro, false);
});
} else {
set(hasIntro, false);
emit('update:modelValue', null);
}
};
</script>
<script>
export default {
model: {
prop: 'modelValue',
event: 'update:modelValue'
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<b-tab :title="langTitle">
<b-tab :title="$gettext('Artwork')">
<b-form-group>
<b-row>
<b-col md="8">
@ -8,7 +8,9 @@
{{ $gettext('Select PNG/JPG artwork file') }}
</template>
<template #description>
{{ $gettext('Artwork must be a minimum size of 1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels for Apple Podcasts.') }}
{{
$gettext('Artwork must be a minimum size of 1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels for Apple Podcasts.')
}}
</template>
<b-form-file id="edit_form_art" accept="image/jpeg, image/png"
@input="uploadNewArt"></b-form-file>
@ -28,58 +30,66 @@
</b-tab>
</template>
<script setup>
import {computed, ref, toRef} from "vue";
import {get, set} from "@vueuse/core";
import {useAxios} from "~/vendor/axios";
const props = defineProps({
modelValue: Object,
artworkSrc: String,
editArtUrl: String,
newArtUrl: String,
});
const emit = defineEmits(['update:modelValue']);
const artworkSrc = toRef(props, 'artworkSrc');
const localSrc = ref(null);
const src = computed(() => {
return get(localSrc) ?? get(artworkSrc);
});
const {axios} = useAxios();
const uploadNewArt = (file) => {
if (!(file instanceof File)) {
return;
}
let fileReader = new FileReader();
fileReader.addEventListener('load', () => {
set(localSrc, fileReader.result);
}, false);
fileReader.readAsDataURL(file);
let url = (props.editArtUrl) ? props.editArtUrl : props.newArtUrl;
let formData = new FormData();
formData.append('art', file);
axios.post(url, formData).then((resp) => {
emit('update:modelValue', resp.data);
});
};
const deleteArt = () => {
if (props.editArtUrl) {
axios.delete(props.editArtUrl).then(() => {
set(localSrc, null);
});
} else {
set(localSrc, null);
}
}
</script>
<script>
export default {
name: 'PodcastCommonArtwork',
props: {
value: Object,
artworkSrc: String,
editArtUrl: String,
newArtUrl: String,
},
data() {
return {
localSrc: null,
};
},
computed: {
langTitle() {
return this.$gettext('Artwork');
},
src() {
return this.localSrc ?? this.artworkSrc;
}
},
methods: {
uploadNewArt(file) {
if (!(file instanceof File)) {
return;
}
let fileReader = new FileReader();
fileReader.addEventListener('load', () => {
this.localSrc = fileReader.result;
}, false);
fileReader.readAsDataURL(file);
let url = (this.editArtUrl) ? this.editArtUrl : this.newArtUrl;
let formData = new FormData();
formData.append('art', file);
this.axios.post(url, formData).then((resp) => {
this.$emit('input', resp.data);
});
},
deleteArt () {
if (this.editArtUrl) {
this.axios.delete(this.editArtUrl).then(() => {
this.localSrc = '';
});
} else {
this.localSrc = '';
}
}
model: {
prop: 'modelValue',
event: 'update:modelValue'
}
};
</script>

View File

@ -1,5 +1,5 @@
<template>
<b-tab :title="langTitle">
<b-tab :title="$gettext('Media')">
<b-form-group>
<b-form-row>
<b-form-group class="col-md-6" label-for="media_file">
@ -12,7 +12,7 @@
}}
</template>
<flow-upload :target-url="targetUrl" :valid-mime-types="acceptMimeTypes"
<flow-upload :target-url="targetUrl" :valid-mime-types="['audio/x-m4a', 'audio/mpeg']"
@success="onFileSuccess"></flow-upload>
</b-form-group>
@ -40,59 +40,56 @@
</b-tab>
</template>
<script>
<script setup>
import FlowUpload from '~/components/Common/FlowUpload';
import {computed, toRef} from "vue";
import {set} from "@vueuse/core";
import {useAxios} from "~/vendor/axios";
const props = defineProps({
modelValue: Object,
recordHasMedia: Boolean,
downloadUrl: String,
editMediaUrl: String,
newMediaUrl: String
});
const emit = defineEmits(['update:modelValue']);
const hasMedia = toRef(props, 'recordHasMedia');
const targetUrl = computed(() => {
return (props.editMediaUrl)
? props.editMediaUrl
: props.newMediaUrl;
});
const onFileSuccess = (file, message) => {
set(hasMedia, true);
if (!props.editMediaUrl) {
emit('update:modelValue', message);
}
};
const {axios} = useAxios();
const deleteMedia = () => {
if (props.editMediaUrl) {
axios.delete(props.editMediaUrl).then(() => {
set(hasMedia, false);
});
} else {
set(hasMedia, false);
emit('update:modelValue', null);
}
}
</script>
<script>
export default {
name: 'EpisodeFormMedia',
components: {FlowUpload},
props: {
value: Object,
recordHasMedia: Boolean,
downloadUrl: String,
editMediaUrl: String,
newMediaUrl: String
},
data() {
return {
hasMedia: this.recordHasMedia,
acceptMimeTypes: ['audio/x-m4a', 'audio/mpeg']
};
},
watch: {
recordHasMedia(newValue) {
this.hasMedia = newValue;
}
},
computed: {
langTitle() {
return this.$gettext('Media');
},
targetUrl() {
return (this.editMediaUrl)
? this.editMediaUrl
: this.newMediaUrl;
}
},
methods: {
onFileSuccess (file, message) {
this.hasMedia = true;
if (!this.editMediaUrl) {
this.$emit('input', message);
}
},
deleteMedia () {
if (this.editMediaUrl) {
this.axios.delete(this.editMediaUrl).then(() => {
this.hasMedia = false;
});
} else {
this.hasMedia = false;
this.$emit('input', null);
}
}
model: {
prop: 'modelValue',
event: 'update:modelValue'
}
};
</script>

View File

@ -1,5 +1,5 @@
<template>
<b-tab :title="langTitle">
<b-tab :title="$gettext('Artwork')">
<b-form-group>
<b-row>
<b-col md="8">
@ -8,14 +8,16 @@
{{ $gettext('Select PNG/JPG artwork file') }}
</template>
<template #description>
{{ $gettext('This image will be used as the default album art when this streamer is live.') }}
{{
$gettext('This image will be used as the default album art when this streamer is live.')
}}
</template>
<b-form-file id="edit_form_art" accept="image/jpeg, image/png"
@input="uploadNewArt"></b-form-file>
</b-form-group>
</b-col>
<b-col md="4" v-if="src && src !== ''">
<b-img :src="src" :alt="langTitle" rounded fluid></b-img>
<b-img :src="src" :alt="$gettext('Artwork')" rounded fluid></b-img>
<div class="buttons pt-3">
<b-button block variant="danger" @click="deleteArt">
@ -28,58 +30,65 @@
</b-tab>
</template>
<script setup>
import {computed, ref, toRef} from "vue";
import {get, set} from "@vueuse/core";
import {useAxios} from "~/vendor/axios";
const props = defineProps({
modelValue: Object,
artworkSrc: String,
editArtUrl: String,
newArtUrl: String,
});
const emit = defineEmits(['update:modelValue']);
const artworkSrc = toRef(props, 'artworkSrc');
const localSrc = ref(null);
const src = computed(() => {
return get(localSrc) ?? get(artworkSrc);
});
const {axios} = useAxios();
const uploadNewArt = (file) => {
if (!(file instanceof File)) {
return;
}
let fileReader = new FileReader();
fileReader.addEventListener('load', () => {
set(localSrc, fileReader.result);
}, false);
fileReader.readAsDataURL(file);
let url = (props.editArtUrl) ? props.editArtUrl : props.newArtUrl;
let formData = new FormData();
formData.append('art', file);
axios.post(url, formData).then((resp) => {
emit('update:modelValue', resp.data);
});
};
const deleteArt = () => {
if (props.editArtUrl) {
axios.delete(props.editArtUrl).then(() => {
set(localSrc, null);
});
} else {
set(localSrc, null);
}
}
</script>
<script>
export default {
name: 'StreamersFormArtwork',
props: {
value: Object,
artworkSrc: String,
editArtUrl: String,
newArtUrl: String,
},
data() {
return {
localSrc: null,
};
},
computed: {
langTitle() {
return this.$gettext('Artwork');
},
src() {
return this.localSrc ?? this.artworkSrc;
}
},
methods: {
uploadNewArt(file) {
if (!(file instanceof File)) {
return;
}
let fileReader = new FileReader();
fileReader.addEventListener('load', () => {
this.localSrc = fileReader.result;
}, false);
fileReader.readAsDataURL(file);
let url = (this.editArtUrl) ? this.editArtUrl : this.newArtUrl;
let formData = new FormData();
formData.append('art', file);
this.axios.post(url, formData).then((resp) => {
this.$emit('input', resp.data);
});
},
deleteArt() {
if (this.editArtUrl) {
this.axios.delete(this.editArtUrl).then(() => {
this.localSrc = '';
});
} else {
this.localSrc = '';
}
}
model: {
prop: 'modelValue',
event: 'update:modelValue'
}
};
</script>

View File

@ -1,6 +1,6 @@
import {createApp} from 'vue';
import InlinePlayer from '~/components/InlinePlayer.vue';
import usePinia from '../vendor/pinia';
import installPinia from '../vendor/pinia';
import gettext from "../vendor/gettext";
document.addEventListener('DOMContentLoaded', function () {
@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', function () {
inlineApp.use(gettext);
/* Pinia */
usePinia(inlineApp);
installPinia(inlineApp);
inlineApp.mount('#radio-player-controls');
});

61
frontend/vue/vendor/axios.js vendored Normal file
View File

@ -0,0 +1,61 @@
import axios from "axios";
import VueAxios from "vue-axios";
import gettext from "~/vendor/gettext";
import {inject} from "vue";
/* Composition API Axios utilities */
export function useAxios() {
return {
axios: inject('axios')
};
}
export default function installAxios(vueApp) {
// Configure auto-CSRF on requests
if (typeof App.api_csrf !== 'undefined') {
axios.defaults.headers.common['X-API-CSRF'] = App.api_csrf;
}
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,10 +1,130 @@
import {BootstrapVue} from 'bootstrap-vue';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import {inject} from "vue";
import gettext from "~/vendor/gettext";
export default function useBootstrapVue(vueApp) {
/* Composition API BootstrapVue utilities */
export function useNotify() {
const $bvToast = inject('bvToast');
const {$gettext} = gettext;
const notify = function (message = null, options = {}) {
if (!!document.hidden) {
return;
}
const defaults = {
variant: 'default',
toaster: 'b-toaster-top-right',
autoHideDelay: 3000,
solid: true
};
$bvToast.toast(message, {...defaults, ...options});
};
const notifyError = (message = null, options = {}) => {
if (message === null) {
message = $gettext('An error occurred and your request could not be completed.');
}
const defaults = {
variant: 'danger',
title: $gettext('Error')
};
notify(message, {...defaults, ...options});
return message;
};
const notifySuccess = (message = null, options = {}) => {
if (message === null) {
message = $gettext('Changes saved.');
}
const defaults = {
variant: 'success',
title: $gettext('Success')
};
notify(message, {...defaults, ...options});
return message;
};
const LOADING_TOAST_ID = 'toast-loading';
const showLoading = (message = null, options = {}) => {
if (message === null) {
message = $gettext('Applying changes...');
}
const defaults = {
id: LOADING_TOAST_ID,
variant: 'warning',
title: $gettext('Please wait...'),
autoHideDelay: 10000,
isStatus: true
};
notify(message, {...defaults, ...options});
return message;
};
const hideLoading = () => {
$bvToast.hide(LOADING_TOAST_ID);
};
let $isAxiosLoading = false;
let $axiosLoadCount = 0;
const setLoading = (isLoading) => {
let prevIsLoading = $isAxiosLoading;
if (isLoading) {
$axiosLoadCount++;
$isAxiosLoading = true;
} else if ($axiosLoadCount > 0) {
$axiosLoadCount--;
$isAxiosLoading = ($axiosLoadCount > 0);
}
// Handle state changes
if (!prevIsLoading && $isAxiosLoading) {
showLoading();
} else if (prevIsLoading && !$isAxiosLoading) {
hideLoading();
}
};
const wrapWithLoading = (promise) => {
setLoading(true);
promise.finally(() => {
setLoading(false);
});
return promise;
};
return {
notify,
notifyError,
notifySuccess,
showLoading,
hideLoading,
setLoading,
wrapWithLoading
};
}
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;

View File

@ -2,6 +2,6 @@ import {createPinia} from 'pinia';
const pinia = createPinia();
export default function usePinia(vueApp) {
export default function installPinia(vueApp) {
vueApp.use(pinia);
}

View File

@ -9,10 +9,6 @@ const swalCustom = Swal.mixin({
showCancelButton: true,
});
export function showAlert(options = {}) {
return swalCustom.fire(options);
}
const swalConfirmDelete = swalCustom.mixin({
title: $gettext('Delete Record?'),
confirmButtonText: $gettext('Delete'),
@ -20,11 +16,26 @@ const swalConfirmDelete = swalCustom.mixin({
focusCancel: true
});
export function confirmDelete(options = {}) {
return swalConfirmDelete.fire(options);
export function useSweetAlert() {
const showAlert = (options = {}) => {
return swalCustom.fire(options);
}
const confirmDelete = (options = {}) => {
return swalConfirmDelete.fire(options);
}
return {
showAlert,
confirmDelete
};
}
export default function useSweetAlert(vueApp) {
vueApp.config.globalProperties.$swal = showAlert;
vueApp.config.globalProperties.$confirmDelete = confirmDelete;
export default function installSweetAlert(vueApp) {
vueApp.config.globalProperties.$swal = (options = {}) => {
return swalCustom.fire(options);
};
vueApp.config.globalProperties.$confirmDelete = (options = {}) => {
return swalConfirmDelete.fire(options);
};
}