237 lines
5.9 KiB
Vue
237 lines
5.9 KiB
Vue
<script setup>
|
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import LocalDataResetDialog from "@/components/global/LocalDataResetDialog.vue";
|
|
import { releaseUpdateState, shortReleaseCommit } from "@/services/releaseUpdate.js";
|
|
import { forceFrontendUpdateAndClearLocal } from "@/services/frontendMaintenance.js";
|
|
|
|
const SHIFT_PRESS_WINDOW_MS = 1200;
|
|
const CLOSE_EVENT = "frontend-maintenance-menu:close";
|
|
|
|
const { t } = useI18n({ useScope: "global" });
|
|
const isOpen = ref(false);
|
|
const isBusy = ref(false);
|
|
const isClearConfirmationOpen = ref(false);
|
|
const shiftPresses = ref([]);
|
|
|
|
const currentVersion = computed(() => shortReleaseCommit(releaseUpdateState.currentCommit));
|
|
const latestVersion = computed(() => shortReleaseCommit(releaseUpdateState.latestCommit || releaseUpdateState.currentCommit));
|
|
|
|
const closeMenu = () => {
|
|
if (!isBusy.value) {
|
|
isOpen.value = false;
|
|
isClearConfirmationOpen.value = false;
|
|
}
|
|
};
|
|
|
|
const handleKeydown = (event) => {
|
|
if (event.key !== "Shift" || event.repeat) {
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
shiftPresses.value = [...shiftPresses.value.filter((pressedAt) => now - pressedAt <= SHIFT_PRESS_WINDOW_MS), now];
|
|
if (shiftPresses.value.length >= 3) {
|
|
shiftPresses.value = [];
|
|
isOpen.value = true;
|
|
}
|
|
};
|
|
|
|
const openClearConfirmation = () => {
|
|
if (!isBusy.value) {
|
|
isClearConfirmationOpen.value = true;
|
|
}
|
|
};
|
|
|
|
const closeClearConfirmation = () => {
|
|
isClearConfirmationOpen.value = false;
|
|
};
|
|
|
|
const forceUpdateAndClearLocal = async () => {
|
|
if (isBusy.value) {
|
|
return;
|
|
}
|
|
|
|
isBusy.value = true;
|
|
try {
|
|
await forceFrontendUpdateAndClearLocal();
|
|
} catch (error) {
|
|
isBusy.value = false;
|
|
console.error("Failed to force frontend update and clear local data:", error);
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
window.addEventListener("keydown", handleKeydown);
|
|
window.addEventListener(CLOSE_EVENT, closeMenu);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener("keydown", handleKeydown);
|
|
window.removeEventListener(CLOSE_EVENT, closeMenu);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<div v-if="isOpen" class="frontend-maintenance-menu" data-testid="frontend-maintenance-menu">
|
|
<button
|
|
type="button"
|
|
class="frontend-maintenance-menu__backdrop"
|
|
:aria-label="t('common.close')"
|
|
@click="closeMenu"
|
|
></button>
|
|
<section class="frontend-maintenance-menu__panel" role="dialog" aria-modal="true" :aria-label="t('maintenance_menu.title')">
|
|
<header class="frontend-maintenance-menu__header">
|
|
<div>
|
|
<p>{{ t("maintenance_menu.title") }}</p>
|
|
<span>{{ t("maintenance_menu.version", { current: currentVersion, latest: latestVersion }) }}</span>
|
|
</div>
|
|
<button type="button" class="frontend-maintenance-menu__close" :aria-label="t('common.close')" @click="closeMenu">
|
|
<i class="fas fa-times" aria-hidden="true"></i>
|
|
</button>
|
|
</header>
|
|
|
|
<button
|
|
type="button"
|
|
class="frontend-maintenance-menu__danger"
|
|
data-testid="frontend-maintenance-force-clear"
|
|
:disabled="isBusy"
|
|
@click="openClearConfirmation"
|
|
>
|
|
<i class="fas fa-sync-alt" aria-hidden="true"></i>
|
|
<span>
|
|
<strong>{{ t("maintenance_menu.force_update_clear") }}</strong>
|
|
<small>{{ t("maintenance_menu.force_update_clear_hint") }}</small>
|
|
</span>
|
|
</button>
|
|
</section>
|
|
<LocalDataResetDialog
|
|
v-model="isClearConfirmationOpen"
|
|
:busy="isBusy"
|
|
@confirm="forceUpdateAndClearLocal"
|
|
@dismiss="closeClearConfirmation"
|
|
/>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.frontend-maintenance-menu {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 10000;
|
|
display: grid;
|
|
place-items: end center;
|
|
padding: 16px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.frontend-maintenance-menu__backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
border: 0;
|
|
background: rgba(12, 22, 34, 0.32);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.frontend-maintenance-menu__panel {
|
|
position: relative;
|
|
width: min(420px, calc(100vw - 32px));
|
|
border: 1px solid #d6dde6;
|
|
border-radius: 8px;
|
|
background: #ffffff;
|
|
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25);
|
|
color: #111827;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.frontend-maintenance-menu__header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 14px 14px 10px;
|
|
border-bottom: 1px solid #edf1f5;
|
|
}
|
|
|
|
.frontend-maintenance-menu__header p {
|
|
margin: 0;
|
|
color: #172033;
|
|
font-size: 0.96rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.frontend-maintenance-menu__header span {
|
|
display: block;
|
|
margin-top: 3px;
|
|
color: #667085;
|
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.frontend-maintenance-menu__close {
|
|
width: 34px;
|
|
height: 34px;
|
|
flex: 0 0 auto;
|
|
border: 1px solid #d9e0e8;
|
|
border-radius: 6px;
|
|
background: #ffffff;
|
|
color: #334155;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.frontend-maintenance-menu__danger {
|
|
width: calc(100% - 28px);
|
|
min-height: 58px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin: 14px;
|
|
padding: 10px 12px;
|
|
border: 1px solid #f2b8b5;
|
|
border-radius: 6px;
|
|
background: #fff7f6;
|
|
color: #981b1b;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
}
|
|
|
|
.frontend-maintenance-menu__danger:disabled {
|
|
cursor: wait;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.frontend-maintenance-menu__danger i {
|
|
width: 20px;
|
|
flex: 0 0 auto;
|
|
text-align: center;
|
|
}
|
|
|
|
.frontend-maintenance-menu__danger strong,
|
|
.frontend-maintenance-menu__danger small {
|
|
display: block;
|
|
}
|
|
|
|
.frontend-maintenance-menu__danger strong {
|
|
font-size: 0.86rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.frontend-maintenance-menu__danger small {
|
|
margin-top: 2px;
|
|
color: #9f1c1c;
|
|
font-size: 0.72rem;
|
|
font-weight: 650;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
@media (min-width: 720px) {
|
|
.frontend-maintenance-menu {
|
|
place-items: end end;
|
|
padding: 22px;
|
|
}
|
|
}
|
|
</style>
|