Migrate to Bootstrap 5 and remove Vue compat plugin

Complete migration from Bootstrap 4 (bootstrap-vue) to Bootstrap 5
(bootstrap-vue-next) and remove the @vue/compat plugin to finalize
the Vue 3 migration.

Bundle size impact:
- Before (Bootstrap 4 + bootstrap-vue): 535 KiB gzipped
- After (Bootstrap 5 + bootstrap-vue-next): 511 KiB gzipped
- Reduction: 24 KiB (4.5% smaller)

Package updates:
- Update bootstrap 4.6.2 -> 5.3.8
- Update bootstrap-vue 2.23.1 -> bootstrap-vue-next 0.40.8
- Remove @vue/compat plugin
- Update vue 3.4.29 -> 3.5.24 and related packages
- Add mitt 3.0.1 for global event bus
- Add vue-demi 0.14.10 for library compatibility

Bootstrap 5 CSS updates:
- Replace directional classes: ml/mr/pl/pr -> ms/me/ps/pe
- Replace text-left/right -> text-start/end
- Replace sr-only -> visually-hidden / visually-hidden-focusable
- Update media breakpoint xs -> sm (Bootstrap 5 removed xs)
- Update color functions: gray("700") -> $gray-700
- Add form-switch border-radius for curved toggles
- Update alert, table, toast, form, and button styles

Bootstrap-Vue-Next API changes:
- Use createBootstrap() for plugin registration
- Update modal footer slots: #modal-footer -> #footer
- Fix form select events: @change -> @update:model-value
- Add v-model bindings to modals instead of manual show()/hide()
- Update toast system with custom plugin wrapping useToast()
- Register components and directives explicitly

Vue 3 specific updates:
- Replace $root.$emit with mitt event bus (eventBus.js)
- Update render function from h(App) to createApp(App)
- Add emits option to components
- Use h() instead of $createElement in mixins
- Add Vue 3 compile-time feature flags with documentation
- Update event listeners: $on/$off to eventBus methods
- Add beforeUnmount cleanup for event listeners

New components and significant additions:
- src/plugins/toast.js - Custom toast plugin wrapping useToast() for
  Options API compatibility
- src/components/Global/ConfirmModal.vue - Global confirmation dialog
  shim to replace Bootstrap 4's removed bvModal.msgBoxConfirm
- src/eventBus.js - mitt-based event bus with Vue 2-compatible API
- Navigation state preservation on page refresh implemented

Critical fixes:
- Add global API interceptor to strip Vue reactivity from payloads
- Preserve binary data (File, Blob, FormData) in API requests
- Fix Generate CSR modal v-model binding for proper open/close
- Remove debug logging and fix jest configuration
- Fix responsive text visibility in AppHeader
- Update BVTableSelectableMixin for proper row selection
- Fix BVToastMixin VNode rendering for Vue 3

Vue 3 modal fixes (lazy-loaded components):
- Add v-model support to network modals (ModalIpv4, ModalIpv6, ModalDns,
  ModalHostname, ModalMacAddress, ModalDefaultGateway) by adding
  modelValue prop, watcher on modelValue that triggers show(), and
  update:modelValue emit in resetForm
- Remove lazy loading from TableIpv4, TableIpv6, TableDns to ensure
  modal component refs are available when v-model triggers
- Fix modal title accessibility by adding title prop to modals
  (ModalAddDestination, ModalUser, ModalAddRoleGroup, etc.)

i18n fixes (computed properties):
- Fix computed properties using i18n translations in ModalAddRoleGroup,
  ModalUser, and ModalUploadCertificate
- Move useI18n() call from data() to setup() and return i18n object
- Use i18n.t() instead of $t in computed properties and templates
- Prevents "this.$t is not a function" and "_ctx.$t is not a function"
  errors in Vue 3

Toast notification fixes:
- Fix toast progress bar visibility by setting progressProps to
  undefined (documented way to opt-out) instead of false
- Change modelValue prop to interval for auto-dismiss timing
- Remove temporary CSS display:none hack from _toasts.scss

Network settings fixes:
- Fix checkbox @change event sending Vue reactive proxy object instead
  of boolean by casting with !! operator in changeDomainNameState and
  related methods in NetworkGlobalSettings.vue
- Ensures API receives plain boolean values in PATCH requests

Navigation fixes:
- Fix nav-link styling for navigation items without children by
  replacing b-nav-item with router-link in AppNavigation.vue
- Prevents blue font color from .nav-link CSS class

Configuration updates:
- Remove vue-compat webpack configuration
- Add Vue 3 feature flags (__VUE_OPTIONS_API__, etc.)
- Add .cursor to .gitignore

Accessibility improvements:
- Add autocomplete attributes to password and credential inputs
- Add modal title props for screen reader support

Build completes successfully and UI behavior matches pre-migration.

Extracted features (to be submitted in follow-up PRs):
The following features were removed from this migration PR to keep it
focused on the Bootstrap 5 upgrade. They will be submitted separately:
1. UnresponsiveModal - Server connectivity watchdog with auto-retry
2. Auth token persistence - sessionStorage support for X-Auth-Token
3. Hardware store error handling - try/catch, dynamic discovery
4. Login page connecting indicator - Backend polling with spinner
5. Test updates - Jest setup and snapshot updates for
   Bootstrap-Vue-Next
6. Documentation updates - Vue 3 and Vue I18n v9+ API documentation
7. Enhanced ConfirmModal - Feature-rich confirmation dialog with
   custom actions

Change-Id: Ib76a58f324b3c926cf536e6e4626e4271639de38
Signed-off-by: Jason Westover <jwestover@nvidia.com>
diff --git a/src/components/AppHeader/AppHeader.vue b/src/components/AppHeader/AppHeader.vue
index 89d561e..5b0072b 100644
--- a/src/components/AppHeader/AppHeader.vue
+++ b/src/components/AppHeader/AppHeader.vue
@@ -31,7 +31,7 @@
         </b-button>
         <b-navbar-nav>
           <b-navbar-brand
-            class="mr-0"
+            class="me-0"
             to="/"
             data-test-id="appHeader-container-overview"
           >
@@ -42,15 +42,15 @@
               :alt="altLogo"
             />
           </b-navbar-brand>
-          <div v-if="isNavTagPresent" :key="routerKey" class="pl-2 nav-tags">
+          <div v-if="isNavTagPresent" :key="routerKey" class="ps-2 nav-tags">
             <span>|</span>
-            <span class="pl-3 asset-tag">{{ assetTag }}</span>
-            <span class="pl-3">{{ modelType }}</span>
-            <span class="pl-3">{{ serialNumber }}</span>
+            <span class="ps-3 asset-tag">{{ assetTag }}</span>
+            <span class="ps-3">{{ modelType }}</span>
+            <span class="ps-3">{{ serialNumber }}</span>
           </div>
         </b-navbar-nav>
         <!-- Right aligned nav items -->
