snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

154
client/web/one/src/app.js Normal file
View File

@@ -0,0 +1,154 @@
import Vue from 'vue'
import './config-check'
import './console-splash'
import './plugins'
import './mixins'
import './components'
import './filters'
import store from './store'
import router from './router'
import { i18n, websocket } from '@cortezaproject/corteza-vue'
export default (options = {}) => {
options = {
el: '#app',
name: 'one',
template: '<router-view v-if="loaded && i18nLoaded" />',
data: () => ({
loaded: false,
i18nLoaded: false,
}),
async created () {
this.$i18n.i18next.on('initialized', () => {
this.i18nLoaded = true
})
this.websocket()
return this.$auth.vue(this).handle().then(async ({ user }) => {
// switch the favicon based on the settings
await this.$Settings.init({ api: this.$SystemAPI }).then(() => {
const icon = this.$Settings.attachment('ui.iconLogo') || '/icon.svg'
const favicon = document.getElementById('favicon')
if (favicon) {
favicon.href = icon
}
})
// switch the page directionality on body based on language
document.body.setAttribute('dir', this.textDirectionality(user.meta.preferredLanguage))
if (user.meta.preferredLanguage) {
// After user is authenticated, get his preferred language
// and instruct i18next to change it
this.$i18n.i18next.changeLanguage(user.meta.preferredLanguage)
}
// switch the webapp theme based on user preference
if (user.meta.theme) {
document.getElementsByTagName('html')[0].setAttribute('data-color-mode', user.meta.theme)
}
this.$store.dispatch('wfPrompts/update')
// Initialize notifications
this.$store.dispatch('notifications/fetchNotifications')
this.loaded = true
}).catch((err) => {
if (err instanceof Error && err.message === 'Unauthenticated') {
// user not logged-in,
// start with authentication flow
this.$auth.startAuthenticationFlow()
return
}
throw err
})
},
methods: {
/**
* Registers event listener for websocket messages and
* routes them depending on their type
*/
websocket () {
// cross-link auth & websocket so that ws can use the right access token
websocket.init(this)
// register event listener for workflow messages
this.$on('websocket-message', ({ data }) => {
const msg = JSON.parse(data)
switch (msg['@type']) {
case 'workflowSessionPrompt': {
this.$store.dispatch('wfPrompts/new', msg['@value'])
break
}
case 'workflowSessionResumed':
this.$store.dispatch('wfPrompts/clear', msg['@value'])
break
case 'notification':
this.$store.dispatch('notifications/addNotification', msg['@value'])
break
case 'notification.read':
this.$store.dispatch('notifications/updateReadNotification', msg['@value'])
break
case 'notification.unread':
this.$store.dispatch('notifications/updateUnreadNotification', msg['@value'])
break
case 'notification.read.all':
this.$store.dispatch('notifications/updateAllReadNotifications', msg['@value'])
break
case 'notification.unread.all':
this.$store.dispatch('notifications/updateAllUnreadNotifications', msg['@value'])
break
case 'notification.delete':
this.$store.dispatch('notifications/removeNotification', msg['@value'])
break
case 'error':
this.toastDanger('Websocket message with error', msg['@value'])
}
})
},
},
router,
store,
i18n: i18n(Vue,
{ app: 'corteza-webapp-one' },
'app',
'layout',
'navigation',
'notification',
'notifications',
),
// Any additional options we want to merge
...options,
}
const app = new Vue(options)
// Simple HMR acceptance
if (module.hot) {
module.hot.accept()
}
return app
}

View File

View File

