Skip to content

Commit

Permalink
core: frontend: Add popup library
Browse files Browse the repository at this point in the history
* Add a statically mounted popup library to allow launching stackable
  popups and get user interaction result
  • Loading branch information
JoaoMario109 committed Mar 11, 2024
1 parent b8df9e8 commit dcb5d84
Show file tree
Hide file tree
Showing 6 changed files with 542 additions and 0 deletions.
3 changes: 3 additions & 0 deletions core/frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@
<new-version-notificator />
<Wizard />
<alerter />
<PopupsManager />
<VTour
name="welcomeTour"
:steps="steps.filter((step) => step?.filter_wifi_connected !== wifi_connected)"
Expand Down Expand Up @@ -367,6 +368,7 @@ import EthernetUpdater from './components/ethernet/EthernetUpdater.vue'
import HealthTrayMenu from './components/health/HealthTrayMenu.vue'
import MavlinkUpdater from './components/mavlink/MavlinkUpdater.vue'
import NotificationTrayButton from './components/notifications/TrayButton.vue'
import PopupsManager from './components/popups/PopupsManager.vue'
import WifiTrayMenu from './components/wifi/WifiTrayMenu.vue'
import WifiUpdater from './components/wifi/WifiUpdater.vue'
import menus, { menuItem } from './menus'
Expand Down Expand Up @@ -398,6 +400,7 @@ export default Vue.extend({
SystemCheckerTrayMenu,
VehicleRebootRequiredTrayMenu,
Wizard,
PopupsManager,
},
data: () => ({
Expand Down
189 changes: 189 additions & 0 deletions core/frontend/src/components/popups/Popup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<template>
<vue-final-modal
v-model="open"
:click-to-close="options.allowClickToClose ?? true"
:esc-to-close="options.allowEscToClose ?? true"
:prevent-click="options.preventClick ?? false"
:z-index-base="zIndexBase"
classes="modal-container"
content-class="modal-content"
@before-close="onBeforeClose"
>
<v-card>
<v-card-title class="justify-space-between text-h5">
<span>{{ options.title }}</span>
<v-icon
v-if="options.showCloseButton ?? true"
icon
@click="dismiss()"
>
mdi-close
</v-icon>
</v-card-title>
<v-card-title
v-if="options.icon ?? true"
class="justify-center"
>
<v-icon
color="white"
size="45"
>
{{ icon }}
</v-icon>
</v-card-title>
<v-card-subtitle
class="mt-1"
>
{{ options.text }}
</v-card-subtitle>
<v-card-actions class="justify-center">
<v-btn
v-if="options.showCancelButton ?? true"
class="ma-2 elevation-2"
color="grey"
text
@click="cancel()"
>
{{ options.cancelButtonText ?? 'Cancel' }}
</v-btn>
<v-btn
v-if="options.showConfirmButton ?? true"
class="ma-2 elevation-2"
color="primary"
text
@click="confirm()"
>
{{ options.confirmButtonText ?? 'Confirm' }}
</v-btn>
</v-card-actions>
</v-card>
</vue-final-modal>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
import { PopupOptions, PopupResult } from '@/types/popups'
export default Vue.extend({
name: 'Popup',
props: {
identifier: {
type: String,
required: true,
},
options: {
type: Object as PropType<PopupOptions>,
required: true,
},
dismissed: {
type: Boolean,
required: true,
},
},
data() {
return {
open: true,
resolved: false,
}
},
computed: {
icon(): string {
switch (this.options.icon) {
case 'info':
return 'mdi-information'
case 'success':
return 'mdi-check-circle'
case 'warning':
return 'mdi-alert-octagon'
case 'error':
return 'mdi-close-circle'
case 'question':
return 'mdi-help-circle'
default:
return ''
}
},
zIndexBase(): number {
switch (this.options.priority) {
case 'low':
return 10000
case 'medium':
return 11000
case 'high':
return 12000
default:
return 9999
}
},
},
watch: {
dismissed(dismissed: boolean): void {
if (dismissed) {
this.dismiss()
}
},
},
methods: {
resolve(result: PopupResult): void {
this.open = false
this.resolved = true
setTimeout(() => {
this.$emit('resolve', result)
}, 300)
},
confirm(): void {
this.resolve({
id: this.identifier,
confirmed: true,
canceled: false,
dismissed: false,
})
},
cancel(): void {
this.resolve({
id: this.identifier,
confirmed: false,
canceled: true,
dismissed: false,
})
},
dismiss(): void {
this.resolve({
id: this.identifier,
confirmed: false,
canceled: false,
dismissed: true,
})
},
onBeforeClose(): void {
if (!this.resolved) {
this.resolve({
id: this.identifier,
confirmed: false,
canceled: false,
dismissed: true,
})
}
},
},
})
</script>
<style scoped>
::v-deep .modal-container {
display: flex;
justify-content: center;
align-items: center;
}
::v-deep .modal-content {
position: relative;
display: flex;
flex-direction: column;
max-height: 90%;
margin: 0 1rem;
padding: 1rem;
border-radius: 0.25rem;
}
</style>
37 changes: 37 additions & 0 deletions core/frontend/src/components/popups/PopupsManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<div
style="z-index: 9999;"
>
<Popup
v-for="popup in popups"
:key="popup.id"
:identifier="popup.id"
:options="popup.options"
:dismissed="popup.dismissed"
@resolve="popup.resolve"
/>
</div>
</template>

<script lang="ts">
import Vue from 'vue'
import popups from '@/store/popups'
import Popup from './Popup.vue'
export default Vue.extend({
name: 'PopupsManager',
components: {
Popup,
},
computed: {
popups() {
return popups.popup_values
},
},
})
</script>

<style>
</style>
77 changes: 77 additions & 0 deletions core/frontend/src/libs/popup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { v4 as uuid } from 'uuid'

import popups from '@/store/popups'
import { PopupOptions, PopupResult } from '@/types/popups'

class Popup {
/**
* The id of the popup
* @type {string}
*/
public id: string

/**
* The options to use when firing this popup
* @type {PopupOptions}
*/
public options: PopupOptions

/**
* Popup constructor
* @param {PopupOptions} options Options to use when firing the popup
*/
constructor(options: PopupOptions, id?: string) {
this.id = id ?? uuid()
this.options = {
...options,
id: this.id,
}
}

/**
* Fire a popup with the given options
* @param {PopupOptions} options Options to use when firing the popup
* @param {string} id Optional id to use when firing the popup so it can be
* programmatically closed after
* @returns {Promise<PopupResult>} A promise that resolves to the popup result
*/
public static fire(options: PopupOptions, id?: string): Promise<PopupResult> {
options.id = id ?? options.id ?? uuid()
return popups.fire(options)
}

/**
* Close a popup with the given id resolving it to a dismissed result
* @param {string} id The id of the popup to close
* @returns {void}
*/
public static close(id: string): void {
popups.close(id)
}

/**
* Close all popups resolving them to dismissed results
* @returns {void}
*/
public static closeAll(): void {
popups.closeAll()
}

/**
* Close current popup resolving it to a dismissed result
* @returns {void}
*/
public close(): void {
popups.close(this.id)
}

/**
* Fire current popup
* @returns {Promise<PopupResult>} A promise that resolves to the popup result
*/
public fire(): Promise<PopupResult> {
return popups.fire(this.options)
}
}

export default Popup
67 changes: 67 additions & 0 deletions core/frontend/src/store/popups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Vue from 'vue'
import {
Action, getModule, Module, Mutation, VuexModule,
} from 'vuex-module-decorators'

import store from '@/store'
import { ActivePopup, PopupOptions, PopupResult } from '@/types/popups'

@Module({
dynamic: true,
store,
name: 'popups',
})
class PopupsStore extends VuexModule {
popups: Record<string, ActivePopup> = {}

@Mutation
addPopup(popup: ActivePopup): void {
Vue.set(this.popups, popup.id, popup)
}

@Mutation
removePopup(id: string): void {
Vue.delete(this.popups, id)
}

@Mutation
close(id: string): void {
if (this.popups[id]) {
this.popups[id].dismissed = true
this.popups = { ...this.popups }
}
}

@Mutation
closeAll(): void {
for (const id in this.popups) {
this.popups[id].dismissed = true
}

this.popups = { ...this.popups }
}

@Action
fire(options: PopupOptions): Promise<PopupResult> {
return new Promise<PopupResult>((resolve) => {
this.addPopup({
id: options.id as string,
dismissed: false,
options,
resolve: (result: PopupResult) => {
this.removePopup(result.id)
resolve(result)
},
})
})
}

get popup_values(): ActivePopup[] {
return Object.values(this.popups)
}
}

export { PopupsStore }

const popups: PopupsStore = getModule(PopupsStore)
export default popups
Loading

0 comments on commit dcb5d84

Please sign in to comment.