mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-07 21:44:51 +00:00
* refactor: add HTTP client wrapper and upgrade axios to v1 Introduce a thin HTTP wrapper (resources/scripts/http) that centralizes axios configuration, interceptors, and auth header injection. All 43 files now import from the wrapper instead of axios directly, making future library swaps a single-file change. Upgrade axios from 0.30.0 to 1.14.0. * fix: restore window.Ls assignment removed during axios refactor company.js uses window.Ls.set() to persist selected company, which broke after the axios plugin (that set window.Ls) was deleted.
477 lines
13 KiB
Vue
477 lines
13 KiB
Vue
<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>
|
|
|
|
<div
|
|
v-if="!allowToUpdate"
|
|
class="mt-6 rounded-md bg-red-50 p-4 text-sm text-red-700"
|
|
>
|
|
{{ $t('settings.update_app.requirements_not_met') }}
|
|
</div>
|
|
|
|
<BaseButton
|
|
class="mt-10"
|
|
variant="primary"
|
|
:disabled="!allowToUpdate"
|
|
@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 http from '@/scripts/http'
|
|
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
|
|
|
|
http.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 http.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 http.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>
|