@@ -0,0 +1,281 @@
<template>
<div class="app-selector d-flex flex-column h-100 py-2">
<div class="d-flex flex-column justify-content-center align-items-center mx-4 my-2">
<b-img
:src="logo"
class="logo px-2"
/>
<div class="search w-100 mx-auto my-4">
<c-input-search
v-model.trim="query"
:aria-label="$t('search')"
:placeholder="$t('search')"
:debounce="200"
/>
</div>
</div>
<div class="flex-fill overflow-auto">
<b-container class="h-100">
<draggable
v-if="filteredApps.length"
v-model="appList"
:disabled="!canCreateApplication || query || isMobileResolution"
group="apps"
class="h-100 w-100"
@end="onDrop"
>
<transition-group
name="apps"
tag="b-row"
class="d-flex flex-wrap align-items-stretch justify-content-center mx-2"
>
<b-col
v-for="app in filteredApps"
:key="app.applicationID"
cols="12"
md="6"
lg="4"
xl="3"
class="p-2"
>
<b-card
no-body
overlay
class="app h-100"
@mouseover="hovered = app.applicationID"
@mouseleave="hovered = undefined"
>
<div class="align-content-center d-flex flex-grow-1 flex-wrap">
<b-card-img
class="rounded-bottom thumbnail"
:src="logoUrl(app)"
:alt="app.unify.name || app.name"
/>
</div>
<h6 class="text-center my-4">
{{ app.unify.name || app.name }}
</h6>
<b-link
:data-test-id="app.name"
:disabled="!app.enabled"
:href="app.unify.url"
:target="openAppInNewTab(app.unify.url)"
:style="[{ cursor: `${app.enabled ? 'pointer': canCreateApplication ? 'grab' : 'default'}` }]"
class="stretched-link"
/>
</b-card>
</b-col>
</transition-group>
</draggable>
<div
v-else
class="d-flex justify-content-center align-items-center mt-5 w-100"
>
<h4 data-test-id="heading-no-apps">
{{ query ? $t('no-applications-found') : $t('no-applications') }}
</h4>
</div>
</b-container>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import Draggable from 'vuedraggable'
import { url, components } from '@cortezaproject/corteza-vue'
const { CInputSearch } = components
export default {
i18nOptions: {
namespaces: 'layout',
},
components: {
CInputSearch,
Draggable,
},
props: {
logo: {
type: String,
default: () => '',
},
},
data () {
return {
query: '',
appList: [],
canCreateApplication: false,
canPin: false,
hovered: undefined,
isMobileResolution: false,
steps: [
{ name: 'app-list', dynamic: false },
{ name: 'low-code', dynamic: true },
{ name: 'crm', dynamic: true },
{ name: 'reporter', dynamic: true },
{ name: 'workflow', dynamic: true },
{ name: 'profile', dynamic: false },
],
}
},
computed: {
...mapGetters({
apps: 'applications/unifyOnly',
}),
filteredApps () {
const query = (this.query || '').toUpperCase()
return this.query
? this.appList.filter(({ name }) => (name.toUpperCase()).includes(query))
: this.appList
},
},
watch: {
apps: {
immediate: true,
handler (apps) {
this.appList = apps
},
},
},
created () {
this.fetchEffective()
if (window.innerWidth < 576) {
this.isMobileResolution = true
}
},
methods: {
...mapActions({
reorderApp: 'applications/reorder',
pinApp: 'applications/pin',
unpinApp: 'applications/unpin',
}),
fetchEffective () {
this.$SystemAPI.permissionsEffective({ resource: 'application' })
.then(p => {
this.canCreateApplication = p.find(per => per.operation === 'application.create').allow || false
// this.canPin = p.find(({ resource, operation, allow }) => resource === 'system' && operation === 'application.flag.self').allow
})
},
handlePin (pin = true, applicationID) {
if (pin) {
this.unpinApp({ applicationID, ownedBy: this.$auth.user.userID })
} else {
this.pinApp({ applicationID, ownedBy: this.$auth.user.userID })
}
},
async onDrop () {
const applicationIDs = this.appList.map(({ applicationID }) => applicationID)
await this.reorderApp(applicationIDs)
},
logoUrl (app) {
if (!app.unify.logo) {
return 'applications/default-app.png'
}
const apiSystem = '/api/system'
const apiBaseUrl = (new URL(url.Make({ url: this.$SystemAPI.baseURL }))).toString()
// Properly handle uploaded logos
// but cut away only /api/system (without any potential base-url prefix)
if (app.unify.logo.startsWith(apiSystem)) {
// remove path from the URL
return apiBaseUrl.substring(0, apiBaseUrl.length - apiSystem.length) + app.unify.logo
}
// Provisioned app logos
return app.unify.logo
},
openAppInNewTab (route) {
return !route.includes('jitsi') ? '' : '_blank'
},
},
}
</script>
<style lang="scss" scoped>
.app-selector {
.logo {
max-height: 20vh;
max-width: 500px;
width: auto;
}
@media only screen and (max-width: 576px) {
.logo {
max-width: 100%;
}
}
.search {
max-width: 600px;
}
.app {
min-height: 13rem;
transition: all 0.2s ease;
box-shadow: 0;
top: 0;
.thumbnail {
max-width: 100%;
max-height: 150px;
object-fit: contain;
}
&:hover {
transition: all 0.2s ease;
box-shadow: 0px 4px 8px rgba(38, 38, 38, 0.2);
top: -2px;
}
}
.star {
position: absolute;
top: .2rem;
left: .2rem;
padding: 0;
margin: 0;
background-color: transparent;
border: none;
.star-icon {
fill: var(--warning);
width: 1.2rem;
height: 1.2rem;
}
}
.apps-leave-active {
position: absolute;
transition: opacity 0.25s ease;
}
.apps-enter, .apps-leave-to {
opacity: 0;
}
.apps-move {
transition: transform 0.25s ease;
}
}
</style>

