11<template >
2- <GlassModal :is-visible =" isVisible" position =" center" >
2+ <GlassModal :is-visible =" isVisible" position =" center" no-close-on-outside-click @outside-click = " requestCloseModal " >
33 <div class =" features-modal p-4 max-w-[95vw]" >
44 <div class =" flex justify-center items-center mb-2" >
55 <h2 class =" text-xl font-semibold" >{{ $t('components.ExternalFeaturesDiscoveryModal.title') }}</h2 >
66 </div >
77 <div class =" fixed top-1 right-1" >
8- <v-btn icon =" mdi-close" size =" small" variant =" text" class =" text-lg" @click =" closeModal " ></v-btn >
8+ <v-btn icon =" mdi-close" size =" small" variant =" text" class =" text-lg" @click =" requestCloseModal " ></v-btn >
99 </div >
1010
1111 <v-tabs v-model =" activeTab" class =" mb-4" >
12- <v-tab value =" actions" >{{ $t('components.ExternalFeaturesDiscoveryModal.actionsTab') }}</v-tab >
13- <v-tab value =" joystick-suggestions" >{{
14- $t('components.ExternalFeaturesDiscoveryModal.joystickMappingsTab')
15- }}</v-tab >
12+ <v-tab value =" actions" :class =" { 'tab-blink': activeTab !== 'actions' && hasPendingActions }" >
13+ {{ $t('components.ExternalFeaturesDiscoveryModal.actionsTab') }}
14+ </v-tab >
15+ <v-tab
16+ value =" joystick-suggestions"
17+ :class =" { 'tab-blink': activeTab !== 'joystick-suggestions' && hasPendingJoystickSuggestions }"
18+ >
19+ {{ $t('components.ExternalFeaturesDiscoveryModal.joystickMappingsTab') }}
20+ </v-tab >
1621 </v-tabs >
1722
1823 <v-tabs-window v-model =" activeTab" >
2934 </p >
3035 </div >
3136
37+ <div v-else-if =" filteredActions.length === 0" class =" text-center py-6" >
38+ <v-icon size =" 40" class =" mb-2 opacity-50" >mdi-check-circle-outline</v-icon >
39+ <p class =" opacity-70 max-w-[70%] mx-auto" >No new actions — all extension actions have been reviewed.</p >
40+ </div >
41+
3242 <!-- New Actions -->
3343 <div v-if =" filteredActions.length > 0" class =" mb-4" >
3444 <div class =" flex items-center gap-2 mb-2" >
232242 </div >
233243
234244 <div v-else class =" actions-container" >
245+ <div v-if =" filteredJoystickSuggestionsByExtension.length === 0" class =" text-center py-6" >
246+ <v-icon size =" 40" class =" mb-2 opacity-50" >mdi-check-circle-outline</v-icon >
247+ <p class =" opacity-70 max-w-[70%] mx-auto" >
248+ No new suggestions — all joystick mapping suggestions have been reviewed.
249+ </p >
250+ </div >
251+
235252 <!-- New Suggestions Section -->
236253 <div v-if =" filteredJoystickSuggestionsByExtension.length > 0" class =" mb-8" >
237254 <div class =" flex items-center gap-2 mb-4" >
763780 </v-card-actions >
764781 </v-card >
765782 </v-dialog >
783+
784+ <!-- Close Confirmation Dialog -->
785+ <v-dialog v-model =" closeConfirmationDialog" max-width =" 500px" >
786+ <v-card class =" rounded-lg" :style =" interfaceStore.globalGlassMenuStyles" >
787+ <v-card-title class =" text-center pt-4 pb-0" >
788+ <div class =" flex items-center justify-center gap-2" >
789+ <v-icon color =" warning" size =" 24" >mdi-alert</v-icon >
790+ <h2 class =" text-xl font-semibold" >Pending Extension Features</h2 >
791+ </div >
792+ </v-card-title >
793+ <v-btn
794+ icon =" mdi-close"
795+ size =" small"
796+ variant =" text"
797+ class =" absolute top-2 right-2 text-lg"
798+ @click =" closeConfirmationDialog = false"
799+ ></v-btn >
800+
801+ <v-card-text class =" px-6 pb-4" >
802+ <p class =" text-center text-sm text-gray-300 mb-4" >
803+ There are still extension features pending your decision. Please accept or ignore each suggestion from
804+ your BlueOS extensions. Otherwise, this dialog will open automatically again the next time you start
805+ Cockpit.
806+ </p >
807+
808+ <div class =" max-h-[260px] overflow-y-auto pr-1 space-y-3" >
809+ <div v-if =" filteredActions.length > 0" >
810+ <div class =" flex items-center gap-2 mb-1" >
811+ <v-icon size =" 16" >mdi-lightning-bolt-outline</v-icon >
812+ <h3 class =" text-sm font-semibold" >Pending actions ({{ filteredActions.length }})</h3 >
813+ </div >
814+ <ul class =" list-disc list-inside text-xs text-gray-300 space-y-0.5" >
815+ <li v-for =" action in filteredActions" :key =" action.id" >
816+ {{ action.name }} <span class =" opacity-60" >— from {{ action.extensionName }}</span >
817+ </li >
818+ </ul >
819+ </div >
820+
821+ <div v-if =" pendingJoystickSuggestions.length > 0" >
822+ <div class =" flex items-center gap-2 mb-1" >
823+ <v-icon size =" 16" >mdi-gamepad-variant-outline</v-icon >
824+ <h3 class =" text-sm font-semibold" >
825+ Pending joystick mappings ({{ pendingJoystickSuggestions.length }})
826+ </h3 >
827+ </div >
828+ <ul class =" list-disc list-inside text-xs text-gray-300 space-y-0.5" >
829+ <li v-for =" item in pendingJoystickSuggestions" :key =" item.id" >
830+ {{ item.actionName }} <span class =" opacity-60" >— from {{ item.extensionName }}</span >
831+ </li >
832+ </ul >
833+ </div >
834+ </div >
835+ </v-card-text >
836+
837+ <div class =" flex justify-center w-full px-6 pb-2" >
838+ <v-divider style =" border-color : #ffffff14 " ></v-divider >
839+ </div >
840+
841+ <v-card-actions class =" px-6 pb-4 justify-space-between" >
842+ <v-btn variant =" text" @click =" confirmCloseModal" >Close anyway</v-btn >
843+ <v-btn @click =" closeConfirmationDialog = false" >Keep reviewing</v-btn >
844+ </v-card-actions >
845+ </v-card >
846+ </v-dialog >
766847 </div >
767848 </GlassModal >
768849</template >
@@ -1166,11 +1247,35 @@ const ignoredJoystickSuggestionsByExtension = computed(() => {
11661247 .filter ((ext ) => ext .suggestionGroups .length > 0 )
11671248})
11681249
1250+ /**
1251+ * Whether there are new actions pending user action
1252+ */
1253+ const hasPendingActions = computed (() => filteredActions .value .length > 0 )
1254+
1255+ /**
1256+ * Whether there are new joystick suggestions pending user action
1257+ */
1258+ const hasPendingJoystickSuggestions = computed (() => filteredJoystickSuggestionsByExtension .value .length > 0 )
1259+
11691260/**
11701261 * Whether there are new extension features that still need user action
11711262 */
11721263const hasPendingBlueOSFeatures = computed (() => {
1173- return filteredActions .value .length > 0 || filteredJoystickSuggestionsByExtension .value .length > 0
1264+ return hasPendingActions .value || hasPendingJoystickSuggestions .value
1265+ })
1266+
1267+ /**
1268+ * Flat list of pending joystick suggestions with their extension names
1269+ */
1270+ const pendingJoystickSuggestions = computed ((): JoystickSuggestionWithExtensionName [] => {
1271+ return filteredJoystickSuggestionsByExtension .value .flatMap ((ext ) =>
1272+ ext .suggestionGroups .flatMap ((group ) =>
1273+ group .buttonMappingSuggestions .map ((suggestion ) => ({
1274+ ... suggestion ,
1275+ extensionName: ext .extensionName ,
1276+ }))
1277+ )
1278+ )
11741279})
11751280
11761281/**
@@ -1508,6 +1613,11 @@ const restoreIgnoredSuggestion = (suggestion: JoystickMapSuggestion): void => {
15081613 })
15091614}
15101615
1616+ /**
1617+ * Controls visibility of the close confirmation dialog
1618+ */
1619+ const closeConfirmationDialog = ref (false )
1620+
15111621/**
15121622 * Close the modal
15131623 */
@@ -1516,6 +1626,25 @@ const closeModal = (): void => {
15161626 emit (' close' )
15171627}
15181628
1629+ /**
1630+ * Request to close the modal. If there are still pending items to decide upon, ask the user to confirm first.
1631+ */
1632+ const requestCloseModal = (): void => {
1633+ if (hasPendingBlueOSFeatures .value ) {
1634+ closeConfirmationDialog .value = true
1635+ return
1636+ }
1637+ closeModal ()
1638+ }
1639+
1640+ /**
1641+ * Confirm closing the modal from the confirmation dialog
1642+ */
1643+ const confirmCloseModal = (): void => {
1644+ closeConfirmationDialog .value = false
1645+ closeModal ()
1646+ }
1647+
15191648/**
15201649 * Check for available actions from BlueOS.
15211650 */
@@ -1594,6 +1723,32 @@ watch(activeTab, () => {
15941723 transition : width 0.2s ease ;
15951724}
15961725
1726+ .tab-blink {
1727+ position : relative ;
1728+ }
1729+
1730+ .tab-blink ::before {
1731+ content : ' ' ;
1732+ position : absolute ;
1733+ inset : 0 ;
1734+ border-top-left-radius : 6px ;
1735+ border-top-right-radius : 6px ;
1736+ background-color : #ffffff22 ;
1737+ animation : tab-blink 1.2s ease-in-out infinite ;
1738+ pointer-events : none ;
1739+ z-index : 0 ;
1740+ }
1741+
1742+ @keyframes tab-blink {
1743+ 0% ,
1744+ 100% {
1745+ opacity : 0 ;
1746+ }
1747+ 50% {
1748+ opacity : 1 ;
1749+ }
1750+ }
1751+
15971752.features-modal :has (.v-expansion-panels ) {
15981753 width : 760px ;
15991754}
0 commit comments