File "UpdateAppSetting-20250318162429.vue"
Full Path: /home/pulsehostuk9/public_html/invoicer.pulsehost.co.uk/resources/scripts/admin/views/settings/UpdateAppSetting-20250318162429.vue
File size: 12.49 KB
MIME-type: text/html
Charset: utf-8
<template>
<BaseSettingCard
:title="$t('settings.update_app.title')"
:description="$t('settings.update_app.description')"
>
<div class="pb-8 ml-0">
<label class="text-sm not-italic font-medium input-label">
{{ $t('settings.update_app.current_version') }}
</label>
<div class="w-full border-b-2 border-gray-100 border-solid pb-4">
<div
class="
box-border
inline-block
w-auto
p-3
my-2
text-sm text-gray-600
bg-gray-200
border border-gray-200 border-solid
rounded-md
version
"
>
{{ currentVersion }}
</div>
</div>
<div class="w-full pt-4">
<BaseCheckbox v-model="insiderChannel" :label="$t('settings.update_app.insider_consent')"/>
</div>
<BaseButton
:loading="isCheckingforUpdate"
:disabled="isCheckingforUpdate || isUpdating"
variant="primary-outline"
class="mt-6"
@click="checkUpdate"
>
{{ $t('settings.update_app.check_update') }}
</BaseButton>
<BaseDivider v-if="isUpdateAvailable" class="mt-6 mb-4" />
<div v-show="!isUpdating" v-if="isUpdateAvailable" class="mt-4 content">
<BaseHeading type="heading-title" class="mb-2">
{{ $t('settings.update_app.avail_update') }}
</BaseHeading>
<div class="rounded-md bg-primary-50 p-4 mb-3">
<div class="flex">
<div class="shrink-0">
<BaseIcon
name="InformationCircleIcon"
class="h-5 w-5 text-primary-400"
aria-hidden="true"
/>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-primary-800">
{{ $t('general.note') }}
</h3>
<div class="mt-2 text-sm text-primary-700">
<p>
{{ $t('settings.update_app.update_warning') }}
</p>
</div>
</div>
</div>
</div>
<label class="text-sm not-italic font-medium input-label">
{{ $t('settings.update_app.next_version') }}
</label>
<br />
<div
class="
box-border
inline-block
w-auto
p-3
my-2
text-sm text-gray-600
bg-gray-200
border border-gray-200 border-solid
rounded-md
version
"
>
{{ updateData.version }}
</div>
<div
class="
pl-5
mt-4
mb-2
text-sm
leading-snug
text-gray-500
update-description
"
style="white-space: pre-wrap; max-width: 480px"
v-if="description"
v-html="description"
></div>
<div
class="
pl-5
mt-2
mb-8
text-sm
leading-snug
text-gray-500
update-changelog
"
style="white-space: pre-wrap; max-width: 480px"
v-if="changelog"
v-html="changelog"
></div>
<label class="text-sm not-italic font-medium input-label">
{{ $t('settings.update_app.requirements') }}
</label>
<table class="w-1/2 mt-2 border-2 border-gray-200 BaseTable-fixed">
<tr
v-for="(ext, i) in requiredExtentions"
:key="i"
class="p-2 border-2 border-gray-200"
>
<td width="70%" class="p-2 text-sm truncate">
{{ i }}
</td>
<td width="30%" class="p-2 text-sm text-right">
<span
v-if="ext"
class="inline-block w-4 h-4 ml-3 mr-2 bg-green-500 rounded-full"
/>
<span
v-else
class="inline-block w-4 h-4 ml-3 mr-2 bg-red-500 rounded-full"
/>
</td>
</tr>
</table>
<BaseButton class="mt-10" variant="primary" @click="onUpdateApp">
{{ $t('settings.update_app.update') }}
</BaseButton>
</div>
<div v-if="isUpdating" class="relative flex justify-between mt-4 content">
<div>
<h6 class="m-0 mb-3 font-medium sw-section-title">
{{ $t('settings.update_app.update_progress') }}
</h6>
<p
class="mb-8 text-sm leading-snug text-gray-500"
style="max-width: 480px"
>
{{ $t('settings.update_app.progress_text') }}
</p>
</div>
<LoadingIcon
class="absolute right-0 h-6 m-1 animate-spin text-primary-400"
/>
</div>
<ul v-if="isUpdating" class="w-full p-0 list-none">
<li
v-for="step in updateSteps"
:key="step.stepUrl"
class="
flex
justify-between
w-full
py-3
border-b border-gray-200 border-solid
last:border-b-0
"
>
<p class="m-0 text-sm leading-8">{{ $t(step.translationKey) }}</p>
<div class="flex flex-row items-center">
<span v-if="step.time" class="mr-3 text-xs text-gray-500">
{{ step.time }}
</span>
<span
:class="statusClass(step)"
class="block py-1 text-sm text-center uppercase rounded-full"
style="width: 88px"
>
{{ getStatus(step) }}
</span>
</div>
</li>
</ul>
</div>
</BaseSettingCard>
</template>
<script setup>
import { useNotificationStore } from '@/scripts/stores/notification'
import axios from 'axios'
import LoadingIcon from '@/scripts/components/icons/LoadingIcon.vue'
import { reactive, ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { handleError } from '@/scripts/helpers/error-handling'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
import { useDialogStore } from '@/scripts/stores/dialog'
import BaseCheckbox from "@/scripts/components/base/BaseCheckbox.vue";
const notificationStore = useNotificationStore()
const dialogStore = useDialogStore()
const { t, tm } = useI18n()
const comapnyStore = useCompanyStore()
const exchangeRateStore = useExchangeRateStore()
let isUpdateAvailable = ref(false)
let isCheckingforUpdate = ref(false)
let description = ref('')
let changelog = ref('');
let currentVersion = ref('')
let insiderChannel = ref('')
let requiredExtentions = ref(null)
let deletedFiles = ref(null)
let isUpdating = ref(false)
const updateSteps = reactive([
{
translationKey: 'settings.update_app.download_zip_file',
stepUrl: '/api/v1/update/download',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.unzipping_package',
stepUrl: '/api/v1/update/unzip',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.copying_files',
stepUrl: '/api/v1/update/copy',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.deleting_files',
stepUrl: '/api/v1/update/delete',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.running_migrations',
stepUrl: '/api/v1/update/migrate',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.finishing_update',
stepUrl: '/api/v1/update/finish',
time: null,
started: false,
completed: false,
},
])
const updateData = reactive({
isMinor: Boolean,
installed: '',
version: '',
})
let minPhpVesrion = ref(null)
window.addEventListener('beforeunload', (event) => {
if (isUpdating.value) {
event.returnValue = 'Update is in progress!'
}
})
// Created
axios.get('/api/v1/app/version').then((res) => {
currentVersion.value = res.data.version
insiderChannel.value = res.data.channel === 'insider'
})
// comapnyStore
// .fetchCompanySettings(['bulk_exchange_rate_configured'])
// .then((res) => {
// isExchangeRateUpdated.value =
// res.data.bulk_exchange_rate_configured === 'YES'
// })
// Comuted props
const allowToUpdate = computed(() => {
if (requiredExtentions.value !== null) {
return Object.keys(requiredExtentions.value).every((k) => {
return requiredExtentions.value[k]
})
}
return true
})
function statusClass(step) {
const status = getStatus(step)
switch (status) {
case 'pending':
return 'text-primary-800 bg-gray-200'
case 'finished':
return 'text-teal-500 bg-teal-100'
case 'running':
return 'text-blue-400 bg-blue-100'
case 'error':
return 'text-danger bg-red-200'
default:
return ''
}
}
async function checkUpdate() {
try {
isCheckingforUpdate.value = true
let response = await axios.get('/api/v1/check/update', {
params: {
channel: insiderChannel ? 'insider' : ''
}
});
isCheckingforUpdate.value = false
if (!response.data.release) {
notificationStore.showNotification({
title: 'Info!',
type: 'info',
message: t('settings.update_app.latest_message'),
})
return;
}
if (response.data) {
updateData.isMinor = response.data.is_minor
updateData.version = response.data.release.version
description.value = response.data.release.description
changelog.value = response.data.release.changelog
requiredExtentions.value = response.data.release.extensions
isUpdateAvailable.value = true
minPhpVesrion.value = response.data.release.min_php_version
deletedFiles.value = response.data.release.deleted_files
}
} catch (e) {
isUpdateAvailable.value = false
isCheckingforUpdate.value = false
handleError(e)
}
}
function onUpdateApp() {
dialogStore
.openDialog({
title: t('general.are_you_sure'),
message: t('settings.update_app.update_warning'),
yesLabel: t('general.ok'),
noLabel: t('general.cancel'),
variant: 'danger',
hideNoButton: false,
size: 'lg',
})
.then(async (res) => {
if (res) {
let path = null
if (!allowToUpdate.value) {
notificationStore.showNotification({
type: 'error',
message:
'Your current configuration does not match the update requirements. Please try again after all the requirements are fulfilled.',
})
return true
}
for (let index = 0; index < updateSteps.length; index++) {
let currentStep = updateSteps[index]
try {
isUpdating.value = true
currentStep.started = true
let updateParams = {
version: updateData.version,
installed: currentVersion.value,
path: path || null,
}
let requestResponse = await axios.post(
currentStep.stepUrl,
updateParams
)
currentStep.completed = true
if (requestResponse.data && requestResponse.data.path) {
path = requestResponse.data.path
}
// on finish
if (currentStep.translationKey == 'settings.update_app.finishing_update') {
isUpdating.value = false
notificationStore.showNotification({
type: 'success',
message: t('settings.update_app.update_success'),
})
setTimeout(() => {
location.reload()
}, 3000)
}
} catch (error) {
currentStep.started = false
currentStep.completed = true
handleError(error)
onUpdateFailed(currentStep.translationKey)
return false
}
}
}
})
}
function onUpdateFailed(translationKey) {
let stepName = t(translationKey)
if (stepName.value) {
onUpdateApp()
return
}
isUpdating.value = false
}
function getStatus(step) {
if (step.started && step.completed) {
return 'finished'
} else if (step.started && !step.completed) {
return 'running'
} else if (!step.started && !step.completed) {
return 'pending'
} else {
return 'error'
}
}
</script>
<style>
.update-changelog ul {
list-style: disc!important;
margin-left: 30px;
}
.update-changelog li {
margin-bottom: 4px;
}
</style>