View File

@@ -0,0 +1,39 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faUserCog,
faGripHorizontal,
faSearch,
faTimes,
faCheck,
faCheckDouble,
faBellSlash,
faBell as faBellSolid,
faEllipsisV,
} from '@fortawesome/free-solid-svg-icons'
import {
faQuestionCircle,
faUser,
faBell,
faTrashAlt,
faEnvelope,
faEnvelopeOpen,
} from '@fortawesome/free-regular-svg-icons'
library.add(
faQuestionCircle,
faUserCog,
faGripHorizontal,
faUser,
faSearch,
faTimes,
faCheck,
faCheckDouble,
faBell,
faBellSolid,
faBellSlash,
faEllipsisV,
faTrashAlt,
faEnvelope,
faEnvelopeOpen,
)

View File

@@ -0,0 +1,8 @@
import Vue from 'vue'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import PortalVue from 'portal-vue'
import './faIcons'
Vue.use(PortalVue)
Vue.component('font-awesome-icon', FontAwesomeIcon)
Vue.component('font-awesome-layers', FontAwesomeLayers)

View File

@@ -0,0 +1,8 @@
[
'CortezaAPI',
].forEach((cfg) => {
if (window[cfg] === undefined) {
throw new Error(`Missing or invalid configuration.
Make sure there is a public/config.js configuration file with window.${cfg} entry.`)
}
})

View File

@@ -0,0 +1,6 @@
/* eslint-disable no-undef */
const text = `%c${WEBAPP || 'Corteza Webapp'}, version: ${VERSION}, build time: ${BUILD_TIME}`
const style = 'background-color: #1397CB; color: white; padding: 3px 10px; border: 1px solid black; font: Courier'
/* eslint-disable no-console */
console.log(text, style)

View File

@@ -0,0 +1,40 @@
import Vue from 'vue'
import Router from 'vue-router'
import BootstrapVue from 'bootstrap-vue'
import c3catalogue from './components/C3'
import { components, i18n } from '@cortezaproject/corteza-vue'
import './components'
import './themes'
import './mixins'
const routes = [
{
path: '/c3',
name: 'c3',
component: components.C3.View,
props: { catalogue: c3catalogue },
},
{ path: '*', redirect: { name: 'c3' } },
]
Vue.use(Router)
Vue.use(BootstrapVue)
export default new Vue({
el: '#app',
name: 'DevEnv',
async created () {
document.body.setAttribute('dir', this.textDirectionality())
},
template: '<router-view/>',
router: new Router({
mode: 'history',
routes,
}),
i18n: i18n(Vue,
{ app: 'corteza-webapp-one' },
'app',
'layout',
'navigation',
),
})