-        <b-navbar-nav class="ml-auto helper-menu">
+        <b-navbar-nav class="ms-auto helper-menu">
           <b-nav-item
             to="/logs/event-logs"
             data-test-id="appHeader-container-health"
@@ -220,11 +220,17 @@
     this.getEvents();
   },
   mounted() {
-    this.$root.$on(
+    require('@/eventBus').default.$on(
       'change-is-navigation-open',
       (isNavigationOpen) => (this.isNavigationOpen = isNavigationOpen),
     );
   },
+  beforeUnmount() {
+    require('@/eventBus').default.$off(
+      'change-is-navigation-open',
+      this.handleNavigationChange,
+    );
+  },
   methods: {
     getSystemInfo() {
       this.$store.dispatch('global/getSystemInfo');
@@ -239,11 +245,11 @@
       this.$store.dispatch('authentication/logout');
     },
     toggleNavigation() {
-      this.$root.$emit('toggle-navigation');
+      require('@/eventBus').default.$emit('toggle-navigation');
     },
     setFocus(event) {
       event.preventDefault();
-      this.$root.$emit('skip-navigation');
+      require('@/eventBus').default.$emit('skip-navigation');
     },
   },
 };
@@ -270,7 +276,7 @@
   .navbar-text,
   .nav-link,
   .btn-link {
-    color: color('white') !important;
+    color: $white !important;
     fill: currentColor;
     padding: 0.68rem 1rem !important;
 
@@ -299,13 +305,13 @@
 
     .helper-menu {
       @include media-breakpoint-down(sm) {
-        background-color: gray('800');
+        background-color: $gray-800;
         width: 100%;
         justify-content: flex-end;
 
         .nav-link,
         .btn {
-          padding: $spacer / 1.125 $spacer / 2;
+          padding: calc(#{$spacer} / 1.125) calc(#{$spacer} / 2);
         }
 
         .nav-link:focus,
@@ -315,8 +321,8 @@
       }
 
       .responsive-text {
-        @include media-breakpoint-down(xs) {
-          @include sr-only;
+        @include media-breakpoint-down(sm) {
+          @include visually-hidden;
         }
       }
     }
@@ -334,12 +340,12 @@
     }
     .nav-tags {
       color: theme-color-level(light, 3);
-      @include media-breakpoint-down(xs) {
-        @include sr-only;
+      @include media-breakpoint-down(sm) {
+        @include visually-hidden;
       }
       .asset-tag {
         @include media-breakpoint-down($responsive-layout-bp) {
-          @include sr-only;
+          @include visually-hidden;
         }
       }
     }
@@ -364,7 +370,7 @@
     }
 
     &.open {
-      background-color: gray('800');
+      background-color: $gray-800;
     }
 
     @include media-breakpoint-up($responsive-layout-bp) {
@@ -388,13 +394,13 @@
 }
 
 .navbar-brand {
-  padding: $spacer/2;
+  padding: calc(#{$spacer} / 2);
   height: $header-height;
   line-height: 1;
   &:focus {
     box-shadow:
       inset 0 0 0 3px $navbar-color,
-      inset 0 0 0 5px color('white');
+      inset 0 0 0 5px $white;
     outline: 0;
   }
 }
diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue
index 45a95f5..aa8598b 100644
--- a/src/components/AppNavigation/AppNavigation.vue
+++ b/src/components/AppNavigation/AppNavigation.vue
@@ -5,32 +5,47 @@
         <b-nav vertical class="mb-4">
           <template v-for="navItem in navigationItems">
             <!-- Navigation items with no children -->
-            <b-nav-item
+            <li
               v-if="!navItem.children"
-              :key="navItem.index"
-              :to="navItem.route"
-              :data-test-id="`nav-item-${navItem.id}`"
+              :key="`nav-${navItem.index}`"
+              class="nav-item"
             >
-              <component :is="navItem.icon" />
-              {{ navItem.label }}
-            </b-nav-item>
+              <router-link
+                :to="navItem.route"
+                :data-test-id="`nav-item-${navItem.id}`"
+                class="nav-link"
+              >
+                <component :is="navItem.icon" />
+                {{ navItem.label }}
+              </router-link>
+            </li>
 
             <!-- Navigation items with children -->
-            <li v-else :key="navItem.index" class="nav-item">
+            <li v-else :key="`nav-group-${navItem.index}`" class="nav-item">
               <b-button
-                v-b-toggle="`${navItem.id}`"
+                :class="{ collapsed: !isItemOpen(navItem.id) }"
                 variant="link"
                 :data-test-id="`nav-button-${navItem.id}`"
+                :aria-controls="navItem.id"
+                :aria-expanded="isItemOpen(navItem.id) ? 'true' : 'false'"
+                @click="toggleCollapse(navItem.id)"
               >
                 <component :is="navItem.icon" />
                 {{ navItem.label }}
                 <icon-expand class="icon-expand" />
               </b-button>
-              <b-collapse :id="navItem.id" tag="ul" class="nav-item__nav">
-                <li class="nav-item">
+              <b-collapse
+                :id="navItem.id"
+                v-model="openSections[navItem.id]"
+                tag="ul"
+                class="nav-item__nav"
+              >
+                <li
+                  v-for="(subNavItem, i) in filteredNavItem(navItem.children)"
+                  :key="i"
+                  class="nav-item"
+                >
                   <router-link
-                    v-for="(subNavItem, i) of filteredNavItem(navItem.children)"
-                    :key="i"
                     :to="subNavItem.route"
                     :data-test-id="`nav-item-${subNavItem.id}`"
                     class="nav-link"
@@ -70,21 +85,50 @@
       $t: useI18n().t,
       isNavigationOpen: false,
       currentUserRole: null,
+      openSections: {},
     };
   },
   watch: {
     $route: function () {
       this.isNavigationOpen = false;
+      // Ensure the parent section of the current route is expanded
+      this.initializeOpenSectionsFromRoute();
     },
     isNavigationOpen: function (isNavigationOpen) {
-      this.$root.$emit('change-is-navigation-open', isNavigationOpen);
+      require('@/eventBus').default.$emit(
+        'change-is-navigation-open',
+        isNavigationOpen,
+      );
     },
   },
   mounted() {
     this.getPrivilege();
-    this.$root.$on('toggle-navigation', () => this.toggleIsOpen());
+    require('@/eventBus').default.$on('toggle-navigation', () =>
+      this.toggleIsOpen(),
+    );
+    // Expand the parent section for the current route on initial load/refresh
+    this.initializeOpenSectionsFromRoute();
+  },
+  beforeUnmount() {
+    require('@/eventBus').default.$off(
+      'toggle-navigation',
+      this.handleToggleNavigation,
+    );
   },
   methods: {
+    isItemOpen(id) {
+      return !!this.openSections[id];
+    },
+    toggleCollapse(id) {
+      if (this.$set) {
+        this.$set(this.openSections, id, !this.openSections[id]);
+      } else {
+        this.openSections = {
+          ...this.openSections,
+          [id]: !this.openSections[id],
+        };
+      }
+    },
     toggleIsOpen() {
       this.isNavigationOpen = !this.isNavigationOpen;
     },
@@ -99,6 +143,20 @@
         });
       } else return navItem;
     },
