Page MenuHomePhorge

D2734.1775175243.diff
No OneTemporary

Authored By
Unknown
Size
154 KB
Referenced Files
None
Subscribers
None

D2734.1775175243.diff

diff --git a/src/package-lock.json b/src/package-lock.json
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -3550,6 +3550,12 @@
"fastq": "^1.6.0"
}
},
+ "@popperjs/core": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz",
+ "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==",
+ "dev": true
+ },
"@stylelint/postcss-css-in-js": {
"version": "0.37.2",
"resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz",
@@ -4555,9 +4561,9 @@
"dev": true
},
"bootstrap": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
- "integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==",
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.0.2.tgz",
+ "integrity": "sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q==",
"dev": true
},
"brace-expansion": {
@@ -4789,6 +4795,12 @@
"integrity": "sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==",
"dev": true
},
+ "cash-dom": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/cash-dom/-/cash-dom-8.1.0.tgz",
+ "integrity": "sha512-QTa50rFuPaX8klEDEbwLr+jVutwpvZEBQ0NpMMyng+je7gNe9Bz/JsOLHIG24tvNSSSIN/Q1QD0bnF6PQzWKHA==",
+ "dev": true
+ },
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
@@ -7662,12 +7674,6 @@
}
}
},
- "jquery": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
- "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
- "dev": true
- },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -9280,12 +9286,6 @@
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
"dev": true
},
- "popper.js": {
- "version": "1.16.1",
- "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
- "integrity": "sha1-KiI8s9x7YhPXQOQDcr5A3kPmWxs=",
- "dev": true
- },
"portfinder": {
"version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
diff --git a/src/package.json b/src/package.json
--- a/src/package.json
+++ b/src/package.json
@@ -17,17 +17,17 @@
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^0.1.10",
+ "@popperjs/core": "^2.9.2",
"anchorme": "^2.1.2",
"axios": "^0.21.1",
- "bootstrap": "^4.6.0",
+ "bootstrap": "^5.0.0",
+ "cash-dom": "^8.1.0",
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
"eslint-plugin-vue": "^7.9.0",
"frappe-charts": "^1.5.8",
- "jquery": "^3.6.0",
"laravel-mix": "^6.0.27",
"openvidu-browser": "^2.18.0",
- "popper.js": "^1.16.0",
"postcss": "^8.3.6",
"resolve-url-loader": "^4.0.0",
"sass": "^1.32.8",
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -10,9 +10,10 @@
import MenuComponent from '../vue/Widgets/Menu'
import SupportForm from '../vue/Widgets/SupportForm'
import store from './store'
+import { Tab } from 'bootstrap'
import { loadLangAsync, i18n } from './locale'
-const loader = '<div class="app-loader"><div class="spinner-border" role="status"><span class="sr-only">Loading</span></div></div>'
+const loader = '<div class="app-loader"><div class="spinner-border" role="status"><span class="visually-hidden">Loading</span></div></div>'
let isLoading = 0
@@ -177,8 +178,14 @@
return `<img src="${src}" alt="${this.appName}">`
},
// Display "loading" overlay inside of the specified element
- addLoader(elem, small = true) {
- $(elem).css({position: 'relative'}).append(small ? $(loader).addClass('small') : $(loader))
+ addLoader(elem, small = true, style = null) {
+ if (style) {
+ $(elem).css(style)
+ } else {
+ $(elem).css('position', 'relative')
+ }
+
+ $(elem).append(small ? $(loader).addClass('small') : $(loader))
},
// Remove loader element added in addLoader()
removeLoader(elem) {
@@ -191,7 +198,7 @@
},
tab(e) {
e.preventDefault()
- $(e.target).tab('show')
+ new Tab(e.target).show()
},
errorPage(code, msg, hint) {
// Until https://github.com/vuejs/vue-router/issues/977 is implemented
@@ -267,10 +274,7 @@
},
clickRecord(event) {
if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) {
- let link = $(event.target).closest('tr').find('a')[0]
- if (link) {
- link.click()
- }
+ $(event.target).closest('tr').find('a').trigger('click')
}
},
domainStatusClass(domain) {
@@ -350,21 +354,19 @@
return page ? page : '404'
},
supportDialog(container) {
- let dialog = $('#support-dialog')
+ let dialog = $('#support-dialog')[0]
- // FIXME: Find a nicer way of doing this
- if (!dialog.length) {
+ if (!dialog) {
+ // FIXME: Find a nicer way of doing this
SupportForm.i18n = i18n
let form = new Vue(SupportForm)
form.$mount($('<div>').appendTo(container)[0])
form.$root = this
form.$toast = this.$toast
- dialog = $(form.$el)
+ dialog = form.$el
}
- dialog.on('shown.bs.modal', () => {
- dialog.find('input').first().focus()
- }).modal()
+ dialog.__vue__.showDialog()
},
userStatusClass(user) {
if (user.isDeleted) {
@@ -464,7 +466,7 @@
// Create an error message
// API responses can use a string, array or object
let msg_text = ''
- if ($.type(msg) !== 'string') {
+ if (typeof(msg) !== 'string') {
$.each(msg, (index, str) => {
msg_text += str + ' '
})
diff --git a/src/resources/js/bootstrap.js b/src/resources/js/bootstrap.js
--- a/src/resources/js/bootstrap.js
+++ b/src/resources/js/bootstrap.js
@@ -1,18 +1,22 @@
/**
- * We'll load jQuery and the Bootstrap jQuery plugin which provides support
- * for JavaScript based Bootstrap features such as modals and tabs. This
- * code may be modified to fit the specific needs of your application.
+ * Import Cash (jQuery replacement)
*/
-window.Popper = require('popper.js').default
-window.$ = window.jQuery = require('jquery')
+import $ from 'cash-dom'
+window.$ = $
-require('bootstrap')
+$.fn.focus = function() {
+ if (this.length && this[0].focus) {
+ this[0].focus()
+ }
+ return this
+}
/**
- * We'll load Vue, VueRouter and global components
+ * Load Vue, VueRouter and global components
*/
+import { Tooltip } from 'bootstrap'
import FontAwesomeIcon from './fontawesome'
import Vue from 'vue'
import VueRouter from 'vue-router'
@@ -24,26 +28,26 @@
Vue.component('SvgIcon', FontAwesomeIcon)
const vTooltip = (el, binding) => {
- const t = []
+ let t = []
if (binding.modifiers.focus) t.push('focus')
if (binding.modifiers.hover) t.push('hover')
if (binding.modifiers.click) t.push('click')
- if (!t.length) t.push('hover')
+ if (!t.length) t.push('click')
- $(el).tooltip({
+ el.tooltip = new Tooltip(el, {
title: binding.value,
placement: binding.arg || 'top',
trigger: t.join(' '),
- html: !!binding.modifiers.html,
- });
+ html: !!binding.modifiers.html
+ })
}
Vue.directive('tooltip', {
bind: vTooltip,
update: vTooltip,
unbind (el) {
- $(el).tooltip('dispose')
+ el.tooltip.dispose()
}
})
@@ -70,7 +74,7 @@
})
/**
- * We'll load the axios HTTP library which allows us to easily issue requests
+ * Load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js
--- a/src/resources/js/meet/app.js
+++ b/src/resources/js/meet/app.js
@@ -1,4 +1,5 @@
import anchorme from 'anchorme'
+import { Dropdown } from 'bootstrap'
import { library } from '@fortawesome/fontawesome-svg-core'
import { OpenVidu } from 'openvidu-browser'
@@ -1033,7 +1034,7 @@
}
if (params.isSelf) {
- wrapper.find('.link-setup').removeClass('hidden').click(() => sessionData.onMediaSetup())
+ wrapper.find('.link-setup').removeClass('hidden').on('click', () => sessionData.onMediaSetup())
} else {
let volumeInput = wrapper.find('.volume input')
let audioButton = wrapper.find('.link-audio')
@@ -1284,19 +1285,19 @@
+ '<div class="dropdown-divider permissions"></div>'
+ '<div class="permissions">'
+ '<h6 class="dropdown-header">' + $t('meet.perm') + '</h6>'
- + '<label class="dropdown-item action-role-publisher custom-control custom-switch">'
- + '<input type="checkbox" class="custom-control-input">'
- + ' <span class="custom-control-label">' + $t('meet.perm-av') + '</span>'
+ + '<label class="dropdown-item action-role-publisher form-check form-switch">'
+ + '<input type="checkbox" class="form-check-input">'
+ + ' <span class="form-check-label">' + $t('meet.perm-av') + '</span>'
+ '</label>'
- + '<label class="dropdown-item action-role-moderator custom-control custom-switch">'
- + '<input type="checkbox" class="custom-control-input">'
- + ' <span class="custom-control-label">' + $t('meet.perm-mod') + '</span>'
+ + '<label class="dropdown-item action-role-moderator form-check form-switch">'
+ + '<input type="checkbox" class="form-check-input">'
+ + ' <span class="form-check-label">' + $t('meet.perm-mod') + '</span>'
+ '</label>'
+ '</div>'
+ '<div class="dropdown-divider interpreting"></div>'
+ '<div class="interpreting">'
+ '<h6 class="dropdown-header">' + $t('meet.lang-int') + '</h6>'
- + '<div class="ml-4 mr-4"><select class="custom-select">'
+ + '<div class="ps-3 pe-3"><select class="form-select">'
+ '<option value="">- ' + $t('form.none') + ' -</option>'
+ languages.join('')
+ '</select></div>'
@@ -1307,8 +1308,9 @@
let nickname = element.find('.meet-nickname')
.addClass('btn btn-outline-' + (params.isSelf ? 'primary' : 'secondary'))
- .attr({title: $t('meet.menu-options'), 'data-toggle': 'dropdown'})
- .dropdown({boundary: container.parentNode})
+ .attr({title: $t('meet.menu-options'), 'data-bs-toggle': 'dropdown'})
+
+ const dropdown = new Dropdown(nickname[0], {boundary: container.parentNode})
if (params.isSelf) {
// Add events for nickname change
@@ -1393,7 +1395,7 @@
.on('change', e => {
const language = $(e.target).val()
sessionData.onConnectionChange(params.connectionId, { language })
- element.find('.meet-nickname').dropdown('hide')
+ dropdown.hide()
})
.on('click', e => {
// Prevents from closing the dropdown menu on click
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -336,6 +336,7 @@
'create' => "Create user",
'custno' => "Customer No.",
'delete' => "Delete user",
+ 'delete-account' => "Delete this account?",
'delete-email' => "Delete {email}",
'delete-text' => "Do you really want to delete this user permanently?"
. " This will delete all account data and withdraw the permission to access the email account."
diff --git a/src/resources/themes/bootstrap.scss b/src/resources/themes/bootstrap.scss
--- a/src/resources/themes/bootstrap.scss
+++ b/src/resources/themes/bootstrap.scss
@@ -1,15 +1,13 @@
-// Bootstrap
@import '~bootstrap/scss/bootstrap';
-// Bootstrap style fixes
+// Bootstrap style fixes and additions
-.btn-link,
-.table thead th {
- border: 0;
+a {
+ text-decoration: none;
}
-small {
- font-size: 0.875em;
+.table > :not(:last-child) > :last-child > * {
+ border-bottom-color: #ccc;
}
.hidden {
diff --git a/src/resources/themes/default/lang/en/faq.php b/src/resources/themes/default/lang/en/faq.php
--- a/src/resources/themes/default/lang/en/faq.php
+++ b/src/resources/themes/default/lang/en/faq.php
@@ -5,5 +5,6 @@
'account-upgrade' => "Can I upgrade an individual account to a group account?",
'storage' => "How much storage comes with my account?",
'tos' => "What are your terms of service?",
+ 'support' => "Need support?",
];
diff --git a/src/resources/themes/default/theme.json b/src/resources/themes/default/theme.json
--- a/src/resources/themes/default/theme.json
+++ b/src/resources/themes/default/theme.json
@@ -39,6 +39,10 @@
{
"href": "https://kb.kolabnow.com/faq/how-much-storage-comes-with-my-account",
"label": "storage"
+ },
+ {
+ "href": "/support",
+ "label": "support"
}
]
}
diff --git a/src/resources/themes/forms.scss b/src/resources/themes/forms.scss
--- a/src/resources/themes/forms.scss
+++ b/src/resources/themes/forms.scss
@@ -33,12 +33,15 @@
label {
margin-right: 0.5em;
+ min-width: 4em;
+ text-align: right;
+ line-height: 1.7;
}
}
.input-group-activable {
&.active {
- :not(.input-group-append):not(.activable) {
+ :not(.activable) {
display: none;
}
}
@@ -70,19 +73,32 @@
margin-top: -0.25rem;
}
-form.read-only {
- .row {
- margin-bottom: 0;
- }
-}
-
// Various improvements for mobile
@include media-breakpoint-down(sm) {
- .form-group {
- margin-bottom: 0.5rem;
+ .row.mb-3 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .nav-tabs {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+
+ .nav-link {
+ white-space: nowrap;
+ padding: 0.5rem 0.75rem;
+ }
+ }
+
+ .tab-content {
+ margin-top: 0.5rem;
+ }
+
+ .col-form-label {
+ color: #666;
+ font-size: 95%;
}
- .form-group.plaintext .col-form-label {
+ .row.plaintext .col-form-label {
padding-bottom: 0;
}
@@ -93,19 +109,11 @@
width: 65%;
}
}
-}
-
-@include media-breakpoint-down(xs) {
- .col-form-label {
- color: #666;
- font-size: 95%;
- }
- .form-group.checkbox {
+ .row.checkbox {
position: relative;
& > div {
- position: initial;
padding-top: 0 !important;
input {
diff --git a/src/resources/themes/meet.scss b/src/resources/themes/meet.scss
--- a/src/resources/themes/meet.scss
+++ b/src/resources/themes/meet.scss
@@ -54,8 +54,7 @@
& + .dropdown-menu {
.permissions > label {
- margin: 0;
- padding-left: 3.75rem;
+ padding-left: 3.5rem;
}
}
}
@@ -514,7 +513,7 @@
}
}
-@include media-breakpoint-down(xs) {
+@include media-breakpoint-down(sm) {
#meet-session-menu {
white-space: nowrap;
margin: 0;
diff --git a/src/resources/themes/menu.scss b/src/resources/themes/menu.scss
--- a/src/resources/themes/menu.scss
+++ b/src/resources/themes/menu.scss
@@ -3,13 +3,10 @@
padding: 0;
line-height: 85px;
- .navbar {
- padding-right: 0;
- }
-
.navbar-brand {
- padding: 0;
+ padding: 0 0.5rem;
outline: 0;
+ margin: 0;
> img {
display: inline;
@@ -20,7 +17,7 @@
.nav-link {
color: #202020;
line-height: 85px;
- padding: 0 0 0 25px;
+ padding: 0 0.5rem 0 0.5rem;
background: transparent;
&:focus {
@@ -46,12 +43,18 @@
overflow: hidden;
padding: 0;
- .navbar {
- padding-right: 0;
+ .container {
+ flex-wrap: nowrap;
+ }
+
+ .navbar-nav {
+ display: flex;
+ flex-direction: row;
}
.navbar-brand {
margin: 0;
+ padding: 0 0.5rem;
img {
width: 170px;
@@ -69,7 +72,6 @@
margin: 2em 0;
.dropdown-toggle {
- padding-left: 20px;
line-height: 30px;
font-weight: lighter;
}
@@ -88,7 +90,7 @@
line-height: 21px;
letter-spacing: 1px;
padding: 6px 34px;
- margin: 25px 0 25px 25px;
+ margin: 25px 0.5rem 25px 0.5rem;
&:focus,
&:hover {
@@ -99,25 +101,18 @@
}
}
}
-
- .navbar {
- .navbar {
- justify-content: flex-end;
- }
- }
-
- #footer-menu {
- .navbar {
- flex-direction: column;
- align-items: flex-end;
- }
- }
}
-@include media-breakpoint-down(md) {
+@include media-breakpoint-down(lg) {
#header-menu {
.navbar-nav {
- padding-bottom: 1em;
+ display: block;
+ width: 100%;
+ padding: 0;
+
+ li {
+ border-top: 1px solid #eee;
+ }
}
.nav-link {
@@ -126,13 +121,15 @@
}
}
+ #language-selector {
+ margin: 0;
+ }
+}
+
+@include media-breakpoint-down(md) {
#footer-menu {
height: 80px;
- .navbar {
- padding-left: 0;
- }
-
.navbar-nav {
display: none;
}
@@ -141,29 +138,11 @@
flex-wrap: nowrap;
}
}
-
- #language-selector {
- margin: 0;
-
- .dropdown-toggle:after {
- display: none;
- }
- }
}
@include media-breakpoint-down(sm) {
#header-menu {
padding: 0 1em;
-
- .navbar-nav {
- display: block;
- width: 100%;
- padding: 0;
-
- li {
- border-top: 1px solid #eee;
- }
- }
}
#footer-menu {
@@ -171,6 +150,10 @@
flex-direction: column;
}
+ .navbar-brand {
+ padding: 0.5rem;
+ }
+
#footer-company {
display: none;
}
diff --git a/src/resources/themes/toast.scss b/src/resources/themes/toast.scss
--- a/src/resources/themes/toast.scss
+++ b/src/resources/themes/toast.scss
@@ -8,7 +8,7 @@
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(52, 58, 64, 0.95) transparent;
- z-index: 1055; // above Bootstrap's modal backdrop and dialogs
+ z-index: 1065; // above Bootstrap's modal backdrop and dialogs
@media (max-width: 375px) {
left: 0;
@@ -42,15 +42,9 @@
margin-right: 0.5rem;
}
- button.close {
- color: #eee;
- opacity: 1 !important;
- text-shadow: none;
- font-size: 1.2rem;
-
- &:hover {
- color: #fff;
- }
+ .btn-close {
+ font-size: 0.8em;
+ cursor: pointer;
}
}
diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue
--- a/src/resources/vue/Admin/Distlist.vue
+++ b/src/resources/vue/Admin/Distlist.vue
@@ -5,7 +5,7 @@
<div class="card-title">{{ list.email }}</div>
<div class="card-text">
<form class="read-only short">
- <div class="form-group row">
+ <div class="row plaintext">
<label for="distlistid" class="col-sm-4 col-form-label">
{{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
</label>
@@ -15,13 +15,13 @@
</span>
</div>
</div>
- <div class="form-group row">
+ <div class="row plaintext">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span :class="$root.distlistStatusClass(list) + ' form-control-plaintext'" id="status">{{ $root.distlistStatusText(list) }}</span>
</div>
</div>
- <div class="form-group row">
+ <div class="row plaintext">
<label for="members-input" class="col-sm-4 col-form-label">{{ $t('distlist.recipients') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="members">
diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue
--- a/src/resources/vue/Admin/Domain.vue
+++ b/src/resources/vue/Admin/Domain.vue
@@ -5,7 +5,7 @@
<div class="card-title">{{ domain.namespace }}</div>
<div class="card-text">
<form class="read-only short">
- <div class="form-group row">
+ <div class="row plaintext">
<label for="domainid" class="col-sm-4 col-form-label">
{{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
</label>
@@ -15,7 +15,7 @@
</span>
</div>
</div>
- <div class="form-group row">
+ <div class="row plaintext">
<label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="status">
@@ -62,7 +62,7 @@
<div class="card-body">
<div class="card-text">
<form class="read-only short">
- <div class="form-group row plaintext">
+ <div class="row plaintext">
<label for="spf_whitelist" class="col-sm-4 col-form-label">{{ $t('domain.spf-whitelist') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="spf_whitelist">
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -5,7 +5,7 @@
<h1 class="card-title">{{ user.email }}</h1>
<div class="card-text">
<form class="read-only short">
- <div v-if="user.wallet.user_id != user.id" class="form-group row plaintext">
+ <div v-if="user.wallet.user_id != user.id" class="row plaintext">
<label for="manager" class="col-sm-4 col-form-label">{{ $t('user.managed-by') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="manager">
@@ -13,7 +13,7 @@
</span>
</div>
</div>
- <div class="form-group row plaintext">
+ <div class="row plaintext">
<label for="userid" class="col-sm-4 col-form-label">ID <span class="text-muted">({{ $t('form.created') }})</span></label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="userid">
@@ -21,7 +21,7 @@
</span>
</div>
</div>
- <div class="form-group row plaintext">
+ <div class="row plaintext">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="status">
@@ -29,31 +29,31 @@
</span>
</div>
</div>
- <div class="form-group row plaintext" v-if="user.first_name">
+ <div class="row plaintext" v-if="user.first_name">
<label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.firstname') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="first_name">{{ user.first_name }}</span>
</div>
</div>
- <div class="form-group row plaintext" v-if="user.last_name">
+ <div class="row plaintext" v-if="user.last_name">
<label for="last_name" class="col-sm-4 col-form-label">{{ $t('form.lastname') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="last_name">{{ user.last_name }}</span>
</div>
</div>
- <div class="form-group row plaintext" v-if="user.organization">
+ <div class="row plaintext" v-if="user.organization">
<label for="organization" class="col-sm-4 col-form-label">{{ $t('user.org') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="organization">{{ user.organization }}</span>
</div>
</div>
- <div class="form-group row plaintext" v-if="user.phone">
+ <div class="row plaintext" v-if="user.phone">
<label for="phone" class="col-sm-4 col-form-label">{{ $t('form.phone') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="phone">{{ user.phone }}</span>
</div>
</div>
- <div class="form-group row plaintext">
+ <div class="row plaintext">
<label for="external_email" class="col-sm-4 col-form-label">{{ $t('user.ext-email') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="external_email">
@@ -62,13 +62,13 @@
</span>
</div>
</div>
- <div class="form-group row plaintext" v-if="user.billing_address">
+ <div class="row plaintext" v-if="user.billing_address">
<label for="billing_address" class="col-sm-4 col-form-label">{{ $t('user.address') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" style="white-space:pre" id="billing_address">{{ user.billing_address }}</span>
</div>
</div>
- <div class="form-group row plaintext">
+ <div class="row plaintext">
<label for="country" class="col-sm-4 col-form-label">{{ $t('user.country') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="country">{{ user.country }}</span>
@@ -132,7 +132,7 @@
</h2>
<div class="card-text">
<form class="read-only short">
- <div class="form-group row">
+ <div class="row">
<label class="col-sm-4 col-form-label">{{ $t('user.discount') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="discount">
@@ -141,7 +141,7 @@
</span>
</div>
</div>
- <div class="form-group row" v-if="wallet.mandate && wallet.mandate.id">
+ <div class="row" v-if="wallet.mandate && wallet.mandate.id">
<label class="col-sm-4 col-form-label">{{ $t('user.auto-payment') }}</label>
<div class="col-sm-8">
<span id="autopayment" :class="'form-control-plaintext' + (wallet.mandateState ? ' text-danger' : '')"
@@ -155,7 +155,7 @@
</span>
</div>
</div>
- <div class="form-group row" v-if="wallet.providerLink">
+ <div class="row" v-if="wallet.providerLink">
<label class="col-sm-4 col-form-label">{{ capitalize(wallet.provider) }} {{ $t('form.id') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" v-html="wallet.providerLink"></span>
@@ -174,8 +174,8 @@
<div class="tab-pane" id="user-aliases" role="tabpanel" aria-labelledby="tab-aliases">
<div class="card-body">
<div class="card-text">
- <table class="table table-sm table-hover">
- <thead class="thead-light">
+ <table class="table table-sm table-hover mb-0">
+ <thead>
<tr>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
@@ -198,7 +198,7 @@
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
- <thead class="thead-light">
+ <thead>
<tr>
<th scope="col">{{ $t('user.subscription') }}</th>
<th scope="col">{{ $t('user.price') }}</th>
@@ -231,8 +231,8 @@
<div class="tab-pane" id="user-domains" role="tabpanel" aria-labelledby="tab-domains">
<div class="card-body">
<div class="card-text">
- <table class="table table-sm table-hover">
- <thead class="thead-light">
+ <table class="table table-sm table-hover mb-0">
+ <thead>
<tr>
<th scope="col">{{ $t('domain.namespace') }}</th>
</tr>
@@ -257,8 +257,8 @@
<div class="tab-pane" id="user-users" role="tabpanel" aria-labelledby="tab-users">
<div class="card-body">
<div class="card-text">
- <table class="table table-sm table-hover">
- <thead class="thead-light">
+ <table class="table table-sm table-hover mb-0">
+ <thead>
<tr>
<th scope="col">{{ $t('form.primary-email') }}</th>
</tr>
@@ -284,8 +284,8 @@
<div class="tab-pane" id="user-distlists" role="tabpanel" aria-labelledby="tab-distlists">
<div class="card-body">
<div class="card-text">
- <table class="table table-sm table-hover">
- <thead class="thead-light">
+ <table class="table table-sm table-hover mb-0">
+ <thead>
<tr>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
@@ -311,7 +311,7 @@
<div class="card-body">
<div class="card-text">
<form class="read-only short">
- <div class="form-group row plaintext">
+ <div class="row plaintext">
<label for="greylisting" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="greylisting">
@@ -331,20 +331,18 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.discount-title') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
- <p class="form-group">
- <select v-model="wallet.discount_id" class="custom-select">
+ <p>
+ <select v-model="wallet.discount_id" class="form-select">
<option value="">- {{ $t('form.none') }} -</option>
<option v-for="item in discounts" :value="item.id" :key="item.id">{{ item.label }}</option>
</select>
</p>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitDiscount()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
@@ -358,17 +356,15 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.ext-email') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
- <p class="form-group">
+ <p>
<input v-model="external_email" name="external_email" class="form-control">
</p>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitEmail()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
@@ -382,29 +378,25 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t(oneoff_negative ? 'user.add-penalty-title' : 'user.add-bonus-title') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<form data-validation-prefix="oneoff_">
- <div class="form-group">
+ <div class="row mb-3">
<label for="oneoff_amount" class="col-form-label">{{ $t('form.amount') }}</label>
<div class="input-group">
<input type="text" class="form-control" id="oneoff_amount" v-model="oneoff_amount" required>
- <span class="input-group-append">
- <span class="input-group-text">{{ wallet.currency }}</span>
- </span>
+ <span class="input-group-text">{{ wallet.currency }}</span>
</div>
</div>
- <div class="form-group">
+ <div class="row">
<label for="oneoff_description" class="col-form-label">{{ $t('form.description') }}</label>
<input class="form-control" id="oneoff_description" v-model="oneoff_description" required>
</div>
</form>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitOneOff()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
@@ -418,16 +410,14 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.reset-2fa-title') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>{{ $t('user.2fa-hint1') }}</p>
<p>{{ $t('user.2fa-hint2') }}</p>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-danger modal-action" @click="reset2FA()">{{ $t('btn.reset') }}</button>
</div>
</div>
@@ -437,6 +427,7 @@
</template>
<script>
+ import { Modal } from 'bootstrap'
import TransactionLog from '../Widgets/TransactionLog'
export default {
@@ -575,13 +566,19 @@
this.oneOffDialog(false)
},
discountEdit() {
- $('#discount-dialog')
- .on('shown.bs.modal', e => {
- $(e.target).find('select').focus()
+ if (!this.discount_dialog) {
+ const dialog = $('#discount-dialog')[0]
+
+ dialog.addEventListener('shown.bs.modal', e => {
+ $(dialog).find('select').focus()
// Note: Vue v-model is strict, convert null to a string
this.wallet.discount_id = this.wallet_discount_id || ''
})
- .modal()
+
+ this.discount_dialog = new Modal(dialog)
+ }
+
+ this.discount_dialog.show()
if (!this.discounts.length) {
// Fetch discounts
@@ -595,11 +592,17 @@
this.external_email = this.user.external_email
this.$root.clearFormValidation($('#email-dialog'))
- $('#email-dialog')
- .on('shown.bs.modal', e => {
- $(e.target).find('input').focus()
+ if (!this.email_dialog) {
+ const dialog = $('#email-dialog')[0]
+
+ dialog.addEventListener('shown.bs.modal', e => {
+ $(dialog).find('input').focus()
})
- .modal()
+
+ this.email_dialog = new Modal(dialog)
+ }
+
+ this.email_dialog.show()
},
setMandateState() {
let mandate = this.wallet.mandate
@@ -613,10 +616,19 @@
},
oneOffDialog(negative) {
this.oneoff_negative = negative
- this.dialog = $('#oneoff-dialog').on('shown.bs.modal', event => {
- this.$root.clearFormValidation(event.target)
- $(event.target).find('#oneoff_amount').focus()
- }).modal()
+
+ if (!this.oneoff_dialog) {
+ const dialog = $('#oneoff-dialog')[0]
+
+ dialog.addEventListener('shown.bs.modal', () => {
+ this.$root.clearFormValidation(dialog)
+ $(dialog).find('#oneoff_amount').focus()
+ })
+
+ this.oneoff_dialog = new Modal(dialog)
+ }
+
+ this.oneoff_dialog.show()
},
penalizeDialog() {
this.oneOffDialog(true)
@@ -627,7 +639,7 @@
this.$nextTick(() => { this.walletReload = false })
},
reset2FA() {
- $('#reset-2fa-dialog').modal('hide')
+ new Modal('#reset-2fa-dialog').hide()
axios.post('/api/v4/users/' + this.user.id + '/reset2FA')
.then(response => {
if (response.data.status == 'success') {
@@ -638,10 +650,10 @@
})
},
reset2FADialog() {
- $('#reset-2fa-dialog').modal()
+ new Modal('#reset-2fa-dialog').show()
},
submitDiscount() {
- $('#discount-dialog').modal('hide')
+ this.discount_dialog.hide()
axios.put('/api/v4/wallets/' + this.user.wallets[0].id, { discount: this.wallet.discount_id })
.then(response => {
@@ -665,7 +677,7 @@
axios.put('/api/v4/users/' + this.user.id, { external_email: this.external_email })
.then(response => {
if (response.data.status == 'success') {
- $('#email-dialog').modal('hide')
+ this.email_dialog.hide()
this.$toast.success(response.data.message)
this.user.external_email = this.external_email
this.external_email = null // required because of Vue
@@ -683,12 +695,12 @@
post.amount *= -1
}
- this.$root.clearFormValidation(this.dialog)
+ this.$root.clearFormValidation('#oneoff-dialog')
axios.post('/api/v4/wallets/' + wallet_id + '/one-off', post)
.then(response => {
if (response.data.status == 'success') {
- this.dialog.modal('hide')
+ this.oneoff_dialog.hide()
this.$toast.success(response.data.message)
this.wallet = Object.assign({}, this.wallet, {balance: response.data.balance})
this.oneoff_amount = ''
diff --git a/src/resources/vue/App.vue b/src/resources/vue/App.vue
--- a/src/resources/vue/App.vue
+++ b/src/resources/vue/App.vue
@@ -77,21 +77,24 @@
$('#faq').remove()
if (result && result.length) {
let faq = $('<div id="faq" class="faq mt-3"><h5>' + this.$t('app.faq') + '</h5><ul class="pl-4"></ul></div>')
- let list = []
+ let list = $([])
result.forEach(item => {
- list.push($('<li>').append($('<a>').attr('href', item.href).text(item.title)))
+ let li = $('<li>').append($('<a>').attr('href', item.href).text(item.title))
// Handle internal links with the vue-router
if (item.href.charAt(0) == '/') {
- list[list.length-1].find('a').on('click', event => {
+ li.find('a').on('click', event => {
event.preventDefault()
this.$router.push(item.href)
})
}
+
+ list = list.add(li)
})
faq.find('ul').append(list)
+
$(this.$el).append(faq)
}
})
diff --git a/src/resources/vue/Distlist/Info.vue b/src/resources/vue/Distlist/Info.vue
--- a/src/resources/vue/Distlist/Info.vue
+++ b/src/resources/vue/Distlist/Info.vue
@@ -6,26 +6,26 @@
<div class="card-body">
<div class="card-title" v-if="list_id !== 'new'">
{{ $tc('distlist.list-title', 1) }}
- <button class="btn btn-outline-danger button-delete float-right" @click="deleteList()" tag="button">
+ <button class="btn btn-outline-danger button-delete float-end" @click="deleteList()" tag="button">
<svg-icon icon="trash-alt"></svg-icon> {{ $t('distlist.delete') }}
</button>
</div>
<div class="card-title" v-if="list_id === 'new'">{{ $t('distlist.new') }}</div>
<div class="card-text">
<form @submit.prevent="submit">
- <div v-if="list_id !== 'new'" class="form-group row plaintext">
+ <div v-if="list_id !== 'new'" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span :class="$root.distlistStatusClass(list) + ' form-control-plaintext'" id="status">{{ $root.distlistStatusText(list) }}</span>
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" :disabled="list_id !== 'new'" required v-model="list.email">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="members-input" class="col-sm-4 col-form-label">{{ $t('distlist.recipients') }}</label>
<div class="col-sm-8">
<list-input id="members" :list="list.members"></list-input>
diff --git a/src/resources/vue/Distlist/List.vue b/src/resources/vue/Distlist/List.vue
--- a/src/resources/vue/Distlist/List.vue
+++ b/src/resources/vue/Distlist/List.vue
@@ -4,13 +4,13 @@
<div class="card-body">
<div class="card-title">
{{ $tc('distlist.list-title', 2) }}
- <router-link class="btn btn-success float-right create-list" :to="{ path: 'distlist/new' }" tag="button">
+ <router-link class="btn btn-success float-end create-list" :to="{ path: 'distlist/new' }" tag="button">
<svg-icon icon="users"></svg-icon> {{ $t('distlist.create') }}
</router-link>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
- <thead class="thead-light">
+ <thead>
<tr>
<th scope="col">{{ $t('distlist.email') }}</th>
</tr>
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -51,11 +51,11 @@
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<form @submit.prevent="submitSettings">
- <div class="form-group row">
+ <div class="row mb-3">
<label for="spf_whitelist" class="col-sm-4 col-form-label">{{ $t('domain.spf-whitelist') }}</label>
<div class="col-sm-8">
<list-input id="spf_whitelist" name="spf_whitelist" :list="spf_whitelist"></list-input>
- <small id="spf-hint" class="form-text text-muted">
+ <small id="spf-hint" class="text-muted d-block mt-2">
{{ $t('domain.spf-whitelist-text') }}
<span class="d-block" v-html="$t('domain.spf-whitelist-ex')"></span>
</small>
diff --git a/src/resources/vue/Domain/List.vue b/src/resources/vue/Domain/List.vue
--- a/src/resources/vue/Domain/List.vue
+++ b/src/resources/vue/Domain/List.vue
@@ -5,7 +5,7 @@
<div class="card-title">{{ $t('user.domains') }}</div>
<div class="card-text">
<table class="table table-sm table-hover">
- <thead class="thead-light">
+ <thead>
<tr>
<th scope="col">{{ $t('domain.namespace') }}</th>
<th scope="col"></th>
diff --git a/src/resources/vue/Login.vue b/src/resources/vue/Login.vue
--- a/src/resources/vue/Login.vue
+++ b/src/resources/vue/Login.vue
@@ -1,37 +1,31 @@
<template>
<div class="container d-flex flex-column align-items-center justify-content-center">
<div id="logon-form" class="card col-sm-8 col-lg-6">
- <div class="card-body">
+ <div class="card-body p-4">
<h1 class="card-title text-center mb-3">{{ $t('login.header') }}</h1>
- <div class="card-text">
+ <div class="card-text m-2 mb-0">
<form class="form-signin" @submit.prevent="submitLogin">
- <div class="form-group">
- <label for="inputEmail" class="sr-only">{{ $t('form.email') }}</label>
+ <div class="row mb-3">
+ <label for="inputEmail" class="visually-hidden">{{ $t('form.email') }}</label>
<div class="input-group">
- <span class="input-group-prepend">
- <span class="input-group-text"><svg-icon icon="user"></svg-icon></span>
- </span>
+ <span class="input-group-text"><svg-icon icon="user"></svg-icon></span>
<input type="email" id="inputEmail" class="form-control" :placeholder="$t('form.email')" required autofocus v-model="email">
</div>
</div>
- <div class="form-group">
- <label for="inputPassword" class="sr-only">{{ $t('form.password') }}</label>
+ <div class="row mb-4">
+ <label for="inputPassword" class="visually-hidden">{{ $t('form.password') }}</label>
<div class="input-group">
- <span class="input-group-prepend">
- <span class="input-group-text"><svg-icon icon="lock"></svg-icon></span>
- </span>
+ <span class="input-group-text"><svg-icon icon="lock"></svg-icon></span>
<input type="password" id="inputPassword" class="form-control" :placeholder="$t('form.password')" required v-model="password">
</div>
</div>
- <div class="form-group pt-3" v-if="$root.isUser">
- <label for="secondfactor" class="sr-only">{{ $t('login.2fa') }}</label>
+ <div class="row mb-3" v-if="$root.isUser">
+ <label for="secondfactor" class="visually-hidden">{{ $t('login.2fa') }}</label>
<div class="input-group">
- <span class="input-group-prepend">
- <span class="input-group-text"><svg-icon icon="key"></svg-icon></span>
- </span>
- <input type="text" id="secondfactor" class="form-control rounded-right" :placeholder="$t('login.2fa')" v-model="secondFactor">
+ <span class="input-group-text"><svg-icon icon="key"></svg-icon></span>
+ <input type="text" id="secondfactor" class="form-control" :placeholder="$t('login.2fa')" v-model="secondFactor">
</div>
- <small class="form-text text-muted">{{ $t('login.2fa_desc') }}</small>
+ <small class="text-muted mt-2">{{ $t('login.2fa_desc') }}</small>
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">
diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue
--- a/src/resources/vue/Meet/Room.vue
+++ b/src/resources/vue/Meet/Room.vue
@@ -17,7 +17,7 @@
<svg-icon icon="hand-paper"></svg-icon>
</button>
<span id="channel-select" :style="'display:' + (channels.length ? '' : 'none')" class="dropdown">
- <button :class="'btn link-channel' + (session.channel ? ' on' : '')" data-toggle="dropdown"
+ <button :class="'btn link-channel' + (session.channel ? ' on' : '')" data-bs-toggle="dropdown"
:title="$t('meet.menu-channel')" aria-haspopup="true" aria-expanded="false"
>
<svg-icon icon="headphones"></svg-icon>
@@ -58,33 +58,33 @@
<div class="volume"><div class="bar"></div></div>
</div>
<div class="col-sm-6 align-self-center">
- <div class="input-group">
- <label for="setup-microphone" class="input-group-prepend mb-0">
- <span class="input-group-text" :title="$t('meet.mic')"><svg-icon icon="microphone"></svg-icon></span>
+ <div class="input-group mb-2">
+ <label for="setup-microphone" class="input-group-text mb-0" :title="$t('meet.mic')">
+ <svg-icon icon="microphone"></svg-icon>
</label>
- <select class="custom-select" id="setup-microphone" v-model="microphone" @change="setupMicrophoneChange">
+ <select class="form-select" id="setup-microphone" v-model="microphone" @change="setupMicrophoneChange">
<option value="">{{ $t('form.none') }}</option>
<option v-for="mic in setup.microphones" :value="mic.deviceId" :key="mic.deviceId">{{ mic.label }}</option>
</select>
</div>
- <div class="input-group mt-2">
- <label for="setup-camera" class="input-group-prepend mb-0">
- <span class="input-group-text" :title="$t('meet.cam')"><svg-icon icon="video"></svg-icon></span>
+ <div class="input-group mb-2">
+ <label for="setup-camera" class="input-group-text mb-0" :title="$t('meet.cam')">
+ <svg-icon icon="video"></svg-icon>
</label>
- <select class="custom-select" id="setup-camera" v-model="camera" @change="setupCameraChange">
+ <select class="form-select" id="setup-camera" v-model="camera" @change="setupCameraChange">
<option value="">{{ $t('form.none') }}</option>
<option v-for="cam in setup.cameras" :value="cam.deviceId" :key="cam.deviceId">{{ cam.label }}</option>
</select>
</div>
- <div class="input-group mt-2">
- <label for="setup-nickname" class="input-group-prepend mb-0">
- <span class="input-group-text" :title="$t('meet.nick')"><svg-icon icon="user"></svg-icon></span>
+ <div class="input-group mb-2">
+ <label for="setup-nickname" class="input-group-text mb-0" :title="$t('meet.nick')">
+ <svg-icon icon="user"></svg-icon>
</label>
<input class="form-control" type="text" id="setup-nickname" v-model="nickname" :placeholder="$t('meet.nick-placeholder')">
</div>
<div class="input-group mt-2" v-if="session.config && session.config.requires_password">
- <label for="setup-password" class="input-group-prepend mb-0">
- <span class="input-group-text" :title="$t('form.password')"><svg-icon icon="key"></svg-icon></span>
+ <label for="setup-password" class="input-group-text mb-0" :title="$t('form.password')">
+ <svg-icon icon="key"></svg-icon>
</label>
<input type="password" class="form-control" id="setup-password" v-model="password" :placeholder="$t('form.password')">
</div>
@@ -126,15 +126,13 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('meet.leave-title') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>{{ $t('meet.leave-body') }}</p>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-danger modal-action" data-dismiss="modal">{{ $t('btn.close') }}</button>
+ <button type="button" class="btn btn-danger modal-action" data-bs-dismiss="modal">{{ $t('btn.close') }}</button>
</div>
</div>
</div>
@@ -145,27 +143,25 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('meet.media-title') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<form class="media-setup-form">
<div class="media-setup-preview"></div>
<div class="input-group mt-2">
- <label for="setup-mic" class="input-group-prepend mb-0">
- <span class="input-group-text" :title="$t('meet.mic')"><svg-icon icon="microphone"></svg-icon></span>
+ <label for="setup-mic" class="input-group-text mb-0" :title="$t('meet.mic')">
+ <svg-icon icon="microphone"></svg-icon>
</label>
- <select class="custom-select" id="setup-mic" v-model="microphone" @change="setupMicrophoneChange">
+ <select class="form-select" id="setup-mic" v-model="microphone" @change="setupMicrophoneChange">
<option value="">{{ $t('form.none') }}</option>
<option v-for="mic in setup.microphones" :value="mic.deviceId" :key="mic.deviceId">{{ mic.label }}</option>
</select>
</div>
<div class="input-group mt-2">
- <label for="setup-cam" class="input-group-prepend mb-0">
- <span class="input-group-text" :title="$t('meet.cam')"><svg-icon icon="video"></svg-icon></span>
+ <label for="setup-cam" class="input-group-text mb-0" :title="$t('meet.cam')">
+ <svg-icon icon="video"></svg-icon>
</label>
- <select class="custom-select" id="setup-cam" v-model="camera" @change="setupCameraChange">
+ <select class="form-select" id="setup-cam" v-model="camera" @change="setupCameraChange">
<option value="">{{ $t('form.none') }}</option>
<option v-for="cam in setup.cameras" :value="cam.deviceId" :key="cam.deviceId">{{ cam.label }}</option>
</select>
@@ -173,7 +169,7 @@
</form>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-action" data-dismiss="modal">{{ $t('btn.close') }}</button>
+ <button type="button" class="btn btn-secondary modal-action" data-bs-dismiss="modal">{{ $t('btn.close') }}</button>
</div>
</div>
</div>
@@ -184,6 +180,7 @@
</template>
<script>
+ import { Modal } from 'bootstrap'
import { Meet, Roles } from '../../js/meet/app.js'
import StatusMessage from '../Widgets/StatusMessage'
import LogonForm from '../Login'
@@ -294,6 +291,18 @@
// Setup the room UI
this.setupSession()
+
+ // Configure dialog events
+ $('#leave-dialog')[0].addEventListener('hide.bs.modal', () => {
+ // FIXME: Where exactly the user should land? Currently he'll land
+ // on dashboard (if he's logged in) or login form (if he's not).
+
+ this.$router.push({ name: 'dashboard' })
+ })
+
+ const dialog = $('#media-setup-dialog')[0]
+ dialog.addEventListener('show.bs.modal', () => { this.meet.setupStart() })
+ dialog.addEventListener('hide.bs.modal', () => { this.meet.setupStop() })
},
beforeDestroy() {
clearTimeout(roomRequest)
@@ -453,9 +462,9 @@
+ `<div class="picture"><img src="${data.picture}"></div>`
+ `<div class="content">`
+ `<p class="mb-2"></p>`
- + `<div class="text-right">`
+ + `<div class="text-end">`
+ `<button type="button" class="btn btn-sm btn-success accept">${this.$t('btn.accept')}</button>`
- + `<button type="button" class="btn btn-sm btn-danger deny ml-2">${this.$t('btn.deny')}</button>`
+ + `<button type="button" class="btn btn-sm btn-danger deny ms-2">${this.$t('btn.deny')}</button>`
)
this.$toast.message({
@@ -523,12 +532,7 @@
// TODO: Display different message for each reason: forceDisconnectByUser,
// forceDisconnectByServer, sessionClosedByServer?
if (event.reason != 'disconnect' && event.reason != 'networkDisconnect' && !this.isRoomOwner()) {
- $('#leave-dialog').on('hide.bs.modal', () => {
- // FIXME: Where exactly the user should land? Currently he'll land
- // on dashboard (if he's logged in) or login form (if he's not).
-
- this.$router.push({ name: 'dashboard' })
- }).modal()
+ new Modal('#leave-dialog').show()
}
}
this.session.onDismiss = connId => { this.dismissParticipant(connId) }
@@ -621,18 +625,16 @@
return this.reqId
},
roomOptions() {
- $('#room-options-dialog').modal()
+ new Modal('#room-options-dialog').show()
},
setupMedia() {
- let dialog = $('#media-setup-dialog')
+ const dialog = $('#media-setup-dialog')[0]
- if (!dialog.find('video').length) {
- $('#meet-setup').find('video,div.volume').appendTo(dialog.find('.media-setup-preview'))
+ if (!$('video', dialog).length) {
+ $('#meet-setup').find('video,div.volume').appendTo($('.media-setup-preview', dialog))
}
- dialog.on('show.bs.modal', () => { this.meet.setupStart() })
- .on('hide.bs.modal', () => { this.meet.setupStop() })
- .modal()
+ new Modal(dialog).show()
},
setupSession() {
this.meet.setupStart({
diff --git a/src/resources/vue/Meet/RoomOptions.vue b/src/resources/vue/Meet/RoomOptions.vue
--- a/src/resources/vue/Meet/RoomOptions.vue
+++ b/src/resources/vue/Meet/RoomOptions.vue
@@ -5,50 +5,46 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('meet.options') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<form id="room-options-password">
- <div id="password-input" class="input-group input-group-activable">
+ <div id="password-input" class="input-group input-group-activable mb-2">
<span class="input-group-text label">{{ $t('meet.password') }}:</span>
<span v-if="config.password" id="password-input-text" class="input-group-text">{{ config.password }}</span>
<span v-else id="password-input-text" class="input-group-text text-muted">{{ $t('meet.password-none') }}</span>
- <input type="text" :value="config.password" name="password" class="form-control rounded-left activable">
- <div class="input-group-append">
- <button type="button" @click="passwordSave" id="password-save-btn" class="btn btn-outline-primary activable rounded-right">{{ $t('btn.save') }}</button>
- <button type="button" v-if="config.password" id="password-clear-btn" @click="passwordClear" class="btn btn-outline-danger rounded">{{ $t('meet.password-clear') }}</button>
- <button type="button" v-else @click="passwordSet" id="password-set-btn" class="btn btn-outline-primary rounded">{{ $t('meet.password-set') }}</button>
- </div>
+ <input type="text" :value="config.password" name="password" class="form-control rounded-start activable">
+ <button type="button" @click="passwordSave" id="password-save-btn" class="btn btn-outline-primary activable rounded-end">{{ $t('btn.save') }}</button>
+ <button type="button" v-if="config.password" id="password-clear-btn" @click="passwordClear" class="btn btn-outline-danger rounded">{{ $t('meet.password-clear') }}</button>
+ <button type="button" v-else @click="passwordSet" id="password-set-btn" class="btn btn-outline-primary rounded">{{ $t('meet.password-set') }}</button>
</div>
- <small class="form-text text-muted">
+ <small class="text-muted">
{{ $t('meet.password-text') }}
</small>
</form>
<hr>
<form id="room-options-lock">
- <div id="room-lock">
+ <div id="room-lock" class="mb-2">
<label for="room-lock-input">{{ $t('meet.lock') }}:</label>
<input type="checkbox" id="room-lock-input" name="lock" value="1" :checked="config.locked" @click="lockSave">
</div>
- <small class="form-text text-muted">
+ <small class="text-muted">
{{ $t('meet.lock-text') }}
</small>
</form>
<hr>
<form id="room-options-nomedia">
- <div id="room-nomedia">
+ <div id="room-nomedia" class="mb-2">
<label for="room-nomedia-input">{{ $t('meet.nomedia') }}:</label>
<input type="checkbox" id="room-nomedia-input" name="lock" value="1" :checked="config.nomedia" @click="nomediaSave">
</div>
- <small class="form-text text-muted">
+ <small class="text-muted">
{{ $t('meet.nomedia-text') }}
</small>
</form>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-action" data-dismiss="modal">{{ $t('btn.close') }}</button>
+ <button type="button" class="btn btn-secondary modal-action" data-bs-dismiss="modal">{{ $t('btn.close') }}</button>
</div>
</div>
</div>
@@ -63,7 +59,7 @@
room: { type: String, default: () => null }
},
mounted() {
- $('#room-options-dialog').on('show.bs.modal', e => {
+ $('#room-options-dialog')[0].addEventListener('show.bs.modal', e => {
$(e.target).find('.input-group-activable.active').removeClass('active')
})
},
diff --git a/src/resources/vue/PasswordReset.vue b/src/resources/vue/PasswordReset.vue
--- a/src/resources/vue/PasswordReset.vue
+++ b/src/resources/vue/PasswordReset.vue
@@ -8,8 +8,8 @@
<span v-if="fromEmail">{{ $t('password.reset-step1-hint', { email: fromEmail }) }}</span>
</p>
<form @submit.prevent="submitStep1" data-validation-prefix="reset_">
- <div class="form-group">
- <label for="reset_email" class="sr-only">{{ $t('form.email') }}</label>
+ <div class="mb-3">
+ <label for="reset_email" class="visually-hidden">{{ $t('form.email') }}</label>
<input type="text" class="form-control" id="reset_email" :placeholder="$t('form.email')" required v-model="email">
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.continue') }}</button>
@@ -24,8 +24,8 @@
{{ $t('password.reset-step2') }}
</p>
<form @submit.prevent="submitStep2" data-validation-prefix="reset_">
- <div class="form-group">
- <label for="reset_short_code" class="sr-only">{{ $t('form.code') }}</label>
+ <div class="mb-3">
+ <label for="reset_short_code" class="visually-hidden">{{ $t('form.code') }}</label>
<input type="text" class="form-control" id="reset_short_code" :placeholder="$t('form.code')" required v-model="short_code">
</div>
<button class="btn btn-secondary" type="button" @click="stepBack">{{ $t('btn.back') }}</button>
@@ -41,12 +41,12 @@
<p class="card-text">
</p>
<form @submit.prevent="submitStep3" data-validation-prefix="reset_">
- <div class="form-group">
- <label for="reset_password" class="sr-only">{{ $t('form.password') }}</label>
+ <div class="mb-3">
+ <label for="reset_password" class="visually-hidden">{{ $t('form.password') }}</label>
<input type="password" class="form-control" id="reset_password" :placeholder="$t('form.password')" required v-model="password">
</div>
- <div class="form-group">
- <label for="reset_confirm" class="sr-only">{{ $t('form.password-confirm') }}</label>
+ <div class="mb-3">
+ <label for="reset_confirm" class="visually-hidden">{{ $t('form.password-confirm') }}</label>
<input type="password" class="form-control" id="reset_confirm" :placeholder="$t('form.password-confirm')" required v-model="password_confirmation">
</div>
<button class="btn btn-secondary" type="button" @click="stepBack">{{ $t('btn.back') }}</button>
diff --git a/src/resources/vue/Reseller/Invitations.vue b/src/resources/vue/Reseller/Invitations.vue
--- a/src/resources/vue/Reseller/Invitations.vue
+++ b/src/resources/vue/Reseller/Invitations.vue
@@ -9,19 +9,17 @@
<div class="mb-2 d-flex">
<form @submit.prevent="searchInvitations" id="search-form" class="input-group" style="flex:1">
<input class="form-control" type="text" :placeholder="$t('invitation.search')" v-model="search">
- <div class="input-group-append">
- <button type="submit" class="btn btn-primary"><svg-icon icon="search"></svg-icon> {{ $t('btn.search') }}</button>
- </div>
+ <button type="submit" class="btn btn-primary"><svg-icon icon="search"></svg-icon> {{ $t('btn.search') }}</button>
</form>
<div>
- <button class="btn btn-success create-invite ml-1" @click="inviteUserDialog">
+ <button class="btn btn-success create-invite ms-1" @click="inviteUserDialog">
<svg-icon icon="envelope-open-text"></svg-icon> {{ $t('invitation.create') }}
</button>
</div>
</div>
<table id="invitations-list" class="table table-sm table-hover">
- <thead class="thead-light">
+ <thead>
<tr>
<th scope="col">{{ $t('user.ext-email') }}</th>
<th scope="col">{{ $t('form.created') }}</th>
@@ -38,11 +36,11 @@
{{ inv.created }}
</td>
<td class="buttons">
- <button class="btn text-danger button-delete p-0 ml-1" @click="deleteInvite(inv.id)">
+ <button class="btn text-danger button-delete p-0 ms-1" @click="deleteInvite(inv.id)">
<svg-icon icon="trash-alt"></svg-icon>
<span class="btn-label">{{ $t('btn.delete') }}</span>
</button>
- <button class="btn button-resend p-0 ml-1" :disabled="inv.isNew || inv.isCompleted" @click="resendInvite(inv.id)">
+ <button class="btn button-resend p-0 ms-1" :disabled="inv.isNew || inv.isCompleted" @click="resendInvite(inv.id)">
<svg-icon icon="redo"></svg-icon>
<span class="btn-label">{{ $t('btn.resend') }}</span>
</button>
@@ -67,9 +65,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('invitation.create-title') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<form>
@@ -79,14 +75,13 @@
</div>
<div class="form-separator"><hr><span>{{ $t('form.or') }}</span></div>
<p>{{ $t('invitation.create-csv') }}</p>
- <div class="custom-file">
- <input id="file" type="file" class="custom-file-input" name="csv" @change="fileChange">
- <label class="custom-file-label" for="file">{{ $t('btn.file') }}</label>
+ <div>
+ <input id="file" type="file" class="form-control" name="csv">
</div>
</form>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="inviteUser()">
<svg-icon icon="paper-plane"></svg-icon> {{ $t('invitation.send') }}
</button>
@@ -98,6 +93,7 @@
</template>
<script>
+ import { Modal } from 'bootstrap'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faEnvelopeOpenText, faPaperPlane, faRedo } from '@fortawesome/free-solid-svg-icons'
@@ -115,6 +111,10 @@
mounted() {
this.$root.startLoading()
this.loadInvitations(null, () => this.$root.stopLoading())
+
+ $('#invite-create')[0].addEventListener('shown.bs.modal', event => {
+ $('input', event.target).first().focus()
+ })
},
methods: {
deleteInvite(id) {
@@ -160,7 +160,7 @@
axios.post('/api/v4/invitations', post, params)
.then(response => {
if (response.data.status == 'success') {
- dialog.modal('hide')
+ this.dialog.hide()
this.$toast.success(response.data.message)
if (response.data.count) {
this.loadInvitations({ reset: true })
@@ -169,16 +169,15 @@
})
},
inviteUserDialog() {
- let dialog = $('#invite-create')
- let form = dialog.find('form')
+ const dialog = $('#invite-create')[0]
+ const form = $('form', dialog)
form.get(0).reset()
this.fileChange({ target: form.find('#file')[0] }) // resets file input label
this.$root.clearFormValidation(form)
- dialog.on('shown.bs.modal', () => {
- dialog.find('input').get(0).focus()
- }).modal()
+ this.dialog = new Modal(dialog)
+ this.dialog.show()
},
loadInvitations(params, callback) {
let loader
diff --git a/src/resources/vue/Reseller/Stats.vue b/src/resources/vue/Reseller/Stats.vue
--- a/src/resources/vue/Reseller/Stats.vue
+++ b/src/resources/vue/Reseller/Stats.vue
@@ -9,7 +9,6 @@
mixins: [Stats],
data() {
return {
-// charts: {},
chartTypes: ['users', 'users-all', 'discounts']
}
}
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -1,16 +1,18 @@
<template>
<div class="container">
<div id="step0" v-if="!invitation">
- <div class="plan-selector card-deck">
- <div v-for="item in plans" :key="item.id" :class="'card bg-light plan-' + item.title">
- <div class="card-header plan-header">
- <div class="plan-ico text-center">
- <svg-icon :icon="plan_icons[item.title]"></svg-icon>
+ <div class="plan-selector row row-cols-sm-2 g-3">
+ <div v-for="item in plans" :key="item.id">
+ <div :class="'card bg-light plan-' + item.title">
+ <div class="card-header plan-header">
+ <div class="plan-ico text-center">
+ <svg-icon :icon="plan_icons[item.title]"></svg-icon>
+ </div>
+ </div>
+ <div class="card-body text-center">
+ <button class="btn btn-primary" :data-title="item.title" @click="selectPlan(item.title)" v-html="item.button"></button>
+ <div class="plan-description text-start mt-3" v-html="item.description"></div>
</div>
- </div>
- <div class="card-body text-center ">
- <button class="btn btn-primary" :data-title="item.title" @click="selectPlan(item.title)" v-html="item.button"></button>
- <div class="plan-description text-left mt-3" v-html="item.description"></div>
</div>
</div>
</div>
@@ -23,14 +25,14 @@
{{ $t('signup.step1') }}
</p>
<form @submit.prevent="submitStep1" data-validation-prefix="signup_">
- <div class="form-group">
+ <div class="mb-3">
<div class="input-group">
<input type="text" class="form-control" id="signup_first_name" :placeholder="$t('form.firstname')" autofocus v-model="first_name">
- <input type="text" class="form-control rounded-right" id="signup_last_name" :placeholder="$t('form.surname')" v-model="last_name">
+ <input type="text" class="form-control rounded-end" id="signup_last_name" :placeholder="$t('form.surname')" v-model="last_name">
</div>
</div>
- <div class="form-group">
- <label for="signup_email" class="sr-only">{{ $t('signup.email') }}</label>
+ <div class="mb-3">
+ <label for="signup_email" class="visually-hidden">{{ $t('signup.email') }}</label>
<input type="text" class="form-control" id="signup_email" :placeholder="$t('signup.email')" required v-model="email">
</div>
<button class="btn btn-secondary" type="button" @click="stepBack">{{ $t('btn.back') }}</button>
@@ -46,8 +48,8 @@
{{ $t('signup.step2') }}
</p>
<form @submit.prevent="submitStep2" data-validation-prefix="signup_">
- <div class="form-group">
- <label for="signup_short_code" class="sr-only">{{ $t('form.code') }}</label>
+ <div class="mb-3">
+ <label for="signup_short_code" class="visually-hidden">{{ $t('form.code') }}</label>
<input type="text" class="form-control" id="signup_short_code" :placeholder="$t('form.code')" required v-model="short_code">
</div>
<button class="btn btn-secondary" type="button" @click="stepBack">{{ $t('btn.back') }}</button>
@@ -64,35 +66,33 @@
{{ $t('signup.step3') }}
</p>
<form @submit.prevent="submitStep3" data-validation-prefix="signup_">
- <div class="form-group" v-if="invitation">
+ <div class="mb-3" v-if="invitation">
<div class="input-group">
<input type="text" class="form-control" id="signup_first_name" :placeholder="$t('form.firstname')" autofocus v-model="first_name">
- <input type="text" class="form-control rounded-right" id="signup_last_name" :placeholder="$t('form.surname')" v-model="last_name">
+ <input type="text" class="form-control rounded-end" id="signup_last_name" :placeholder="$t('form.surname')" v-model="last_name">
</div>
</div>
- <div class="form-group">
- <label for="signup_login" class="sr-only"></label>
+ <div class="mb-3">
+ <label for="signup_login" class="visually-hidden"></label>
<div class="input-group">
<input type="text" class="form-control" id="signup_login" required v-model="login" :placeholder="$t('signup.login')">
- <span class="input-group-append input-group-prepend">
- <span class="input-group-text">@</span>
- </span>
- <input v-if="is_domain" type="text" class="form-control rounded-right" id="signup_domain" required v-model="domain" :placeholder="$t('form.domain')">
- <select v-else class="custom-select rounded-right" id="signup_domain" required v-model="domain">
+ <span class="input-group-text">@</span>
+ <input v-if="is_domain" type="text" class="form-control rounded-end" id="signup_domain" required v-model="domain" :placeholder="$t('form.domain')">
+ <select v-else class="form-select rounded-end" id="signup_domain" required v-model="domain">
<option v-for="domain in domains" :key="domain" :value="domain">{{ domain }}</option>
</select>
</div>
</div>
- <div class="form-group">
- <label for="signup_password" class="sr-only">{{ $t('form.password') }}</label>
+ <div class="mb-3">
+ <label for="signup_password" class="visually-hidden">{{ $t('form.password') }}</label>
<input type="password" class="form-control" id="signup_password" :placeholder="$t('form.password')" required v-model="password">
</div>
- <div class="form-group">
- <label for="signup_confirm" class="sr-only">{{ $t('form.password-confirm') }}</label>
+ <div class="mb-3">
+ <label for="signup_confirm" class="visually-hidden">{{ $t('form.password-confirm') }}</label>
<input type="password" class="form-control" id="signup_confirm" :placeholder="$t('form.password-confirm')" required v-model="password_confirmation">
</div>
- <div class="form-group pt-2 pb-2">
- <label for="signup_voucher" class="sr-only">{{ $t('signup.voucher') }}</label>
+ <div class="mb-3 pt-2">
+ <label for="signup_voucher" class="visually-hidden">{{ $t('signup.voucher') }}</label>
<input type="text" class="form-control" id="signup_voucher" :placeholder="$t('signup.voucher')" v-model="voucher">
</div>
<button v-if="!invitation" class="btn btn-secondary" type="button" @click="stepBack">{{ $t('btn.back') }}</button>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -6,7 +6,7 @@
<div class="card-body">
<div class="card-title" v-if="user_id !== 'new'">{{ $t('user.title') }}
<button
- class="btn btn-outline-danger button-delete float-right"
+ class="btn btn-outline-danger button-delete float-end"
@click="showDeleteConfirmation()" type="button"
>
<svg-icon icon="trash-alt"></svg-icon> {{ $t('user.delete') }}
@@ -30,59 +30,59 @@
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
<div class="card-body">
<form @submit.prevent="submit">
- <div v-if="user_id !== 'new'" class="form-group row plaintext">
+ <div v-if="user_id !== 'new'" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span :class="$root.userStatusClass(user) + ' form-control-plaintext'" id="status">{{ $root.userStatusText(user) }}</span>
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.firstname') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="first_name" v-model="user.first_name">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="last_name" class="col-sm-4 col-form-label">{{ $t('form.lastname') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="last_name" v-model="user.last_name">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="organization" class="col-sm-4 col-form-label">{{ $t('user.org') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="organization" v-model="user.organization">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" :disabled="user_id !== 'new'" required v-model="user.email">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="aliases-input" class="col-sm-4 col-form-label">{{ $t('user.aliases-email') }}</label>
<div class="col-sm-8">
<list-input id="aliases" :list="user.aliases"></list-input>
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="password" class="col-sm-4 col-form-label">{{ $t('form.password') }}</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password" v-model="user.password" :required="user_id === 'new'">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="password_confirmaton" class="col-sm-4 col-form-label">{{ $t('form.password-confirm') }}</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password_confirmation" v-model="user.password_confirmation" :required="user_id === 'new'">
</div>
</div>
- <div v-if="user_id === 'new'" id="user-packages" class="form-group row">
+ <div v-if="user_id === 'new'" id="user-packages" class="row mb-3">
<label class="col-sm-4 col-form-label">Package</label>
<div class="col-sm-8">
<table class="table table-sm form-list">
- <thead class="thead-light sr-only">
+ <thead class="visually-hidden">
<tr>
<th scope="col"></th>
<th scope="col">{{ $t('user.package') }}</th>
@@ -106,9 +106,9 @@
{{ $root.priceLabel(pkg.cost, discount) }}
</td>
<td class="buttons">
- <button v-if="pkg.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip.click="pkg.description">
+ <button v-if="pkg.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip="pkg.description">
<svg-icon icon="info-circle"></svg-icon>
- <span class="sr-only">{{ $t('btn.moreinfo') }}</span>
+ <span class="visually-hidden">{{ $t('btn.moreinfo') }}</span>
</button>
</td>
</tr>
@@ -120,11 +120,11 @@
</small>
</div>
</div>
- <div v-if="user_id !== 'new'" id="user-skus" class="form-group row">
+ <div v-if="user_id !== 'new'" id="user-skus" class="row mb-3">
<label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
<div class="col-sm-8">
<table class="table table-sm form-list">
- <thead class="thead-light sr-only">
+ <thead class="visually-hidden">
<tr>
<th scope="col"></th>
<th scope="col">{{ $t('user.subscription') }}</th>
@@ -147,7 +147,7 @@
<div v-if="sku.range" class="range-input">
<label class="text-nowrap">{{ sku.range.min }} {{ sku.range.unit }}</label>
<input
- type="range" class="custom-range" @input="rangeUpdate"
+ type="range" class="form-range" @input="rangeUpdate"
:value="sku.value || sku.range.min"
:min="sku.range.min"
:max="sku.range.max"
@@ -158,9 +158,9 @@
{{ $root.priceLabel(sku.cost, discount) }}
</td>
<td class="buttons">
- <button v-if="sku.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip.click="sku.description">
+ <button v-if="sku.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip="sku.description">
<svg-icon icon="info-circle"></svg-icon>
- <span class="sr-only">{{ $t('btn.moreinfo') }}</span>
+ <span class="visually-hidden">{{ $t('btn.moreinfo') }}</span>
</button>
</td>
</tr>
@@ -179,11 +179,11 @@
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<form @submit.prevent="submitSettings">
- <div class="form-group row checkbox">
+ <div class="row checkbox mb-3">
<label for="greylisting" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
<div class="col-sm-8 pt-2">
- <input type="checkbox" id="greylisting" name="greylisting" value="1" :checked="user.config.greylisting">
- <small id="greylisting-hint" class="form-text text-muted">
+ <input type="checkbox" id="greylisting" name="greylisting" value="1" class="form-check-input d-block mb-2" :checked="user.config.greylisting">
+ <small id="greylisting-hint" class="text-muted">
{{ $t('user.greylisting-text') }}
</small>
</div>
@@ -201,15 +201,13 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.delete-email', { email: user.email }) }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>{{ $t('user.delete-text') }}</p>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-danger modal-action" @click="deleteUser()">
<svg-icon icon="trash-alt"></svg-icon> {{ $t('btn.delete') }}
</button>
@@ -221,6 +219,7 @@
</template>
<script>
+ import { Modal } from 'bootstrap'
import ListInput from '../Widgets/ListInput'
import StatusComponent from '../Widgets/Status'
@@ -312,6 +311,9 @@
},
mounted() {
$('#first_name').focus()
+ $('#delete-warning')[0].addEventListener('shown.bs.modal', event => {
+ $(event.target).find('button.modal-cancel').focus()
+ })
},
methods: {
submit() {
@@ -469,15 +471,12 @@
})
},
showDeleteConfirmation() {
- // Deleting self, redirect to /profile/delete page
if (this.user_id == this.$store.state.authInfo.id) {
+ // Deleting self, redirect to /profile/delete page
this.$router.push({ name: 'profile-delete' })
} else {
// Display the warning
- let dialog = $('#delete-warning')
- dialog.on('shown.bs.modal', () => {
- dialog.find('button.modal-cancel').focus()
- }).modal()
+ new Modal('#delete-warning').show()
}
}
}
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -4,13 +4,13 @@
<div class="card-body">
<div class="card-title">
{{ $t('user.list-title') }}
- <router-link class="btn btn-success float-right create-user" :to="{ path: 'user/new' }" tag="button">
+ <router-link class="btn btn-success float-end create-user" :to="{ path: 'user/new' }" tag="button">
<svg-icon icon="user"></svg-icon> {{ $t('user.create') }}
</router-link>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
- <thead class="thead-light">
+ <thead>
<tr>
<th scope="col">{{ $t('form.primary-email') }}</th>
</tr>
diff --git a/src/resources/vue/User/Profile.vue b/src/resources/vue/User/Profile.vue
--- a/src/resources/vue/User/Profile.vue
+++ b/src/resources/vue/User/Profile.vue
@@ -6,7 +6,7 @@
{{ $t('user.profile-title') }}
<router-link
v-if="$root.isController(wallet_id)"
- class="btn btn-outline-danger button-delete float-right"
+ class="btn btn-outline-danger button-delete float-end"
to="/profile/delete" tag="button"
>
<svg-icon icon="trash-alt"></svg-icon> {{ $t('user.profile-delete') }}
@@ -14,70 +14,70 @@
</div>
<div class="card-text">
<form @submit.prevent="submit">
- <div class="form-group row plaintext">
+ <div class="row mb-3 plaintext">
<label class="col-sm-4 col-form-label">{{ $t('user.custno') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="userid">{{ user_id }}</span>
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.firstname') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="first_name" v-model="profile.first_name">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="last_name" class="col-sm-4 col-form-label">{{ $t('form.lastname') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="last_name" v-model="profile.last_name">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="organization" class="col-sm-4 col-form-label">{{ $t('user.org') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="organization" v-model="profile.organization">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="phone" class="col-sm-4 col-form-label">{{ $t('form.phone') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="phone" v-model="profile.phone">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="external_email" class="col-sm-4 col-form-label">{{ $t('user.ext-email') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="external_email" v-model="profile.external_email">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="billing_address" class="col-sm-4 col-form-label">{{ $t('user.address') }}</label>
<div class="col-sm-8">
<textarea class="form-control" id="billing_address" rows="3" v-model="profile.billing_address"></textarea>
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="country" class="col-sm-4 col-form-label">{{ $t('user.country') }}</label>
<div class="col-sm-8">
- <select class="form-control custom-select" id="country" v-model="profile.country">
+ <select class="form-select" id="country" v-model="profile.country">
<option value="">-</option>
<option v-for="(item, code) in countries" :value="code" :key="code">{{ item[1] }}</option>
</select>
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="password" class="col-sm-4 col-form-label">{{ $t('form.password') }}</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password" v-model="profile.password">
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="password_confirmaton" class="col-sm-4 col-form-label">{{ $t('form.password-confirm') }}</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password_confirmation" v-model="profile.password_confirmation">
</div>
</div>
- <button class="btn btn-primary button-submit" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
+ <button class="btn btn-primary button-submit mt-2" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
</div>
diff --git a/src/resources/vue/User/ProfileDelete.vue b/src/resources/vue/User/ProfileDelete.vue
--- a/src/resources/vue/User/ProfileDelete.vue
+++ b/src/resources/vue/User/ProfileDelete.vue
@@ -2,7 +2,7 @@
<div class="container">
<div class="card" id="user-delete">
<div class="card-body">
- <div class="card-title">Delete this account?</div>
+ <div class="card-title">{{ $t('user.delete-account') }}</div>
<div class="card-text">
<p>{{ $t('user.profile-delete-text1') }} <strong>{{ $t('user.profile-delete-warning') }}</strong>.</p>
<p>{{ $t('user.profile-delete-text2') }}</p>
diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue
--- a/src/resources/vue/Wallet.vue
+++ b/src/resources/vue/Wallet.vue
@@ -69,11 +69,9 @@
<select id="receipt-id" class="form-control">
<option v-for="(receipt, index) in receipts" :key="index" :value="receipt">{{ receipt }}</option>
</select>
- <div class="input-group-append">
- <button type="button" class="btn btn-secondary" @click="receiptDownload">
- <svg-icon icon="download"></svg-icon> {{ $t('btn.download') }}
- </button>
- </div>
+ <button type="button" class="btn btn-secondary" @click="receiptDownload">
+ <svg-icon icon="download"></svg-icon> {{ $t('btn.download') }}
+ </button>
</div>
<p v-if="!receipts.length">
{{ $t('wallet.receipts-none') }}
@@ -98,9 +96,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ paymentDialogTitle }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<div id="payment-method" v-if="paymentForm == 'method'">
@@ -125,15 +121,11 @@
{{ $t('wallet.payment-amount-hint') }}
</p>
<form id="payment-form" @submit.prevent="payment">
- <div class="form-group">
- <div class="input-group">
- <input type="text" class="form-control" id="amount" v-model="amount" required>
- <span class="input-group-append">
- <span class="input-group-text">{{ wallet.currency }}</span>
- </span>
- </div>
+ <div class="input-group">
+ <input type="text" class="form-control" id="amount" v-model="amount" required>
+ <span class="input-group-text">{{ wallet.currency }}</span>
</div>
- <div v-if="wallet.currency != selectedPaymentMethod.currency && !isNaN(amount)" class="alert alert-warning">
+ <div v-if="wallet.currency != selectedPaymentMethod.currency && !isNaN(amount)" class="alert alert-warning m-0 mt-3">
{{ $t('wallet.payment-warning', { price: $root.price(amount * selectedPaymentMethod.exchangeRate * 100, selectedPaymentMethod.currency) }) }}
</div>
</form>
@@ -143,37 +135,35 @@
<p>
{{ $t('wallet.auto-payment-hint') }}
</p>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="mandate_amount" class="col-sm-6 col-form-label">{{ $t('wallet.fill-up') }}</label>
- <div class="input-group col-sm-6">
- <input type="text" class="form-control" id="mandate_amount" v-model="mandate.amount" required>
- <span class="input-group-append">
+ <div class="col-sm-6">
+ <div class="input-group">
+ <input type="text" class="form-control" id="mandate_amount" v-model="mandate.amount" required>
<span class="input-group-text">{{ wallet.currency }}</span>
- </span>
+ </div>
</div>
</div>
- <div class="form-group row">
+ <div class="row mb-3">
<label for="mandate_balance" class="col-sm-6 col-form-label">{{ $t('wallet.when-below') }}</label>
<div class="col-sm-6">
<div class="input-group">
<input type="text" class="form-control" id="mandate_balance" v-model="mandate.balance" required>
- <span class="input-group-append">
- <span class="input-group-text">{{ wallet.currency }}</span>
- </span>
+ <span class="input-group-text">{{ wallet.currency }}</span>
</div>
</div>
</div>
<p v-if="!mandate.isValid">
{{ $t('wallet.auto-payment-next') }}
</p>
- <div v-if="mandate.isValid && mandate.isDisabled" class="disabled-mandate alert alert-danger">
+ <div v-if="mandate.isValid && mandate.isDisabled" class="disabled-mandate alert alert-danger m-0">
{{ $t('wallet.auto-payment-disabled-next') }}
</div>
</form>
</div>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action"
v-if="paymentForm == 'auto' && (mandate.isValid || mandate.isPending)"
@click="autoPayment"
@@ -200,6 +190,7 @@
</template>
<script>
+ import { Modal } from 'bootstrap'
import TransactionLog from './Widgets/TransactionLog'
import PaymentLog from './Widgets/PaymentLog'
@@ -261,16 +252,14 @@
.then(response => {
this.showPendingPayments = response.data.hasPending
})
-
},
updated() {
$(this.$el).find('ul.nav-tabs a').on('click', e => {
- e.preventDefault()
- $(e.target).tab('show')
+ this.$root.tab(e)
+
if ($(e.target).is('#tab-history')) {
this.loadTransactions = true
- }
- if ($(e.target).is('#tab-payments')) {
+ } else if ($(e.target).is('#tab-payments')) {
this.loadPayments = true
}
})
@@ -295,16 +284,10 @@
},
selectPaymentMethod(method) {
this.formLock = false
-
this.selectedPaymentMethod = method
-
this.paymentForm = this.nextForm
- this.formLock = false
- setTimeout(() => {
- this.dialog.find('#mandate_amount').focus()
- this.dialog.find('#amount').focus()
- }, 10)
+ setTimeout(() => { $('#payment-dialog').find('#amount,#mandate_amount').focus() }, 10)
},
payment() {
if (this.formLock) {
@@ -362,7 +345,7 @@
} else {
// an update
if (response.data.status == 'success') {
- this.dialog.modal('hide');
+ this.dialog.hide();
this.mandate = response.data
this.$toast.success(response.data.message)
}
@@ -381,43 +364,34 @@
}
})
},
-
paymentMethodForm(nextForm) {
- const dialog = $('#payment-dialog')
this.formLock = false
this.paymentMethods = []
-
this.paymentForm = 'method'
this.nextForm = nextForm
- if (nextForm == 'auto') {
- this.paymentDialogTitle = this.$t('wallet.auto-payment-setup')
- } else {
- this.paymentDialogTitle = this.$t('wallet.top-up')
- }
+ this.paymentDialogTitle = this.$t(nextForm == 'auto' ? 'wallet.auto-payment-setup' : 'wallet.top-up')
- const methods = $('#payment-method')
- this.$root.addLoader(methods, false)
- axios.get('/api/v4/payments/methods', {params: {type: nextForm == 'manual' ? 'oneoff' : 'recurring'}})
- .then(response => {
- this.$root.removeLoader(methods)
- this.paymentMethods = response.data
- })
- .catch(this.$root.errorHandler)
+ this.dialog = new Modal('#payment-dialog')
+ this.dialog.show()
- this.dialog = dialog.on('shown.bs.modal', () => {}).modal()
+ this.$nextTick().then(() => {
+ const form = $('#payment-method')
+ this.$root.addLoader(form, false, {'min-height': '10em'})
+
+ axios.get('/api/v4/payments/methods', {params: {type: nextForm == 'manual' ? 'oneoff' : 'recurring'}})
+ .then(response => {
+ this.$root.removeLoader(form)
+ this.paymentMethods = response.data
+ })
+ })
},
autoPaymentForm(event, title) {
- const dialog = $('#payment-dialog')
-
this.paymentForm = 'auto'
this.paymentDialogTitle = title
this.formLock = false
- this.dialog = dialog.on('shown.bs.modal', () => {
- dialog.find('#mandate_amount').focus()
- }).modal()
-
- setTimeout(() => { this.dialog.find('#mandate_amount').focus()}, 10)
+ this.dialog = new Modal('#payment-dialog')
+ this.dialog.show()
},
receiptDownload() {
const receipt = $('#receipt-id').val()
diff --git a/src/resources/vue/Widgets/ListInput.vue b/src/resources/vue/Widgets/ListInput.vue
--- a/src/resources/vue/Widgets/ListInput.vue
+++ b/src/resources/vue/Widgets/ListInput.vue
@@ -2,21 +2,17 @@
<div class="list-input" :id="id">
<div class="input-group">
<input :id="id + '-input'" type="text" class="form-control main-input" @keydown="keyDown">
- <div class="input-group-append">
- <a href="#" class="btn btn-outline-secondary" @click.prevent="addItem">
- <svg-icon icon="plus"></svg-icon>
- <span class="sr-only">{{ $t('btn.add') }}</span>
- </a>
- </div>
+ <a href="#" class="btn btn-outline-secondary" @click.prevent="addItem">
+ <svg-icon icon="plus"></svg-icon>
+ <span class="visually-hidden">{{ $t('btn.add') }}</span>
+ </a>
</div>
<div class="input-group" v-for="(item, index) in list" :key="index">
<input type="text" class="form-control" v-model="list[index]">
- <div class="input-group-append">
- <a href="#" class="btn btn-outline-secondary" @click.prevent="deleteItem(index)">
- <svg-icon icon="trash-alt"></svg-icon>
- <span class="sr-only">{{ $t('btn.delete') }}</span>
- </a>
- </div>
+ <a href="#" class="btn btn-outline-secondary" @click.prevent="deleteItem(index)">
+ <svg-icon icon="trash-alt"></svg-icon>
+ <span class="visually-hidden">{{ $t('btn.delete') }}</span>
+ </a>
</div>
</div>
</template>
diff --git a/src/resources/vue/Widgets/Menu.vue b/src/resources/vue/Widgets/Menu.vue
--- a/src/resources/vue/Widgets/Menu.vue
+++ b/src/resources/vue/Widgets/Menu.vue
@@ -1,15 +1,15 @@
<template>
- <nav :id="mode + '-menu'" class="navbar navbar-expand-lg navbar-light">
- <div class="container">
+ <nav :id="mode + '-menu'" :class="'navbar navbar-light navbar-expand-' + (mode == 'header' ? 'lg' : 'sm')">
+ <div class="container p-0">
<router-link class="navbar-brand" to="/" v-html="$root.logo(mode)"></router-link>
<button v-if="mode == 'header'" class="navbar-toggler" type="button"
- data-toggle="collapse" :data-target="'#' + mode + '-menu-navbar'"
- aria-controls="navbar" aria-expanded="false" :aria-label="$t('menu.toggle')"
+ data-bs-toggle="collapse" data-bs-target="#header-menu-navbar"
+ aria-controls="header-menu-navbar" aria-expanded="false" :aria-label="$t('menu.toggle')"
>
<span class="navbar-toggler-icon"></span>
</button>
- <div :id="mode + '-menu-navbar'" :class="'navbar' + (mode == 'header' ? ' collapse navbar-collapse' : '')">
- <ul class="navbar-nav">
+ <div :id="mode + '-menu-navbar'" :class="mode == 'header' ? 'collapse navbar-collapse justify-content-end' : ''">
+ <ul class="navbar-nav justify-content-end">
<li class="nav-item" v-for="item in menu" :key="item.index">
<a v-if="item.href" :class="'nav-link link-' + item.index" :href="item.href">{{ item.title }}</a>
<router-link v-if="item.to"
@@ -34,8 +34,8 @@
<router-link class="nav-link menulogin link-login" :to="{name: 'login'}">{{ $t('menu.login') }}</router-link>
</li>
<li v-if="languages.length > 1 && mode == 'header'" id="language-selector" class="nav-item dropdown">
- <a href="#" class="nav-link link-lang dropdown-toggle" role="button" data-toggle="dropdown">{{ getLang().toUpperCase() }}</a>
- <div class="dropdown-menu dropdown-menu-right">
+ <a href="#" class="nav-link link-lang dropdown-toggle" role="button" data-bs-toggle="dropdown">{{ getLang().toUpperCase() }}</a>
+ <div class="dropdown-menu dropdown-menu-right mb-2">
<a class="dropdown-item" href="#" v-for="lang in languages" :key="lang" @click="setLang(lang)">
{{ lang.toUpperCase() }} - {{ $t('lang.' + lang) }}
</a>
diff --git a/src/resources/vue/Widgets/PaymentLog.vue b/src/resources/vue/Widgets/PaymentLog.vue
--- a/src/resources/vue/Widgets/PaymentLog.vue
+++ b/src/resources/vue/Widgets/PaymentLog.vue
@@ -1,7 +1,7 @@
<template>
<div>
<table class="table table-sm m-0 payments">
- <thead class="thead-light">
+ <thead>
<tr>
<th scope="col">{{ $t('form.date') }}</th>
<th scope="col">{{ $t('form.description') }}</th>
diff --git a/src/resources/vue/Widgets/SupportForm.vue b/src/resources/vue/Widgets/SupportForm.vue
--- a/src/resources/vue/Widgets/SupportForm.vue
+++ b/src/resources/vue/Widgets/SupportForm.vue
@@ -4,37 +4,35 @@
<form class="modal-content" @submit.prevent="submit">
<div class="modal-header">
<h5 class="modal-title">{{ $t('support.title') }}</h5>
- <button type="button" class="close" data-dismiss="modal" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
- <div class="form">
- <div class="form-group">
- <label for="support-user">{{ $t('support.id') }}</label>
+ <form>
+ <div class="mb-3">
+ <label for="support-user" class="form-label">{{ $t('support.id') }}</label>
<input id="support-user" type="text" class="form-control" :placeholder="$t('support.id-pl')" v-model="user" />
- <small class="form-text text-muted">{{ $t('support.id-hint') }}</small>
+ <small class="text-muted">{{ $t('support.id-hint') }}</small>
</div>
- <div class="form-group">
- <label for="support-name">{{ $t('support.name') }}</label>
+ <div class="mb-3">
+ <label for="support-name" class="form-label">{{ $t('support.name') }}</label>
<input id="support-name" type="text" class="form-control" :placeholder="$t('support.name-pl')" v-model="name" />
</div>
- <div class="form-group">
- <label for="support-email">{{ $t('support.email') }}</label>
+ <div class="mb-3">
+ <label for="support-email" class="form-label">{{ $t('support.email') }}</label>
<input id="support-email" type="email" class="form-control" :placeholder="$t('support.email-pl')" v-model="email" required />
</div>
- <div class="form-group">
- <label for="support-summary">{{ $t('support.summary') }}</label>
+ <div class="mb-3">
+ <label for="support-summary" class="form-label">{{ $t('support.summary') }}</label>
<input id="support-summary" type="text" class="form-control" :placeholder="$t('support.summary-pl')" v-model="summary" required />
</div>
- <div class="form-group">
- <label for="support-body">{{ $t('support.expl') }}</label>
+ <div>
+ <label for="support-body" class="form-label">{{ $t('support.expl') }}</label>
<textarea id="support-body" class="form-control" rows="5" v-model="body" required></textarea>
</div>
- </div>
+ </form>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="submit" class="btn btn-primary modal-action"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</div>
</form>
@@ -43,6 +41,8 @@
</template>
<script>
+ import { Modal } from 'bootstrap'
+
export default {
data() {
return {
@@ -54,21 +54,32 @@
}
},
mounted() {
- this.dialog = $('#support-dialog')
- .on('hide.bs.modal', () => {
- this.lockForm(false)
- if (this.cancelToken) {
- this.cancelToken()
- this.cancelToken = null
- }
- })
- .on('show.bs.modal', () => {
+ const dialog = this.$el
+
+ dialog.addEventListener('hide.bs.modal', () => {
+ this.lockForm(false)
+ if (this.cancelToken) {
+ this.cancelToken()
this.cancelToken = null
- })
+ }
+ })
+
+ dialog.addEventListener('show.bs.modal', () => {
+ this.cancelToken = null
+ })
+
+ dialog.addEventListener('shown.bs.modal', () => {
+ $(dialog).find('input').first().focus()
+ })
+
+ this.dialog = new Modal(dialog)
},
methods: {
lockForm(lock) {
- this.dialog.find('input,textarea,.modal-action').prop('disabled', lock)
+ $(this.$el).find('input,textarea,.modal-action').prop('disabled', lock)
+ },
+ showDialog() {
+ this.dialog.show()
},
submit() {
this.lockForm(true)
@@ -94,7 +105,7 @@
this.summary = ''
this.body = ''
this.lockForm(false)
- this.dialog.modal('hide')
+ this.dialog.hide()
this.$toast.success(response.data.message)
})
.catch(error => {
diff --git a/src/resources/vue/Widgets/ToastMessage.vue b/src/resources/vue/Widgets/ToastMessage.vue
--- a/src/resources/vue/Widgets/ToastMessage.vue
+++ b/src/resources/vue/Widgets/ToastMessage.vue
@@ -7,9 +7,7 @@
<svg-icon icon="exclamation-circle" v-else-if="data.type == 'warning'"></svg-icon>
<svg-icon :icon="data.icon" v-else-if="data.type == 'custom' && data.icon"></svg-icon>
<strong>{{ data.title || $t('msg.' + data.type) }}</strong>
- <button type="button" class="close" data-dismiss="toast" :aria-label="$t('btn.close')">
- <span aria-hidden="true">&times;</span>
- </button>
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" :aria-label="$t('btn.close')"></button>
</div>
<div v-if="data.body" v-html="data.body" class="toast-body"></div>
<div v-else class="toast-body">{{ data.msg }}</div>
@@ -17,27 +15,29 @@
</template>
<script>
+ import { Toast } from 'bootstrap'
+
export default {
props: {
data: { type: Object, default: () => {} }
},
mounted() {
- $(this.$el)
- .on('hidden.bs.toast', () => {
- (this.$el).remove()
- this.$destroy()
- })
- .on('shown.bs.toast', () => {
- if (this.data.onShow) {
- this.data.onShow(this.$el)
- }
- })
- .toast({
+ this.$el.addEventListener('hidden.bs.toast', () => {
+ (this.$el).remove()
+ this.$destroy()
+ })
+
+ this.$el.addEventListener('shown.bs.toast', () => {
+ if (this.data.onShow) {
+ this.data.onShow(this.$el)
+ }
+ })
+
+ new Toast(this.$el, {
animation: true,
autohide: this.data.timeout > 0,
delay: this.data.timeout
- })
- .toast('show')
+ }).show()
},
methods: {
className() {
diff --git a/src/resources/vue/Widgets/TransactionLog.vue b/src/resources/vue/Widgets/TransactionLog.vue
--- a/src/resources/vue/Widgets/TransactionLog.vue
+++ b/src/resources/vue/Widgets/TransactionLog.vue
@@ -1,7 +1,7 @@
<template>
<div>
<table class="table table-sm m-0 transactions">
- <thead class="thead-light">
+ <thead>
<tr>
<th scope="col">{{ $t('form.date') }}</th>
<th scope="col" v-if="isAdmin">{{ $t('form.user') }}</th>
diff --git a/src/resources/vue/Widgets/UserSearch.vue b/src/resources/vue/Widgets/UserSearch.vue
--- a/src/resources/vue/Widgets/UserSearch.vue
+++ b/src/resources/vue/Widgets/UserSearch.vue
@@ -4,13 +4,11 @@
<form @submit.prevent="searchUser" class="row justify-content-center">
<div class="input-group col-sm-8">
<input class="form-control" type="text" :placeholder="$t('user.search-pl')" v-model="search">
- <div class="input-group-append">
- <button type="submit" class="btn btn-primary"><svg-icon icon="search"></svg-icon> {{ $t('btn.search') }}</button>
- </div>
+ <button type="submit" class="btn btn-primary"><svg-icon icon="search"></svg-icon> {{ $t('btn.search') }}</button>
</div>
</form>
<table v-if="users.length" class="table table-sm table-hover mt-4">
- <thead class="thead-light">
+ <thead>
<tr>
<th scope="col">{{ $t('form.primary-email') }}</th>
<th scope="col">{{ $t('form.id') }}</th>
@@ -47,7 +45,7 @@
}
},
mounted() {
- $('#search-box input').focus()
+ $('#search-box input', this.$el).focus()
},
methods: {
searchUser() {
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -160,7 +160,7 @@
*/
public function clearToasts()
{
- $this->script("jQuery('.toast-container > *').remove()");
+ $this->script("\$('.toast-container > *').remove()");
return $this;
}
diff --git a/src/tests/Browser/Components/Dialog.php b/src/tests/Browser/Components/Dialog.php
--- a/src/tests/Browser/Components/Dialog.php
+++ b/src/tests/Browser/Components/Dialog.php
@@ -45,7 +45,7 @@
public function elements()
{
return [
- '@title' => '.modal-header .modal-title',
+ '@title' => '.modal-title',
'@body' => '.modal-body',
'@button-action' => '.modal-footer button.modal-action',
'@button-cancel' => '.modal-footer button.modal-cancel',
diff --git a/src/tests/Browser/Components/Toast.php b/src/tests/Browser/Components/Toast.php
--- a/src/tests/Browser/Components/Toast.php
+++ b/src/tests/Browser/Components/Toast.php
@@ -97,6 +97,6 @@
*/
public function closeToast($browser)
{
- $this->element->findElements(WebDriverBy::cssSelector('button.close'))[0]->click();
+ $this->element->findElements(WebDriverBy::cssSelector('.btn-close'))[0]->click();
}
}
diff --git a/src/tests/Browser/FaqTest.php b/src/tests/Browser/FaqTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/FaqTest.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Tests\Browser;
+
+use Tests\Browser;
+use Tests\Browser\Pages\Signup;
+use Tests\TestCaseDusk;
+
+class FaqTest extends TestCaseDusk
+{
+ /**
+ * Test FAQ widget
+ */
+ public function testFaq(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Signup())
+ ->whenAvailable('#faq', function ($browser) {
+ $browser->assertSeeIn('h5', 'FAQ')
+ ->assertElementsCount('ul > li', 4)
+ ->assertSeeIn('li:last-child a', 'Need support?')
+ ->click('li:last-child a');
+ })
+ ->waitForLocation('/support');
+ });
+ }
+}
diff --git a/src/tests/Browser/Meet/RoomSetupTest.php b/src/tests/Browser/Meet/RoomSetupTest.php
--- a/src/tests/Browser/Meet/RoomSetupTest.php
+++ b/src/tests/Browser/Meet/RoomSetupTest.php
@@ -168,7 +168,7 @@
// Join the room (click the button twice, to make sure it does not
// produce redundant participants/subscribers in the room)
->clickWhenEnabled('@setup-button')
- ->pause(10)
+ ->pause(5)
->click('@setup-button')
->waitFor('@session')
->assertMissing('@setup-form')
diff --git a/src/tests/Browser/Pages/UserInfo.php b/src/tests/Browser/Pages/UserInfo.php
--- a/src/tests/Browser/Pages/UserInfo.php
+++ b/src/tests/Browser/Pages/UserInfo.php
@@ -42,6 +42,7 @@
'@nav' => 'ul.nav-tabs',
'@packages' => '#user-packages',
'@settings' => '#settings',
+ '@general' => '#general',
'@skus' => '#user-skus',
'@status' => '#status-box',
];
diff --git a/src/tests/Browser/Reseller/InvitationsTest.php b/src/tests/Browser/Reseller/InvitationsTest.php
--- a/src/tests/Browser/Reseller/InvitationsTest.php
+++ b/src/tests/Browser/Reseller/InvitationsTest.php
@@ -94,12 +94,11 @@
->assertMissing('@body input#email.is-invalid')
// Submit an empty file
->attach('@body input#file', __DIR__ . '/../../data/empty.csv')
- ->assertSeeIn('@body input#file + label', 'empty.csv')
->click('@button-action')
->assertToast(Toast::TYPE_ERROR, "Form validation error")
// ->waitFor('input#file.is-invalid')
->assertSeeIn(
- '@body input#file.is-invalid + label + .invalid-feedback',
+ '@body input#file.is-invalid + .invalid-feedback',
"Failed to find any valid email addresses in the uploaded file."
)
// Submit non-empty file
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -113,10 +113,10 @@
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login', 'lang'], 'signup');
});
- $browser->waitFor('@step0 .plan-selector > .card');
+ $browser->waitFor('@step0 .plan-selector .card');
// Assert first plan box and press the button
- $browser->with('@step0 .plan-selector > .plan-individual', function ($step) {
+ $browser->with('@step0 .plan-selector .plan-individual', function ($step) {
$step->assertVisible('button')
->assertSeeIn('button', 'Individual Account')
->assertVisible('.plan-description')
@@ -139,7 +139,7 @@
->assertMissing('@step3');
// Choose the group account plan
- $browser->click('@step0 .plan-selector > .plan-group button')
+ $browser->click('@step0 .plan-selector .plan-group button')
->waitForLocation('/signup/group')
->assertVisible('@step1')
->assertMissing('@step0')
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -146,7 +146,7 @@
->click('@table tr:nth-child(3) a')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
- ->with('@form', function (Browser $browser) {
+ ->with('@general', function (Browser $browser) {
// Assert form content
$browser->assertSeeIn('div.row:nth-child(1) label', 'Status')
->assertSeeIn('div.row:nth-child(1) #status', 'Active')
@@ -181,7 +181,7 @@
->click('@table tr:nth-child(3) a')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
- ->with('@form', function (Browser $browser) {
+ ->with('@general', function (Browser $browser) {
// Test error handling (password)
$browser->type('#password', 'aaaaaa')
->vueClear('#password_confirmation')
@@ -223,7 +223,7 @@
$this->assertTrue(!empty($alias));
// Test subscriptions
- $browser->with('@form', function (Browser $browser) {
+ $browser->with('@general', function (Browser $browser) {
$browser->assertSeeIn('div.row:nth-child(9) label', 'Subscriptions')
->assertVisible('@skus.row:nth-child(9)')
->with('@skus', function ($browser) {
@@ -301,7 +301,7 @@
$this->assertUserEntitlements($john, $expected);
// Test subscriptions interaction
- $browser->with('@form', function (Browser $browser) {
+ $browser->with('@general', function (Browser $browser) {
$browser->with('@skus', function ($browser) {
// Uncheck 'groupware', expect activesync unchecked
$browser->click('#sku-input-groupware')
@@ -343,8 +343,9 @@
$john = $this->getTestUser('john@kolab.org');
$john->setSetting('greylist_enabled', null);
- $this->browse(function (Browser $browser) {
- $browser->on(new UserInfo())
+ $this->browse(function (Browser $browser) use ($john) {
+ $browser->visit('/user/' . $john->id)
+ ->on(new UserInfo())
->assertElementsCount('@nav a', 2)
->assertSeeIn('@nav #tab-general', 'General')
->assertSeeIn('@nav #tab-settings', 'Settings')
@@ -373,7 +374,7 @@
->click('button.create-user')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'New user account')
- ->with('@form', function (Browser $browser) {
+ ->with('@general', function (Browser $browser) {
// Assert form content
$browser->assertFocused('div.row:nth-child(1) input')
->assertSeeIn('div.row:nth-child(1) label', 'First Name')
@@ -428,7 +429,7 @@
});
// Test form error handling (aliases)
- $browser->with('@form', function (Browser $browser) {
+ $browser->with('@general', function (Browser $browser) {
$browser->type('#email', 'julia.roberts@kolab.org')
->type('#password_confirmation', 'simple123')
->with(new ListInput('#aliases'), function (Browser $browser) {
@@ -442,7 +443,7 @@
});
// Successful account creation
- $browser->with('@form', function (Browser $browser) {
+ $browser->with('@general', function (Browser $browser) {
$browser->type('#first_name', 'Julia')
->type('#last_name', 'Roberts')
->type('#organization', 'Test Org')
@@ -462,6 +463,7 @@
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
$alias = UserAlias::where('user_id', $julia->id)->where('alias', 'julia.roberts2@kolab.org')->first();
+
$this->assertTrue(!empty($alias));
$this->assertUserEntitlements($julia, ['mailbox', 'storage', 'storage', 'storage', 'storage', 'storage']);
$this->assertSame('Julia', $julia->getSetting('first_name'));
@@ -469,26 +471,33 @@
$this->assertSame('Test Org', $julia->getSetting('organization'));
// Some additional tests for the list input widget
- $browser->click('tbody tr:nth-child(4) a')
+ $browser->click('@table tbody tr:nth-child(4) a')
->on(new UserInfo())
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertListInputValue(['julia.roberts2@kolab.org'])
->addListEntry('invalid address')
- ->type('.input-group:nth-child(2) input', '@kolab.org');
+ ->type('.input-group:nth-child(2) input', '@kolab.org')
+ ->keys('.input-group:nth-child(2) input', '{enter}');
})
- ->click('button[type=submit]')
+ // TODO: Investigate why this click does not work, for now we
+ // submit the form with Enter key above
+ //->click('@general button[type=submit]')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertVisible('.input-group:nth-child(2) input.is-invalid')
->assertVisible('.input-group:nth-child(3) input.is-invalid')
->type('.input-group:nth-child(2) input', 'julia.roberts3@kolab.org')
- ->type('.input-group:nth-child(3) input', 'julia.roberts4@kolab.org');
+ ->type('.input-group:nth-child(3) input', 'julia.roberts4@kolab.org')
+ ->keys('.input-group:nth-child(3) input', '{enter}');
})
- ->click('button[type=submit]')
+ // TODO: Investigate why this click does not work, for now we
+ // submit the form with Enter key above
+ //->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
$aliases = $julia->aliases()->orderBy('alias')->get()->pluck('alias')->all();
+
$this->assertSame(['julia.roberts3@kolab.org', 'julia.roberts4@kolab.org'], $aliases);
});
}
@@ -585,7 +594,7 @@
->waitFor('@table tr:nth-child(2)')
->click('@table tr:nth-child(2) a') // joe@kolab.org
->on(new UserInfo())
- ->with('@form', function (Browser $browser) {
+ ->with('@general', function (Browser $browser) {
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
@@ -614,7 +623,7 @@
$browser->visit(new UserList())
->click('button.create-user')
->on(new UserInfo())
- ->with('@form', function (Browser $browser) {
+ ->with('@general', function (Browser $browser) {
$browser->whenAvailable('@packages', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 2)
->assertSeeIn('tbody tr:nth-child(1) .price', '8,91 CHF/month¹') // Groupware
@@ -648,7 +657,7 @@
$browser->visit('/user/' . $joe->id)
->on(new UserInfo())
- ->with('@form', function (Browser $browser) {
+ ->with('@general', function (Browser $browser) {
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
@@ -726,7 +735,7 @@
->click('#sku-input-beta')
->click('#sku-input-distlist');
})
- ->click('button[type=submit]')
+ ->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
$expected = [
@@ -741,8 +750,9 @@
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
+ ->waitFor('#sku-input-beta')
->click('#sku-input-beta')
- ->click('button[type=submit]')
+ ->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
$expected = [
diff --git a/src/webpack.mix.js b/src/webpack.mix.js
--- a/src/webpack.mix.js
+++ b/src/webpack.mix.js
@@ -14,14 +14,6 @@
const glob = require('glob');
const mix = require('laravel-mix');
-mix.webpackConfig({
- resolve: {
- alias: {
- 'jquery$': 'jquery/dist/jquery.slim.js',
- }
- }
-})
-
mix.js('resources/js/user/app.js', 'public/js/user.js').vue()
.js('resources/js/admin/app.js', 'public/js/admin.js').vue()
.js('resources/js/reseller/app.js', 'public/js/reseller.js').vue()

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 12:14 AM (20 h, 24 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821650
Default Alt Text
D2734.1775175243.diff (154 KB)

Event Timeline