View File

@@ -0,0 +1,6 @@
import Vue from 'vue'
import { filters } from '@cortezaproject/corteza-vue'
for (const n in filters) {
Vue.filter(n, filters[n])
}

View File

@@ -0,0 +1,4 @@
import app from './app'
import './themes'
app()

View File

@@ -0,0 +1,8 @@
import Vue from 'vue'
import resourceTranslations from './resource-translations'
import { mixins } from '@cortezaproject/corteza-vue'
Vue.mixin(mixins.toast)
Vue.mixin(resourceTranslations)

View File

@@ -0,0 +1,33 @@
export default {
computed: {
currentLanguage () {
return this.$i18n.i18next.language
},
},
methods: {
/**
* Returns directionality of the page according to the language
* - Arabic (ar)
* - Hebrew (he)
* - Pashto (pa)
* - Persian (fa)
* - Urdu (ur)
* - Sindhi (sd)
* @returns {string} rtl | ltr
*/
textDirectionality (language = this.currentLanguage) {
switch (language) {
case 'ar':
case 'he':
case 'pa':
case 'fa':
case 'ur':
case 'sd':
return 'rtl'
default:
return 'ltr'
}
},
},
}

View File

@@ -0,0 +1,35 @@
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import Router from 'vue-router'
import Vuex from 'vuex'
import VueNativeSock from 'vue-native-websocket'
import { plugins, websocket } from '@cortezaproject/corteza-vue'
Vue.use(BootstrapVue, {
BToast: {
// see https://bootstrap-vue.org/docs/components/toast#comp-ref-b-toast-props
autoHideDelay: 7000,
toaster: 'b-toaster-bottom-right',
},
BModal: {
noEnforceFocus: true,
},
})
Vue.use(plugins.Auth(), {
app: 'unify',
rootApp: true,
})
Vue.use(Router)
Vue.use(Vuex)
Vue.use(BootstrapVue)
Vue.use(plugins.CortezaAPI('system'))
Vue.use(plugins.CortezaAPI('compose'))
Vue.use(plugins.CortezaAPI('automation'))
Vue.use(plugins.Settings, { api: Vue.prototype.$SystemAPI })
Vue.use(VueNativeSock, websocket.endpoint(), websocket.config)

View File

@@ -0,0 +1,7 @@
import Router from 'vue-router'
import routes from './views/routes'
export default new Router({
mode: 'history',
routes,
})

View File

@@ -0,0 +1,60 @@
const state = {
set: [],
}
const getters = {
set (state) {
return state.set
},
unifyOnly (state) {
return state.set.filter(({ unify: { listed } = { listed: false } }) => listed)
},
}
const mutations = {
updateSet (state, set) {
state.set = set
},
}
/**
* @param localStorage
* @param api
*/
export default ({ api }) => {
return {
namespaced: true,
state,
getters,
mutations,
actions: {
async load ({ commit }) {
if (api && api.applicationList) {
return api.applicationList({ sort: 'weight', incFlags: 0 })
.then(({ set }) => commit('updateSet', set))
}
},
async reorder ({ dispatch }, applicationIDs) {
return api.applicationReorder({ applicationIDs }).then(() => {
return dispatch('load')
})
},
async pin ({ dispatch }, { applicationID, ownedBy }) {
return api.applicationFlagCreate({ applicationID, flag: 'pinned', ownedBy }).then(() => {
return dispatch('load')
}).catch(() => {})
},
async unpin ({ dispatch }, { applicationID, ownedBy }) {
return api.applicationFlagDelete({ applicationID, flag: 'pinned', ownedBy }).then(() => {
return dispatch('load')
}).catch(() => {})
},
},
}
}

View File