+    initializeOpenSectionsFromRoute() {
+      const currentPath = this.$route?.path;
+      if (!currentPath) return;
+      const sectionsToOpen = {};
+      for (const item of this.navigationItems) {
+        if (
+          item.children &&
+          item.children.some((child) => child.route === currentPath)
+        ) {
+          sectionsToOpen[item.id] = true;
+        }
+      }
+      this.openSections = { ...this.openSections, ...sectionsToOpen };
+    },
   },
 };
 </script>
@@ -108,15 +166,15 @@
   fill: currentColor;
   height: 1.2rem;
   width: 1.2rem;
-  margin-left: 0 !important; //!important overriding button specificity
+  margin-inline-start: 0 !important; //!important overriding button specificity
   vertical-align: text-bottom;
   &:not(.icon-expand) {
-    margin-right: $spacer;
+    margin-inline-end: $spacer;
   }
 }
 
 .nav {
-  padding-top: $spacer / 4;
+  padding-top: calc(#{$spacer} / 4);
   @include media-breakpoint-up($responsive-layout-bp) {
     padding-top: $spacer;
   }
@@ -124,15 +182,16 @@
 
 .nav-item__nav {
   list-style: none;
-  padding-left: 0;
-  margin-left: 0;
+  padding-inline-start: 0;
+  margin-inline-start: 0;
 
   .nav-item {
     outline: none;
+    list-style: none;
   }
 
   .nav-link {
-    padding-left: $spacer * 4;
+    padding-inline-start: $spacer * 4;
     outline: none;
 
     &:not(.nav-link--current) {
@@ -144,7 +203,7 @@
 .btn-link {
   display: inline-block;
   width: 100%;
-  text-align: left;
+  text-align: start;
   text-decoration: none !important;
   border-radius: 0;
 
@@ -156,16 +215,16 @@
 }
 
 .icon-expand {
-  float: right;
-  margin-top: $spacer / 4;
+  float: inline-end;
+  margin-top: calc(#{$spacer} / 4);
 }
 
 .btn-link,
 .nav-link {
   position: relative;
   font-weight: $headings-font-weight;
-  padding-left: $spacer; // defining consistent padding for links and buttons
-  padding-right: $spacer;
+  padding-inline-start: $spacer; // defining consistent padding for links and buttons
+  padding-inline-end: $spacer;
   color: theme-color('secondary');
 
   &:hover {
@@ -198,7 +257,7 @@
     position: absolute;
     top: 0;
     bottom: 0;
-    left: 0;
+    inset-inline-start: 0;
     width: 4px;
     background-color: theme-color('primary');
   }
@@ -221,7 +280,7 @@
   background-color: theme-color('light');
   transform: translateX(-$navigation-width);
   transition: transform $exit-easing--productive $duration--moderate-02;
-  border-right: 1px solid theme-color-level('light', 2.85);
+  border-inline-end: 1px solid theme-color-level('light', 2.85);
 
   @include media-breakpoint-down(md) {
     z-index: $zindex-fixed + 2;
diff --git a/src/components/Global/Alert.vue b/src/components/Global/Alert.vue
index e8de9e2..a66d112 100644
--- a/src/components/Global/Alert.vue
+++ b/src/components/Global/Alert.vue
@@ -24,7 +24,7 @@
 
 <script>
 import StatusIcon from '@/components/Global/StatusIcon';
-import { BAlert } from 'bootstrap-vue';
+import { BAlert } from 'bootstrap-vue-next';
 
 export default {
   name: 'Alert',
diff --git a/src/components/Global/ButtonBackToTop.vue b/src/components/Global/ButtonBackToTop.vue
index 6d2f740..be6c75d 100644
--- a/src/components/Global/ButtonBackToTop.vue
+++ b/src/components/Global/ButtonBackToTop.vue
@@ -8,7 +8,9 @@
     @click="scrollToTop"
   >
     <icon-up-to-top />
-    <span class="sr-only">{{ $t('global.ariaLabel.scrollToTop') }}</span>
+    <span class="visually-hidden-focusable">
+      {{ $t('global.ariaLabel.scrollToTop') }}
+    </span>
   </b-button>
 </template>
 
diff --git a/src/components/Global/ConfirmModal.vue b/src/components/Global/ConfirmModal.vue
new file mode 100644
index 0000000..8a9a7b8
--- /dev/null
+++ b/src/components/Global/ConfirmModal.vue
@@ -0,0 +1,54 @@
+<template>
+  <!-- Simplified ConfirmModal using native Window.confirm() -->
+  <!-- This component preserves the API for future proper modal implementation -->
+  <div style="display: none"></div>
+</template>
+
+<script>
+export default {
+  name: 'ConfirmModal',
+  data() {
+    return {
+      resolve: null,
+    };
+  },
+  created() {
+    const bus = require('@/eventBus').default;
+    bus.$on('confirm:open', this.handleConfirm);
+  },
+  beforeUnmount() {
+    require('@/eventBus').default.$off('confirm:open', this.handleConfirm);
+  },
+  methods: {
+    handleConfirm(options) {
+      // Extract message from options (could be string or object)
+      const message =
+        typeof options === 'string'
+          ? options
+          : options.message || 'Are you sure?';
+
+      // Use native browser confirm for now
+      // The following parameters are accepted but not used by the window.confirm() shim.
+      // They will be used when the proper Bootstrap 5 modal is implemented:
+      // - title: Modal title text
+      // - okTitle: OK/Confirm button text
+      // - cancelTitle: Cancel button text
+      // - okVariant: OK button Bootstrap variant (e.g., 'danger', 'primary')
+      // - cancelVariant: Cancel button Bootstrap variant (e.g., 'secondary')
+      // - autoFocusButton: Which button to focus ('ok' or 'cancel')
+      // - processing: Show processing state with progress bar
+      // - processingText: Processing state message
+      // - processingMax: Processing progress bar maximum value
+      //
+      // Code can safely pass these parameters now and they will work when the
+      // proper modal implementation is added.
+      const result = window.confirm(message);
+
+      // Resolve the promise with result
+      if (options.resolve) {
+        options.resolve(result);
+      }
+    },
+  },
+};
+</script>
diff --git a/src/components/Global/FormFile.vue b/src/components/Global/FormFile.vue
index c337bf1..57eface 100644
--- a/src/components/Global/FormFile.vue
+++ b/src/components/Global/FormFile.vue
@@ -1,37 +1,39 @@
 <template>
   <div class="custom-form-file-container">
-    <label>
-      <b-form-file
-        :id="id"
-        v-model="file"
-        :accept="accept"
-        :disabled="disabled"
-        :state="state"
-        plain
-        @input="$emit('input', $event)"
-      >
-      </b-form-file>
-      <span
-        class="add-file-btn btn"
-        :class="{
-          disabled,
-          'btn-secondary': isSecondary,
-          'btn-primary': !isSecondary,
-        }"
-      >
-        {{ $t('global.fileUpload.browseText') }}
-      </span>
-      <slot name="invalid"></slot>
-    </label>
+    <b-form-file
+      :id="id"
+      ref="fileInput"
+      v-model="file"
+      :accept="accept"
+      :disabled="disabled"
+      :state="state"
+      plain
+      @input="$emit('input', $event)"
+    >
+    </b-form-file>
+    <button
+      type="button"
+      class="add-file-btn btn mt-2"
+      :class="{
+        disabled,
+        'btn-secondary': isSecondary,
+        'btn-primary': !isSecondary,
+      }"
+      :disabled="disabled"
+      @click="openFilePicker"
+    >
+      {{ $t('global.fileUpload.browseText') }}
+    </button>
+    <slot name="invalid"></slot>
     <div v-if="file" class="clear-selected-file px-3 py-2 mt-2">
       {{ file ? file.name : '' }}
       <b-button
         variant="light"
-        class="px-2 ml-auto"
+        class="px-2 ms-auto"
         :disabled="disabled"
         @click="file = null"
         ><icon-close :title="$t('global.fileUpload.clearSelectedFile')" /><span
-          class="sr-only"
+          class="visually-hidden-focusable"
           >{{ $t('global.fileUpload.clearSelectedFile') }}</span
         >
       </b-button>
@@ -40,7 +42,7 @@
 </template>
 
 <script>
-import { BFormFile } from 'bootstrap-vue';
+import { BFormFile } from 'bootstrap-vue-next';
 import IconClose from '@carbon/icons-vue/es/close/20';
 import { useI18n } from 'vue-i18n';
 
@@ -81,36 +83,48 @@
       return this.variant === 'secondary';
     },
   },
+  methods: {
+    openFilePicker() {
+      // Access the native input element within the BFormFile component
+      const fileInput = document.getElementById(this.id);
+      if (fileInput) {
+        fileInput.click();
+      }
+    },
+  },
 };
 </script>
 
 <style lang="scss" scoped>
-.form-control-file {
+// Hide the native file input but keep it accessible
+:deep(.form-control),
+:deep(input[type='file']) {
   opacity: 0;
   height: 0;
-  &:focus + span {
+  width: 0;
+  position: absolute;
+  pointer-events: none;
+}
+
+.add-file-btn {
+  &.disabled {
+    border-color: $gray-400;
+    background-color: $gray-400;
+    color: $gray-600;
+    box-shadow: none !important;
+  }
+  &:focus {
     box-shadow:
       inset 0 0 0 3px theme-color('primary'),
       inset 0 0 0 5px $white;
   }
 }
 
-// Get mouse pointer on complete element
-.add-file-btn {
-  position: relative;
-  &.disabled {
-    border-color: gray('400');
-    background-color: gray('400');
-    color: gray('600');
-    box-shadow: none !important;
-  }
-}
-
 .clear-selected-file {
   display: flex;
   align-items: center;
   background-color: theme-color('light');
-  word-break: break-all; // break long file name into multiple lines
+  word-break: break-all;
   .btn {
     width: 36px;
     height: 36px;
diff --git a/src/components/Global/InfoTooltip.vue b/src/components/Global/InfoTooltip.vue
index fc80216..cd7acc1 100644
--- a/src/components/Global/InfoTooltip.vue
+++ b/src/components/Global/InfoTooltip.vue
@@ -6,7 +6,9 @@
     :title="title"
   >
     <icon-tooltip />
-    <span class="sr-only">{{ $t('global.ariaLabel.tooltip') }}</span>
+    <span class="visually-hidden-focusable">
+      {{ $t('global.ariaLabel.tooltip') }}
+    </span>
   </b-button>
 </template>
 
diff --git a/src/components/Global/InputPasswordToggle.vue b/src/components/Global/InputPasswordToggle.vue
index b682cd5..4212dc2 100644
--- a/src/components/Global/InputPasswordToggle.vue
+++ b/src/components/Global/InputPasswordToggle.vue
@@ -10,7 +10,7 @@
     >
       <icon-view-off v-if="isVisible" />
       <icon-view v-else />
-      <span class="sr-only">{{ togglePasswordLabel }}</span>
+      <span class="visually-hidden">{{ togglePasswordLabel }}</span>
     </b-button>
   </div>
 </template>
@@ -31,8 +31,7 @@
   },
   methods: {
     toggleVisibility() {
-      const firstChild = this.$children[0];
-      const inputEl = firstChild ? firstChild.$el : null;
+      const inputEl = this.$el.querySelector('input');
 
       this.isVisible = !this.isVisible;
 
@@ -55,5 +54,6 @@
 <style lang="scss" scoped>
 .input-password-toggle-container {
   position: relative;
+  display: inline-block;
 }
 </style>
diff --git a/src/components/Global/LoadingBar.vue b/src/components/Global/LoadingBar.vue
index 8b63093..9297690 100644
--- a/src/components/Global/LoadingBar.vue
+++ b/src/components/Global/LoadingBar.vue
@@ -24,16 +24,21 @@
     };
   },
   created() {
-    this.$root.$on('loader-start', () => {
+    this.$eventBus.on('loader-start', () => {
       this.startLoadingInterval();
     });
-    this.$root.$on('loader-end', () => {
+    this.$eventBus.on('loader-end', () => {
       this.endLoadingInterval();
     });
-    this.$root.$on('loader-hide', () => {
+    this.$eventBus.on('loader-hide', () => {
       this.hideLoadingBar();
     });
   },
+  beforeUnmount() {
+    this.$eventBus.off('loader-start', this.handleLoaderStart);
+    this.$eventBus.off('loader-end', this.handleLoaderEnd);
+    this.$eventBus.off('loader-hide', this.handleLoaderHide);
+  },
   methods: {
     startLoadingInterval() {
       this.clearLoadingInterval();
diff --git a/src/components/Global/PageContainer.vue b/src/components/Global/PageContainer.vue
index ab4adb6..762a0d7 100644
--- a/src/components/Global/PageContainer.vue
+++ b/src/components/Global/PageContainer.vue
@@ -10,10 +10,18 @@
   name: 'PageContainer',
   mixins: [JumpLinkMixin],
   created() {
-    this.$root.$on('skip-navigation', () => {
+    // Use global event bus instead of removed $root.$on
+    const eventBus = require('@/eventBus').default;
+    eventBus.$on('skip-navigation', () => {
       this.setFocus(this.$el);
     });
   },
+  beforeUnmount() {
+    require('@/eventBus').default.$off(
+      'skip-navigation',
+      this.handleSkipNavigation,
+    );
+  },
 };
 </script>
 <style lang="scss" scoped>
@@ -22,8 +30,8 @@
   height: 100%;
   padding-top: $spacer * 1.5;
   padding-bottom: $spacer * 3;
-  padding-left: $spacer;
-  padding-right: $spacer;
+  padding-inline-start: $spacer;
+  padding-inline-end: $spacer;
 
   &:focus-visible {
     box-shadow: inset 0 0 0 2px theme-color('primary');
@@ -31,7 +39,7 @@
   }
 
   @include media-breakpoint-up($responsive-layout-bp) {
-    padding-left: $spacer * 2;
+    padding-inline-start: $spacer * 2;
   }
 }
 </style>
diff --git a/src/components/Global/Search.vue b/src/components/Global/Search.vue
index dcc1ca0..82d9719 100644
--- a/src/components/Global/Search.vue
+++ b/src/components/Global/Search.vue
@@ -2,16 +2,18 @@
   <div class="search-global">
     <b-form-group
       :label="$t('global.form.search')"
-      :label-for="`searchInput-${_uid}`"
+      :label-for="`searchInput-${uid}`"
       label-class="invisible"
       class="mb-2"
     >
       <b-input-group size="md" class="align-items-center">
-        <b-input-group-prepend>
-          <icon-search class="search-icon" />
-        </b-input-group-prepend>
+        <template #prepend>
+          <b-input-group-text>
+            <icon-search class="search-icon" />
+          </b-input-group-text>
+        </template>
         <b-form-input
-          :id="`searchInput-${_uid}`"
+          :id="`searchInput-${uid}`"
           ref="searchInput"
           v-model="filter"
           class="search-input"
@@ -29,7 +31,9 @@
           @click="onClearSearch"
         >
           <icon-close />
-          <span class="sr-only">{{ $t('global.ariaLabel.clearSearch') }}</span>
+          <span class="visually-hidden">
+            {{ $t('global.ariaLabel.clearSearch') }}
+          </span>
         </b-button>
       </b-input-group>
     </b-form-group>
@@ -58,6 +62,7 @@
     return {
       $t: useI18n().t,
       filter: null,
+      uid: Math.random().toString(36).slice(2),
     };
   },
   methods: {
@@ -67,21 +72,15 @@
     onClearSearch() {
       this.filter = '';
       this.$emit('clear-search');
-      this.$refs.searchInput.focus();
+      const input = this.$refs.searchInput;
+      if (input && typeof input.focus === 'function') input.focus();
     },
   },
 };
 </script>
 
 <style lang="scss" scoped>
-.search-input {
-  padding-left: ($spacer * 2);
-}
 .search-icon {
-  position: absolute;
-  left: 10px;
-  top: 12px;
-  z-index: 4;
-  stroke: gray('400');
+  stroke: $gray-400;
 }
 </style>
diff --git a/src/components/Global/StatusIcon.vue b/src/components/Global/StatusIcon.vue
index 4552633..9044e52 100644
--- a/src/components/Global/StatusIcon.vue
+++ b/src/components/Global/StatusIcon.vue
@@ -47,7 +47,7 @@
     color: theme-color('danger');
   }
   &.secondary {
-    color: gray('600');
+    color: $gray-600;
     transform: rotate(-45deg);
   }
   &.warning {
diff --git a/src/components/Global/TableDateFilter.vue b/src/components/Global/TableDateFilter.vue
index ca1c852..6d11a07 100644
--- a/src/components/Global/TableDateFilter.vue
+++ b/src/components/Global/TableDateFilter.vue
@@ -4,13 +4,13 @@
       <b-form-group
         :label="$t('global.table.fromDate')"
         label-for="input-from-date"
-        class="mr-3 my-0 w-100"
+        class="me-3 my-0 w-100"
       >
         <b-input-group>
           <b-form-input
             id="input-from-date"
             v-model="fromDate"
-            placeholder="YYYY-MM-DD"
+            type="date"
             :state="getValidationState(v$.fromDate)"
             class="form-control-with-button mb-3 mb-md-0"
             @blur="v$.fromDate.$touch()"
@@ -20,31 +20,13 @@
               {{ $t('global.form.invalidFormat') }}
             </template>
             <template v-if="v$.fromDate.maxDate.$invalid">
-              {{ $t('global.form.dateMustBeBefore', { date: toDate }) }}
+              {{
+                $t('global.form.dateMustBeBefore', {
+                  date: toDate,
+                })
+              }}
             </template>
           </b-form-invalid-feedback>
-          <b-form-datepicker
-            v-model="fromDate"
-            class="btn-datepicker btn-icon-only"
-            button-only
-            right
-            :max="toDate"
-            :hide-header="true"
-            :locale="locale"
-            :label-help="
-              $t('global.calendar.useCursorKeysToNavigateCalendarDates')
-            "
-            :title="$t('global.calendar.selectDate')"
-            button-variant="link"
-            aria-controls="input-from-date"
-          >
-            <template #button-content>
-              <icon-calendar />
-              <span class="sr-only">
-                {{ $t('global.calendar.selectDate') }}
-              </span>
-            </template>
-          </b-form-datepicker>
         </b-input-group>
       </b-form-group>
       <b-form-group
@@ -56,7 +38,7 @@
           <b-form-input
             id="input-to-date"
             v-model="toDate"
-            placeholder="YYYY-MM-DD"
+            type="date"
             :state="getValidationState(v$.toDate)"
             class="form-control-with-button"
             @blur="v$.toDate.$touch()"
@@ -66,31 +48,13 @@
               {{ $t('global.form.invalidFormat') }}
             </template>
             <template v-if="v$.toDate.minDate.$invalid">
-              {{ $t('global.form.dateMustBeAfter', { date: fromDate }) }}
+              {{
+                $t('global.form.dateMustBeAfter', {
+                  date: fromDate,
+                })
+              }}
             </template>
           </b-form-invalid-feedback>
-          <b-form-datepicker
-            v-model="toDate"
-            class="btn-datepicker btn-icon-only"
-            button-only
-            right
-            :min="fromDate"
-            :hide-header="true"
-            :locale="locale"
-            :label-help="
-              $t('global.calendar.useCursorKeysToNavigateCalendarDates')
-            "
-            :title="$t('global.calendar.selectDate')"
-            button-variant="link"
-            aria-controls="input-to-date"
-          >
-            <template #button-content>
-              <icon-calendar />
-              <span class="sr-only">
-                {{ $t('global.calendar.selectDate') }}
-              </span>
-            </template>
-          </b-form-datepicker>
         </b-input-group>
       </b-form-group>
     </b-col>
@@ -98,7 +62,6 @@
 </template>
 
 <script>
-import IconCalendar from '@carbon/icons-vue/es/calendar/20';
 import { helpers } from 'vuelidate/lib/validators';
 import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
 import { useVuelidate } from '@vuelidate/core';
@@ -107,7 +70,6 @@
 const isoDateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;
 
 export default {
-  components: { IconCalendar },
   mixins: [VuelidateMixin],
   emits: ['change'],
   setup() {
diff --git a/src/components/Global/TableFilter.vue b/src/components/Global/TableFilter.vue
index 690f453..e3afb3e 100644
--- a/src/components/Global/TableFilter.vue
+++ b/src/components/Global/TableFilter.vue
@@ -3,7 +3,7 @@
     <p class="d-inline-block mb-0">
       <b-badge v-for="(tag, index) in tags" :key="index" pill>
         {{ tag }}
-        <b-button-close
+        <b-close-button
           :disabled="dropdownVisible"
           :aria-hidden="true"
           @click="removeTag(tag)"
@@ -22,7 +22,7 @@
         <icon-filter />
         {{ $t('global.action.filter') }}
       </template>
-      <b-dropdown-form>
+      <div class="px-3 py-2">
         <b-form-group
           v-for="(filter, index) of filters"
           :key="index"
@@ -35,20 +35,21 @@
               :value="value"
               :data-test-id="`tableFilter-checkbox-${value}`"
             >
-              <b-dropdown-item>
-                {{ value }}
-              </b-dropdown-item>
+              <span class="dropdown-item-text">{{ value }}</span>
             </b-form-checkbox>
           </b-form-checkbox-group>
         </b-form-group>
-      </b-dropdown-form>
-      <b-dropdown-item-button
-        variant="primary"
-        data-test-id="tableFilter-button-clearAll"
-        @click="clearAllTags"
-      >
-        {{ $t('global.action.clearAll') }}
-      </b-dropdown-item-button>
+      </div>
+      <div class="px-3 pb-2">
+        <b-button
+          size="sm"
+          variant="primary"
+          data-test-id="tableFilter-button-clearAll"
+          @click="clearAllTags"
+        >
+          {{ $t('global.action.clearAll') }}
+        </b-button>
+      </div>
     </b-dropdown>
   </div>
 </template>
@@ -113,6 +114,6 @@
 
 <style lang="scss" scoped>
 .badge {
-  margin-right: $spacer / 2;
+  margin-inline-end: calc(#{$spacer} / 2);
 }
 </style>
diff --git a/src/components/Global/TableRowAction.vue b/src/components/Global/TableRowAction.vue
index e00a380..79162bc 100644
--- a/src/components/Global/TableRowAction.vue
+++ b/src/components/Global/TableRowAction.vue
@@ -1,8 +1,9 @@
 <template>
   <span>
-    <b-link
+    <b-button
       v-if="value === 'export'"
-      class="align-bottom btn-icon-only py-0 btn-link"
+      variant="link"
+      class="align-bottom btn-icon-only py-0"
       :download="download"
       :href="href"
       :title="title"
@@ -10,46 +11,48 @@
       <slot name="icon">
         {{ $t('global.action.export') }}
       </slot>
-      <span v-if="btnIconOnly" class="sr-only">{{ title }}</span>
-    </b-link>
-    <b-link
+      <span v-if="btnIconOnly" class="visually-hidden">{{ title }}</span>
+    </b-button>
+    <b-button
       v-else-if="
         value === 'download' && downloadInNewTab && downloadLocation !== ''
       "
-      class="align-bottom btn-icon-only py-0 btn-link"
+      variant="link"
+      class="align-bottom btn-icon-only py-0"
       target="_blank"
       :href="downloadLocation"
       :title="title"
     >
       <slot name="icon" />
-      <span class="sr-only">
+      <span class="visually-hidden">
         {{ $t('global.action.download') }}
       </span>
-    </b-link>
-    <b-link
+    </b-button>
+    <b-button
       v-else-if="value === 'download' && downloadLocation !== ''"
-      class="align-bottom btn-icon-only py-0 btn-link"
+      variant="link"
+      class="align-bottom btn-icon-only py-0"
       :download="exportName"
       :href="downloadLocation"
       :title="title"
     >
       <slot name="icon" />
-      <span class="sr-only">
+      <span class="visually-hidden">
         {{ $t('global.action.download') }}
       </span>
-    </b-link>
+    </b-button>
     <b-button
       v-else-if="showButton"
       variant="link"
       :class="{ 'btn-icon-only': btnIconOnly }"
       :disabled="!enabled"
-      :title="btnIconOnly ? title : !title"
+      :title="title"
       @click="$emit('click-table-action', value)"
     >
       <slot name="icon">
         {{ title }}
       </slot>
-      <span v-if="btnIconOnly" class="sr-only">{{ title }}</span>
+      <span v-if="btnIconOnly" class="visually-hidden">{{ title }}</span>
     </b-button>
   </span>
 </template>
diff --git a/src/components/Global/TableToolbar.vue b/src/components/Global/TableToolbar.vue
index 373b90a..ad7c996 100644
--- a/src/components/Global/TableToolbar.vue
+++ b/src/components/Global/TableToolbar.vue
@@ -101,7 +101,7 @@
 
 // Using v-deep to style export slot child-element
 // depricated and vue-js 3
-.toolbar-actions ::v-deep .btn {
+.toolbar-actions :deep(.btn) {
   position: relative;
   &:after {
     content: '';
diff --git a/src/components/Mixins/BVPaginationMixin.js b/src/components/Mixins/BVPaginationMixin.js
index 1aa20a5..0834ae7 100644
--- a/src/components/Mixins/BVPaginationMixin.js
+++ b/src/components/Mixins/BVPaginationMixin.js
@@ -24,6 +24,15 @@
   },
 ];
 const BVPaginationMixin = {
+  watch: {
+    perPage(newPerPage) {
+      // When switching to "View all" (perPage === 0), reset to first page
+      // to avoid empty views when previously on a later page.
+      if (newPerPage === 0) {
+        this.currentPage = 1;
+      }
+    },
+  },
   methods: {
     getTotalRowCount(count) {
       return this.perPage === 0 ? 0 : count;
diff --git a/src/components/Mixins/BVTableSelectableMixin.js b/src/components/Mixins/BVTableSelectableMixin.js
index b4f0b95..48f5073 100644
--- a/src/components/Mixins/BVTableSelectableMixin.js
+++ b/src/components/Mixins/BVTableSelectableMixin.js
@@ -3,38 +3,123 @@
 export const tableHeaderCheckboxIndeterminate = false;
 
 const BVTableSelectableMixin = {
+  data() {
+    return {
+      selectedRows: [],
+      tableHeaderCheckboxModel: false,
+      tableHeaderCheckboxIndeterminate: false,
+    };
+  },
+  watch: {
+    currentPage() {
+      // Bootstrap Vue 2 behavior: Clear selections when page changes
+      // This prevents confusion with checkboxes appearing checked on the new page
+      const table = this.$refs.table;
+      if (table) {
+        table.clearSelected();
+        this.selectedRows = [];
+        this.tableHeaderCheckboxModel = false;
+        this.tableHeaderCheckboxIndeterminate = false;
+      }
+    },
+  },
   methods: {
     clearSelectedRows(tableRef) {
-      if (tableRef) tableRef.clearSelected();
+      if (tableRef) {
+        tableRef.clearSelected();
+        this.selectedRows = [];
+        this.tableHeaderCheckboxModel = false;
+        this.tableHeaderCheckboxIndeterminate = false;
+      }
     },
     toggleSelectRow(tableRef, rowIndex) {
       if (tableRef && rowIndex !== undefined) {
-        tableRef.isRowSelected(rowIndex)
-          ? tableRef.unselectRow(rowIndex)
-          : tableRef.selectRow(rowIndex);
-      }
-    },
-    onRowSelected(selectedRows, totalRowsCount) {
-      if (selectedRows && totalRowsCount !== undefined) {
-        this.selectedRows = selectedRows;
-        if (selectedRows.length === 0) {
-          this.tableHeaderCheckboxIndeterminate = false;
-          this.tableHeaderCheckboxModel = false;
-        } else if (selectedRows.length === totalRowsCount) {
-          this.tableHeaderCheckboxIndeterminate = false;
-          this.tableHeaderCheckboxModel = true;
+        const wasSelected = tableRef.isRowSelected(rowIndex);
+
+        if (wasSelected) {
+          tableRef.unselectRow(rowIndex);
         } else {
-          this.tableHeaderCheckboxIndeterminate = true;
-          this.tableHeaderCheckboxModel = true;
+          tableRef.selectRow(rowIndex);
         }
+
+        // Manually trigger onRowSelected after toggle since unselectRow might not fire event
+        this.$nextTick(() => {
+          this.onRowSelected();
+        });
       }
     },
-    onChangeHeaderCheckbox(tableRef) {
-      if (tableRef) {
-        if (this.tableHeaderCheckboxModel) tableRef.selectAllRows();
-        else tableRef.clearSelected();
+    onRowSelected() {
+      /*
+       * Bootstrap Vue Next fires @row-selected for each individual row change.
+       * Query the table's internal state to get ALL currently selected rows.
+       */
+      const table = this.$refs.table;
+      if (!table) return;
+
+      const allItems = table.filteredItems || table.items || [];
+      const selectedItems = allItems.filter((item, index) => {
+        return table.isRowSelected(index);
+      });
+
+      this.selectedRows = selectedItems;
+
+      // Update header checkbox state
+      const currentPage = this.currentPage || 1;
+      const perPage = this.perPage || 10;
+      const startIndex = (currentPage - 1) * perPage;
+      const endIndex = Math.min(startIndex + perPage, allItems.length);
+      const pageItemsCount = endIndex - startIndex;
+
+      const selectedOnPageCount = selectedItems.filter((item) =>
+        allItems
+          .slice(startIndex, endIndex)
+          .some((pageItem) => pageItem === item),
+      ).length;
+
+      if (selectedOnPageCount === 0) {
+        this.tableHeaderCheckboxIndeterminate = false;
+        this.tableHeaderCheckboxModel = false;
+      } else if (selectedOnPageCount === pageItemsCount) {
+        this.tableHeaderCheckboxIndeterminate = false;
+        this.tableHeaderCheckboxModel = true;
+      } else {
+        this.tableHeaderCheckboxIndeterminate = true;
+        this.tableHeaderCheckboxModel = true;
       }
     },
+    onChangeHeaderCheckbox(tableRef, event) {
+      /*
+       * Bootstrap Vue Next Migration:
+       * Handle header checkbox to select/deselect all rows on current page.
+       */
+      if (!tableRef) return;
+
+      // Extract checked state from event (could be boolean or Event object)
+      const isChecked =
+        typeof event === 'boolean' ? event : event?.target?.checked;
+
+      if (isChecked) {
+        // Select all rows on the current page
+        const currentPage = this.currentPage || 1;
+        const perPage = this.perPage || 10;
+        const startIndex = (currentPage - 1) * perPage;
+        const allItems = tableRef.filteredItems || tableRef.items || [];
+        const endIndex = Math.min(startIndex + perPage, allItems.length);
+
+        for (let i = startIndex; i < endIndex; i++) {
+          tableRef.selectRow(i);
+        }
+      } else {
+        // Deselect all rows
+        tableRef.clearSelected();
+        // Manually trigger update since clearSelected might not fire @row-selected
+        this.selectedRows = [];
+        this.tableHeaderCheckboxModel = false;
+        this.tableHeaderCheckboxIndeterminate = false;
+      }
+
+      // onRowSelected will be triggered automatically for selections
+    },
   },
 };
 
diff --git a/src/components/Mixins/BVToastMixin.js b/src/components/Mixins/BVToastMixin.js
index c8b58da..0d9fff5 100644
--- a/src/components/Mixins/BVToastMixin.js
+++ b/src/components/Mixins/BVToastMixin.js
@@ -1,58 +1,86 @@
+import { h } from 'vue';
 import StatusIcon from '../Global/StatusIcon';
 import i18n from '@/i18n';
-
 const BVToastMixin = {
   components: {
     StatusIcon,
   },
   methods: {
     $_BVToastMixin_createTitle(title, status) {
-      const statusIcon = this.$createElement('StatusIcon', {
-        props: { status },
-      });
-      const titleWithIcon = this.$createElement(
-        'strong',
-        { class: 'toast-icon' },
-        [statusIcon, title],
-      );
-      return titleWithIcon;
+      const statusIcon = h(StatusIcon, { status });
+      return h('strong', { class: 'toast-icon' }, [statusIcon, title]);
     },
     $_BVToastMixin_createBody(messageBody) {
       if (Array.isArray(messageBody)) {
-        return messageBody.map((message) =>
-          this.$createElement('p', { class: 'mb-0' }, message),
-        );
+        return messageBody.map((message) => h('p', { class: 'mb-0' }, message));
       } else {
-        return [this.$createElement('p', { class: 'mb-0' }, messageBody)];
+        return [h('p', { class: 'mb-0' }, messageBody)];
       }
     },
     $_BVToastMixin_createTimestamp() {
       const timestamp = this.$filters.formatTime(new Date());
-      return this.$createElement('p', { class: 'mt-3 mb-0' }, timestamp);
+      return h('p', { class: 'mt-3 mb-0' }, timestamp);
     },
     $_BVToastMixin_createRefreshAction() {
-      return this.$createElement(
+      return h(
         'BLink',
         {
           class: 'd-inline-block mt-3',
-          on: {
-            click: () => {
-              this.$root.$emit('refresh-application');
-            },
+          onClick: () => {
+            require('@/eventBus').default.$emit('refresh-application');
           },
         },
         i18n.global.t('global.action.refresh'),
       );
     },
     $_BVToastMixin_initToast(body, title, variant) {
-      this.$root.$bvToast.toast(body, {
-        title,
-        variant,
-        autoHideDelay: 10000, //auto hide in milliseconds
-        noAutoHide: variant !== 'success',
-        isStatus: true,
-        solid: true,
-      });
+      // Use global toast plugin (works with Options API)
+      // Extract text content from VNodes for display
+
+      // Extract title text from VNode
+      const titleText =
+        typeof title === 'string'
+          ? title
+          : title?.children?.[1] || title?.children || '';
+
+      // Extract body text from VNode array
+      // Each VNode (paragraph) should be on its own line
+      const bodyLines = Array.isArray(body)
+        ? body.map((node) => {
+            if (typeof node === 'string') return node;
+            // Extract text from VNode children
+            const text = node?.children || node?.props?.children || '';
+            // Ensure timestamps and other paragraphs are on separate lines
+            return text;
+          })
+        : [typeof body === 'string' ? body : body?.children || ''];
+
+      // Join with newlines to ensure timestamps appear on their own line
+      const bodyText = bodyLines.filter(Boolean).join('\n');
+
+      // Show toast via global plugin
+      if (this.$toast) {
+        this.$toast.show({
+          body: bodyText,
+          props: {
+            title: titleText,
+            variant,
+            isStatus: true,
+            solid: false, // Use light backgrounds with dark text (not solid colors)
+            // Success toasts auto-dismiss after 10s, others stay until closed
+            interval: variant === 'success' ? 10000 : 0,
+            // Note: Progress bar hidden via CSS in _toasts.scss (JS props to hide progress bar don't work as documented in Bootstrap Vue Next 0.40.8)
+          },
+        });
+      } else {
+        // Fallback: log to console
+        /* eslint-disable no-console */
+        console[variant === 'danger' ? 'error' : 'log'](
+          `[toast:${variant}]`,
+          bodyText,
+        );
+        /* eslint-enable no-console */
+      }
     },
     successToast(
       message,
@@ -65,7 +93,10 @@
       const body = this.$_BVToastMixin_createBody(message);
       const title = this.$_BVToastMixin_createTitle(t, 'success');
       if (refreshAction) body.push(this.$_BVToastMixin_createRefreshAction());
-      if (timestamp) body.push(this.$_BVToastMixin_createTimestamp());
+      if (timestamp) {
+        body.push(' '); // Extra newline for spacing above timestamp
+        body.push(this.$_BVToastMixin_createTimestamp());
+      }
       this.$_BVToastMixin_initToast(body, title, 'success');
     },
     errorToast(
@@ -79,7 +110,10 @@
       const body = this.$_BVToastMixin_createBody(message);
       const title = this.$_BVToastMixin_createTitle(t, 'danger');
       if (refreshAction) body.push(this.$_BVToastMixin_createRefreshAction());
-      if (timestamp) body.push(this.$_BVToastMixin_createTimestamp());
+      if (timestamp) {
+        body.push(' '); // Extra newline for spacing above timestamp
+        body.push(this.$_BVToastMixin_createTimestamp());
+      }
       this.$_BVToastMixin_initToast(body, title, 'danger');
     },
     warningToast(
@@ -93,7 +127,10 @@
       const body = this.$_BVToastMixin_createBody(message);
       const title = this.$_BVToastMixin_createTitle(t, 'warning');
       if (refreshAction) body.push(this.$_BVToastMixin_createRefreshAction());
-      if (timestamp) body.push(this.$_BVToastMixin_createTimestamp());
+      if (timestamp) {
+        body.push(' '); // Extra newline for spacing above timestamp
+        body.push(this.$_BVToastMixin_createTimestamp());
+      }
       this.$_BVToastMixin_initToast(body, title, 'warning');
     },
     infoToast(
@@ -107,7 +144,10 @@
       const body = this.$_BVToastMixin_createBody(message);
       const title = this.$_BVToastMixin_createTitle(t, 'info');
       if (refreshAction) body.push(this.$_BVToastMixin_createRefreshAction());
-      if (timestamp) body.push(this.$_BVToastMixin_createTimestamp());
+      if (timestamp) {
+        body.push(' '); // Extra newline for spacing above timestamp
+        body.push(this.$_BVToastMixin_createTimestamp());
+      }
       this.$_BVToastMixin_initToast(body, title, 'info');
     },
   },
diff --git a/src/components/Mixins/LoadingBarMixin.js b/src/components/Mixins/LoadingBarMixin.js
index d115270..b1adc78 100644
--- a/src/components/Mixins/LoadingBarMixin.js
+++ b/src/components/Mixins/LoadingBarMixin.js
@@ -3,15 +3,15 @@
 const LoadingBarMixin = {
   methods: {
     startLoader() {
-      this.$root.$emit('loader-start');
+      require('@/eventBus').default.$emit('loader-start');
       this.loading = true;
     },
     endLoader() {
-      this.$root.$emit('loader-end');
+      require('@/eventBus').default.$emit('loader-end');
       this.loading = false;
     },
     hideLoader() {
-      this.$root.$emit('loader-hide');
+      require('@/eventBus').default.$emit('loader-hide');
     },
   },
 };
diff --git a/src/components/Mixins/TableRowExpandMixin.js b/src/components/Mixins/TableRowExpandMixin.js
index 0450877..5f56968 100644
--- a/src/components/Mixins/TableRowExpandMixin.js
+++ b/src/components/Mixins/TableRowExpandMixin.js
@@ -5,11 +5,10 @@
   methods: {
     toggleRowDetails(row) {
       row.toggleDetails();
-      row.detailsShowing
-        ? (this.expandRowLabel = i18n.global.t('global.table.expandTableRow'))
-        : (this.expandRowLabel = i18n.global.t(
-            'global.table.collapseTableRow',
-          ));
+      // When details are shown, label should instruct to collapse; otherwise, expand
+      this.expandRowLabel = row.detailsShowing
+        ? i18n.global.t('global.table.collapseTableRow')
+        : i18n.global.t('global.table.expandTableRow');
     },
   },
 };
diff --git a/src/components/Mixins/VuelidateMixin.js b/src/components/Mixins/VuelidateMixin.js
index fec8525..8274df6 100644
--- a/src/components/Mixins/VuelidateMixin.js
+++ b/src/components/Mixins/VuelidateMixin.js
@@ -1,6 +1,7 @@
 const VuelidateMixin = {
   methods: {
     getValidationState(model) {
+      if (!model) return null;
       const { $dirty, $error } = model;
       return $dirty ? !$error : null;
     },