@@ -0,0 +1,29 @@
import Vue from 'vue'
import Vuex from 'vuex'
import applications from './applications'
import { store as cvStore } from '@cortezaproject/corteza-vue'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
applications: applications({ api: Vue.prototype.$SystemAPI }),
wfPrompts: {
namespaced: true,
...cvStore.wfPrompts({
api: Vue.prototype.$AutomationAPI,
ws: Vue.prototype.$socket,
webapp: 'one',
}),
},
notifications: {
namespaced: true,
...cvStore.notifications({
api: Vue.prototype.$SystemAPI,
}),
},
},
})
export default store

View File

@@ -0,0 +1 @@
import './index.scss'

View File

@@ -0,0 +1,3 @@
@import "./rtl.scss";
@import "./custom";
@import "./poppins.scss";

View File

@@ -0,0 +1,146 @@
// Fonts Path
$fonts_dir : './fonts/' !default;
@font-face {
font-family: 'Poppins-Bold';
src: url($fonts_dir + 'poppins/Poppins-Bold.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins-BlackItalic';
src: url($fonts_dir + 'poppins/Poppins-BlackItalic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-Italic';
src: url($fonts_dir + 'poppins/Poppins-Italic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-BoldItalic';
src: url($fonts_dir + 'poppins/Poppins-BoldItalic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-ExtraBoldItalic';
src: url($fonts_dir + 'poppins/Poppins-ExtraBoldItalic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-ExtraLightItalic';
src: url($fonts_dir + 'poppins/Poppins-ExtraLightItalic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-Black';
src: url($fonts_dir + 'poppins/Poppins-Black.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins-ExtraBold';
src: url($fonts_dir + 'poppins/Poppins-ExtraBold.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins-ExtraLight';
src: url($fonts_dir + 'poppins/Poppins-ExtraLight.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins-MediumItalic';
src: url($fonts_dir + 'poppins/Poppins-MediumItalic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-SemiBoldItalic';
src: url($fonts_dir + 'poppins/Poppins-SemiBoldItalic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-SemiBold';
src: url($fonts_dir + 'poppins/Poppins-SemiBold.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins-LightItalic';
src: url($fonts_dir + 'poppins/Poppins-LightItalic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-Regular';
src: url($fonts_dir + 'poppins/Poppins-Regular.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins-Medium';
src: url($fonts_dir + 'poppins/Poppins-Medium.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins-Light';
src: url($fonts_dir + 'poppins/Poppins-Light.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins-ThinItalic';
src: url($fonts_dir + 'poppins/Poppins-ThinItalic.ttf') format('truetype');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Poppins-Thin';
src: url($fonts_dir + 'poppins/Poppins-Thin.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}

View File

@@ -0,0 +1,9 @@
[dir="rtl"] {
text-align: right;
.popover,
ul.vs__dropdown-menu,
.dropdown-menu {
right: auto !important;
}
}

View File

@@ -0,0 +1 @@
import /* webpackChunkName: 'corteza-base' */ './corteza-base'

View File

@@ -0,0 +1,296 @@
<template>
<main>
<div class="logo">
<img
src="/applications/jitsi.png"
>
</div>
<div id="roomselection">
<span>{{ $t('toStart') }}</span>
<input
id="roomInputField"
v-model="roomName"
type="text"
:placeholder="$t('roomName')"
>
<button
data-test-id="button-create-room"
:disabled="jitsi || (cleanup(roomName).length === 0)"
@click="onCreate"
>
{{ $t('create') }}
</button>
<div
v-show="jitsi"
ref="jitsiInterface"
class="jitsiInterface"
/>
</div>
</main>
</template>
<script>
import Vue from 'vue'
import LoadScript from 'vue-plugin-load-script'
Vue.use(LoadScript)
Vue.loadScript('https://meet.jit.si/external_api.js')
const domain = 'meet.jit.si'
export default {
i18nOptions: {
namespaces: 'app',
keyPrefix: 'jitsi',
},
name: 'JitsiBridge',
params: {
user: {
type: Object,
required: true,
},
},
data () {
return {
channelID: null,
roomName: '',
jitsi: null,
channels: null,
}
},
destroyed () {
this.dispose()
},
methods: {
dispose () {
if (this.jitsi) {
this.jitsi.dispose()
this.jitsi = null
}
},
cleanup (str) {
return str.replace(/[^a-z0-9+]+/gi, '')
},
onJoin () {
this.open({
roomName: this.channelID,
userDisplayName: this.$auth.user.name || this.$auth.user.email,
})
},
onCreate () {
this.open({
roomName: this.roomName,
userDisplayName: this.$auth.user.name || this.$auth.user.email,
})
},
onClose () {
this.dispose()
},
removeJitsiAfterHangup () {
this.dispose()
},
open ({ roomName, userDisplayName } = {}) {
this.dispose()
const $t = (k) => this.$t(k)
/* eslint-disable no-undef */
this.jitsi = new JitsiMeetExternalAPI(domain, {
roomName: `crust_${this.cleanup(roomName || 'unnamed')}`,
width: '100%',
height: '100%',
parentNode: this.$refs.jitsiInterface,
interfaceConfigOverwrite: {
DEFAULT_BACKGROUND: '#232323',
SHOW_JITSI_WATERMARK: true,
SHOW_WATERMARK_FOR_GUESTS: false,
SHOW_BRAND_WATERMARK: false,
BRAND_WATERMARK_LINK: '',
SHOW_POWERED_BY: false,
DEFAULT_REMOTE_DISPLAY_NAME: $t('jitsi.defaultRemoteDisplayName'),
DEFAULT_LOCAL_DISPLAY_NAME: userDisplayName || $t('jitsi.defaultLocalDisplayName'),
TOOLBAR_BUTTONS: [
'microphone',
'camera',
'closedcaptions',
'desktop',
'fullscreen',
'fodeviceselection',
'hangup',
'profile',
'info',
'recording',
'settings',
'tileview',
'videoquality',
'filmstrip',
'invite',
'shortcuts',
],
SETTINGS_SECTIONS: [
'devices',
'language',
'moderator',
'profile',
'calendar',
],
},
})
// add an event listner to remove jitsi after the local party has hung up the call.
// this is to remove the page that mentions slack after the rating page.
this.jitsi.addEventListeners({
readyToClose: this.removeJitsiAfterHangup,
})
window.jitsi = this.jitsi
},
},
}
</script>
<style lang="scss" scoped>
main {
// we need explicitly overflow&height settings here because app's html/body does not scroll
// this way we keep the entire app layout in order
overflow: auto;
height: 100vh;
.logo {
text-align: center;
margin-top: 4rem;
img {
max-width: 200px;
}
}
.jitsiInterface {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: #232323;
& > iframe {
flex: 1 1 auto;
}
}
#roomselection {
max-width: 400px;
margin: 50px auto;
padding: 50px;
background: var(--white);
}
input {
height: 30px;
width: 100%;
border: 1px solid var(--secondary);
padding-left: 10px;
font-size: 14px;
display: block;
margin-top: 10px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
select {
height: 30px;
width: 100%;
margin-top: 10px;
background: transparent;
padding-left: 10px;
font-size: 14px;
-webkit-border-radius:0px;
-moz-border-radius:0px;
border-radius:0px;
-webkit-appearance:none;
-moz-appearance:none;
appearance:none;
border: 1px solid var(--secondary);
}
#roomdropdown::after {
border: 4px dashed transparent;
border-top: 4px solid var(--secondary);
content: "";
display: inline-block;
float: right;
margin-right: 10px;
margin-top: -15px;
}
select:focus,
input:focus {
outline: none;
}
button {
cursor: pointer;
background: transparent;
color: var(--primary);
font-size: 14px;
line-height: 38px;
text-decoration: none;
display: block;
width: 150px;
text-align: center;
height: 40px;
margin: 20px auto 0;
-webkit-transition: color .2s,background-color .2s;
transition: color .2s,background-color .2s;
border: 1px solid var(--primary);
&:hover {
border: 1px solid var(--primary);
background: var(--primary);
color: var(--white);
}
&:disabled {
cursor: not-allowed;
color: var(--secondary);
border-color: var(--secondary);
&:hover {
background: transparent;
}
}
}
h4 {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
text-align: center;
margin: 30px 0;
color: var(--secondary);
&:before,
&:after {
content: '';
border-top: 1px solid var(--secondary);
margin: 0 20px 0 0;
flex: 1 0 20px;
}
&:after {
margin: 0 0 0 20px;
}
}
}
</style>

View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@@ -0,0 +1,121 @@
<template>
<div class="d-flex flex-column w-100 h-viewport overflow-hidden">
<header
v-show="loaded"
>
<c-topbar
hide-app-selector
:settings="$Settings.get('ui.topbar', {})"
:labels="{
appMenu: $t('appMenu'),
helpForum: $t('help.forum'),
helpDocumentation: $t('help.documentation'),
helpFeedback: $t('help.feedback'),
helpVersion: $t('help.version'),
userSettingsLoggedInAs: $t('userSettings.loggedInAs', { user }),
userSettingsProfile: $t('userSettings.profile'),
userSettingsChangePassword: $t('userSettings.changePassword'),
userSettingsLogout: $t('userSettings.logout'),
userSettingsTheme: $t('userSettings.theme'),
lightTheme: $t('themes.labels.light'),
darkTheme: $t('themes.labels.dark'),
}"
/>
</header>
<main
v-show="loaded"
class="flex-fill overflow-hidden"
>
<c-app-selector
:logo="logo"
/>
</main>
<c-loader-logo
v-if="!loaded"
:logo="logo"
/>
<c-prompts />
<c-extend-session
v-if="isAutoLogoutEnabled"
:timeout="$Settings.get('auth.autoLogout.timeout')"
:labels="{
extend: $t('extendSession.labels.extend'),
warning: (countdownTime) => $t('extendSession.labels.warning', { countdownTime }),
}"
/>
<c-notification-sidebar v-if="!$Settings.get('ui.topbar', {}).hideNotifications" />
</div>
</template>
<script>
import { mapActions } from 'vuex'
import CAppSelector from '../components/CAppSelector'
import { components } from '@cortezaproject/corteza-vue'
const { CTopbar, CLoaderLogo, CPrompts, CExtendSession, CNotificationSidebar } = components
export default {
i18nOptions: {
namespaces: 'navigation',
},
components: {
CAppSelector,
CTopbar,
CLoaderLogo,
CPrompts,
CExtendSession,
CNotificationSidebar,
},
data () {
return {
loaded: false,
}
},
computed: {
icon () {
return this.$Settings.attachment('ui.iconLogo')
},
logo () {
return this.$Settings.attachment('ui.mainLogo')
},
user () {
const { user } = this.$auth
return user.name || user.handle || user.email || ''
},
isAutoLogoutEnabled () {
return this.$Settings.get('auth.autoLogout.enabled')
},
},
created () {
this.preloadApplications()
.then(() => {
setTimeout(() => {
this.loaded = true
}, 2000)
})
},
methods: {
...mapActions({
preloadApplications: 'applications/load',
}),
},
}
</script>
<style scoped>
.h-viewport {
height: 100vh;
height: 100dvh;
}
</style>

View File

@@ -0,0 +1,31 @@
/**
* Simple route generator
*
* @param name {String}
* @param path {String}
* @param component {String}
* @returns {Object}
*/
function r (name, path, component) {
return {
path,
name,
component: () => import('./' + component + '.vue'),
props: true,
// canReuse: false,
}
}
export default [
r('layout', '/', 'Layout'),
{
...r('bridge', '/bridge', 'Bridge/index'),
children: [
r('bridge-jitsi', 'jitsi', 'Bridge/Jitsi'),
],
},
// When everything else fails, go to root
{ path: '*', redirect: { name: 'layout' } },
]