Add files from webapp-one with client/web/one prefix
3
client/web/one/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 8
|
||||
16
client/web/one/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
26
client/web/one/.eslintrc.js
Normal file
@@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
root: false,
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/recommended',
|
||||
'@vue/standard',
|
||||
],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'vue/component-name-in-template-casing': ['error', 'kebab-case'],
|
||||
'vue/no-v-html': 'off',
|
||||
// @todo remove this asap - add enough tests first
|
||||
'vue/name-property-casing': 'off',
|
||||
'vue/prop-name-casing': 'off',
|
||||
'vue/order-in-components': ['error'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
},
|
||||
}
|
||||
20
client/web/one/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
/.act*
|
||||
/workflow
|
||||
37
client/web/one/Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
# build-stage
|
||||
FROM node:12.14-alpine as build-stage
|
||||
|
||||
ENV PATH /app/node_modules/.bin:$PATH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
|
||||
RUN yarn install
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN yarn build
|
||||
|
||||
|
||||
# deploy stage
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY CONTRIBUTING.* DCO LICENSE README.* ./
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --start-period=10s --timeout=30s \
|
||||
CMD wget --quiet --tries=1 --spider "http://127.0.0.1:80/config.js" || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
39
client/web/one/Makefile
Normal file
@@ -0,0 +1,39 @@
|
||||
.PHONY: dep test build release upload
|
||||
|
||||
YARN_FLAGS ?= --non-interactive --no-progress --silent --emoji false
|
||||
YARN = yarn $(YARN_FLAGS)
|
||||
|
||||
REPO_NAME ?= corteza-webapp-one
|
||||
|
||||
BUILD_FLAVOUR ?= corteza
|
||||
BUILD_FLAGS ?= --production
|
||||
BUILD_DEST_DIR = dist
|
||||
BUILD_TIME ?= $(shell date +%FT%T%z)
|
||||
BUILD_VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||
BUILD_NAME = $(REPO_NAME)-$(BUILD_VERSION)
|
||||
|
||||
RELEASE_NAME = $(BUILD_NAME).tar.gz
|
||||
RELEASE_EXTRA_FILES ?= README.md LICENSE CONTRIBUTING.md DCO
|
||||
RELEASE_PKEY ?= .upload-rsa
|
||||
|
||||
dep:
|
||||
$(YARN) install
|
||||
|
||||
test:
|
||||
$(YARN) lint
|
||||
$(YARN) test:unit
|
||||
|
||||
build:
|
||||
$(YARN) build $(BUILD_FLAGS)
|
||||
|
||||
release:
|
||||
@ cp $(RELEASE_EXTRA_FILES) $(BUILD_DEST_DIR)
|
||||
@ tar -C $(BUILD_DEST_DIR) -czf $(RELEASE_NAME) $(dir $(BUILD_DEST_DIR))
|
||||
|
||||
upload: $(RELEASE_PKEY)
|
||||
@ echo "put *.tar.gz" | sftp -q -o "StrictHostKeyChecking no" -i $(RELEASE_PKEY) $(RELEASE_SFTP_URI)
|
||||
@ rm -f $(RELEASE_PKEY)
|
||||
|
||||
$(RELEASE_PKEY):
|
||||
@ echo $(RELEASE_SFTP_KEY) | base64 -d > $(RELEASE_PKEY)
|
||||
@ chmod 0400 $@
|
||||
13
client/web/one/babel.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app',
|
||||
],
|
||||
|
||||
env: {
|
||||
test: {
|
||||
plugins: [
|
||||
[ 'istanbul', { useInlineSourceMaps: false } ],
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
42
client/web/one/entrypoint.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
if [ ! -z "${1:-}" ]; then
|
||||
exec "$@"
|
||||
else
|
||||
# check if config.js is present (via volume)
|
||||
# or if it's missing
|
||||
if [ ! -f "./config.js" ]; then
|
||||
# config.js missing, generate it
|
||||
if [ ! -z "${CONFIGJS:-}" ]; then
|
||||
# use $CONFIGJS variable that might be passed to the container:
|
||||
# --env CONFIGJS="$(cat public/config.example.js)"
|
||||
echo "${CONFIGJS}" > ./config.js
|
||||
else
|
||||
# Try to guess where the API is located by using DOMAIN or VIRTUAL_HOST and prefix it with "api."
|
||||
API_HOST=${API_HOST:-"api.${VIRTUAL_HOST:-"${DOMAIN:-"local.cortezaproject.org"}"}"}
|
||||
API_BASEURL=${API_FULL_URL:-"//${API_HOST}"}
|
||||
|
||||
echo "window.CortezaAPI = '${API_BASEURL}'" > ./config.js
|
||||
fi
|
||||
fi
|
||||
|
||||
BASE_PATH=${BASE_PATH:-"/"}
|
||||
if [ $BASE_PATH != "/" ]; then
|
||||
BASE_PATH_LENGTH=${#BASE_PATH}
|
||||
BASE_PATH_LAST_CHAR=${BASE_PATH:BASE_PATH_LENGTH-1:1}
|
||||
|
||||
if [ $BASE_PATH_LAST_CHAR != "/" ]; then
|
||||
BASE_PATH="$BASE_PATH/"
|
||||
fi
|
||||
fi
|
||||
|
||||
sed -i "s|<base href=/ >|<base href=\"$BASE_PATH\">|g" ./index.html
|
||||
sed -i "s|<base href=\"/\">|<base href=\"$BASE_PATH\">|g" ./index.html
|
||||
|
||||
sed -i "s|{{BASE_PATH}}|$BASE_PATH|g" /etc/nginx/nginx.conf
|
||||
|
||||
|
||||
nginx -g "daemon off;"
|
||||
fi
|
||||
46
client/web/one/nginx.conf
Normal file
@@ -0,0 +1,46 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /dev/stdout warn;
|
||||
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format json escape=json
|
||||
'{'
|
||||
'"@timestamp":"$time_iso8601",'
|
||||
'"remote_addr":"$remote_addr",'
|
||||
'"request":"$request",'
|
||||
'"status":$status,'
|
||||
'"body_bytes_sent":$body_bytes_sent,'
|
||||
'"request_time":$request_time,'
|
||||
'"http_referrer":"$http_referer",'
|
||||
'"http_user_agent":"$http_user_agent"'
|
||||
'}';
|
||||
|
||||
access_log /dev/stdout json;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 300;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
|
||||
index index.html;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location {{BASE_PATH}} {
|
||||
try_files $uri {{BASE_PATH}}index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
client/web/one/package.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "corteza-webapp-one",
|
||||
"description": "Corteza One WebApp",
|
||||
"version": "2022.9.2",
|
||||
"license": "Apache-2.0",
|
||||
"contributors": [
|
||||
"Denis Arh <denis.arh@gmail.com>"
|
||||
],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev-env": "vue-cli-service serve src/dev-env.js",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"lint": "vue-cli-service lint",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test:unit:cc": "nyc vue-cli-service test:unit",
|
||||
"cdeps": "yarn upgrade @cortezaproject/corteza-js @cortezaproject/corteza-vue"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "yarn lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cortezaproject/corteza-js": "^2022.9.2",
|
||||
"@cortezaproject/corteza-vue": "^2022.9.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.21",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.10.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.10.1",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.9",
|
||||
"acorn": "^7.1.1",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"kind-of": "^6.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.6",
|
||||
"postcss-rtl": "^1.7.3",
|
||||
"resolve-url-loader": "^3.1.2",
|
||||
"set-value": "^4.0.1",
|
||||
"vue": "2.6.14",
|
||||
"vue-native-websocket": "^2.0.15",
|
||||
"vue-plugin-load-script": "^1.2.0",
|
||||
"vue-router": "3.1.6",
|
||||
"vue-select": "^3.18.3",
|
||||
"vue-tour": "^2.0.0",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuex": "3.1.3",
|
||||
"webpack": "4.42.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.10.0",
|
||||
"@vue/cli-plugin-eslint": "^4.1.2",
|
||||
"@vue/cli-plugin-unit-mocha": "^3.10.0",
|
||||
"@vue/cli-service": "^3.10.0",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"babel-eslint": "^10.0.2",
|
||||
"babel-plugin-istanbul": "^5.2.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-plugin-vue": "^5.1.2",
|
||||
"flush-promises": "^1.0.2",
|
||||
"null-loader": "^4.0.1",
|
||||
"nyc": "^14.1.1",
|
||||
"sass": "^1.49.9",
|
||||
"sass-loader": "^10",
|
||||
"sinon": "^7.3.2",
|
||||
"vue-template-compiler": "2.6.14"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/**/node-forge": "^0.10.0",
|
||||
"**/**/serialize-javascript": "^3.1.0",
|
||||
"**/**/moment": "2.29.2"
|
||||
},
|
||||
"nyc": {
|
||||
"all": true,
|
||||
"reporter": [
|
||||
"lcov",
|
||||
"text"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.{js,vue}"
|
||||
],
|
||||
"exclude": [
|
||||
"**/index.js",
|
||||
"**/*.spec.js"
|
||||
],
|
||||
"extension": [
|
||||
".js",
|
||||
".vue"
|
||||
],
|
||||
"check-coverage": true,
|
||||
"per-file": true,
|
||||
"branches": 0,
|
||||
"lines": 0,
|
||||
"functions": 0,
|
||||
"statements": 0
|
||||
}
|
||||
}
|
||||
6
client/web/one/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
'postcss-rtl': {},
|
||||
},
|
||||
}
|
||||
1
client/web/one/public/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
config.js
|
||||
BIN
client/web/one/public/applications/admin-area.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
client/web/one/public/applications/default-app.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
client/web/one/public/applications/default_icon.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
client/web/one/public/applications/default_logo.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
client/web/one/public/applications/discovery.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
client/web/one/public/applications/google-maps.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
client/web/one/public/applications/jitsi.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
client/web/one/public/applications/low-code-crm-app.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
BIN
client/web/one/public/applications/low-code-platform.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
client/web/one/public/applications/messaging.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
client/web/one/public/applications/privacy.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
client/web/one/public/applications/reporter.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
client/web/one/public/applications/urban-data-platform.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
client/web/one/public/applications/video-conference.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
client/web/one/public/applications/workflows.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
12
client/web/one/public/config.example.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Corteza API location
|
||||
window.CortezaAPI = 'https://api.cortezaproject.your-domain.tld';
|
||||
|
||||
// CortezaAuth can be autoconfigured by replacing /api with /auth in CortezaAPI
|
||||
// or by appending /auth to the end of CortezaAPI string
|
||||
// When this is not possible and your configuration is more exotic you can set it
|
||||
// explicitly:
|
||||
// window.CortezaAuth = 'https://api.cortezaproject.your-domain.tld/auth';
|
||||
|
||||
// Configure CortezaWebapp when your web applications are not placed on the root.
|
||||
// This is autoconfigured from the value of <base> tag href attribute in most cases.
|
||||
// window.CortezaWebapp = 'https://cortezaproject.your-domain.tld';
|
||||
BIN
client/web/one/public/favicon32x32.png
Normal file
|
After Width: | Height: | Size: 702 B |
31
client/web/one/public/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!--
|
||||
base location is root by default for all webapps
|
||||
|
||||
href value should be modified when app is placed under subdir
|
||||
-->
|
||||
<base href="/" />
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" id="favicon">
|
||||
<link rel="preload" href="<%= BASE_URL %>config.js" as="script" />
|
||||
<title><%= FLAVOUR %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>
|
||||
JavaScript is disabled in your browser!
|
||||
</strong>
|
||||
<p>
|
||||
We're sorry but Corteza doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.
|
||||
</p>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="<%= BASE_URL %>config.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
114
client/web/one/src/app.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import './config-check'
|
||||
import './console-splash'
|
||||
|
||||
import './plugins'
|
||||
import './mixins'
|
||||
import './components'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
|
||||
import { i18n, websocket } from '@cortezaproject/corteza-vue'
|
||||
|
||||
export default (options = {}) => {
|
||||
options = {
|
||||
el: '#app',
|
||||
name: 'one',
|
||||
template: '<div v-if="loaded && i18nLoaded"><router-view/></div>',
|
||||
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
i18nLoaded: false,
|
||||
}),
|
||||
|
||||
async created () {
|
||||
this.$i18n.i18next.on('loaded', () => {
|
||||
this.i18nLoaded = true
|
||||
})
|
||||
|
||||
return this.$auth.vue(this).handle().then(({ user }) => {
|
||||
// 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)
|
||||
}
|
||||
|
||||
this.$store.dispatch('wfPrompts/update')
|
||||
|
||||
return this.$Settings.init({ api: this.$SystemAPI }).finally(() => {
|
||||
this.websocket()
|
||||
|
||||
this.loaded = true
|
||||
|
||||
// This bit removes code from the query params
|
||||
//
|
||||
// Vue router can't be used here because when on any child route there is no
|
||||
// guarantee that the route has loaded and so it may redirect us to the root page.
|
||||
//
|
||||
// @todo dig a bit deeper if there is a better vue-like solution; atm none were ok.
|
||||
const url = new URL(window.location.href)
|
||||
if (url.searchParams.get('code')) {
|
||||
url.searchParams.delete('code')
|
||||
window.location.replace(url.toString())
|
||||
}
|
||||
})
|
||||
}).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 'error':
|
||||
console.error('websocket message with error', msg['@value'])
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
router,
|
||||
store,
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-one' },
|
||||
'app',
|
||||
'layout',
|
||||
'navigation',
|
||||
),
|
||||
|
||||
// Any additional options we want to merge
|
||||
...options,
|
||||
}
|
||||
|
||||
return new Vue(options)
|
||||
}
|
||||
0
client/web/one/src/components/C3.js
Normal file
346
client/web/one/src/components/CAppSelector.vue
Normal file
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div
|
||||
class="app-selector d-flex flex-column h-100"
|
||||
>
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<b-img
|
||||
:src="logo"
|
||||
class="logo px-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search w-100 mx-auto my-3 px-5">
|
||||
<div class="flex-grow-1 mt-1">
|
||||
<c-input-search
|
||||
v-model.trim="query"
|
||||
data-v-onboarding="app-list"
|
||||
: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"
|
||||
group="apps"
|
||||
class="h-100 w-100"
|
||||
:disabled="!canCreateApplication || query || isMobileResolution"
|
||||
@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"
|
||||
sm="6"
|
||||
md="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
class="p-0 mb-3 mt-1"
|
||||
:data-v-onboarding="getStepName(app.unify.url)"
|
||||
>
|
||||
<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>
|
||||
|
||||
<b-card-text
|
||||
class="text-center my-4 h6"
|
||||
>
|
||||
{{ app.unify.name || app.name }}
|
||||
</b-card-text>
|
||||
|
||||
<b-link
|
||||
:data-test-id="app.name"
|
||||
:disabled="!app.enabled"
|
||||
:href="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 w-100"
|
||||
>
|
||||
<h4
|
||||
data-test-id="heading-no-apps"
|
||||
class="mt-5"
|
||||
>
|
||||
{{ $t('no-applications') }}
|
||||
</h4>
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
|
||||
<portal
|
||||
to="topbar-help-dropdown"
|
||||
>
|
||||
<b-dropdown-item
|
||||
data-test-id="dropdown-helper-tour"
|
||||
@click="$refs.tour.onStartClick()"
|
||||
>
|
||||
{{ $t('start-tour') }}
|
||||
</b-dropdown-item>
|
||||
</portal>
|
||||
|
||||
<tour-start
|
||||
@start="startTour"
|
||||
/>
|
||||
|
||||
<tour
|
||||
ref="tour"
|
||||
name="app-list"
|
||||
:steps="filteredSteps"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { components, url } from '@cortezaproject/corteza-vue'
|
||||
const { Tour, TourStart, CInputSearch } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'layout',
|
||||
},
|
||||
|
||||
components: {
|
||||
Draggable,
|
||||
Tour,
|
||||
TourStart,
|
||||
CInputSearch,
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
filteredSteps () {
|
||||
return this.steps.filter(step => {
|
||||
if (step.dynamic) {
|
||||
return this.filteredApps.some(app => {
|
||||
return this.getStepName(app.unify.url) === step.name
|
||||
})
|
||||
}
|
||||
return true
|
||||
}).map(s => { return s.name })
|
||||
},
|
||||
},
|
||||
|
||||
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',
|
||||
}),
|
||||
|
||||
getStepName (url) {
|
||||
switch (url) {
|
||||
case 'compose/':
|
||||
return 'low-code'
|
||||
case 'compose/ns/crm/pages':
|
||||
return 'crm'
|
||||
case 'reporter/':
|
||||
return 'reporter'
|
||||
case 'workflow/':
|
||||
return 'workflow'
|
||||
}
|
||||
},
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
startTour () {
|
||||
this.$refs.tour.onStart()
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-selector {
|
||||
.logo {
|
||||
max-height: 25vh;
|
||||
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;
|
||||
margin: 0 0.625rem;
|
||||
|
||||
.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: $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>
|
||||
21
client/web/one/src/components/faIcons.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faUserCog,
|
||||
faGripHorizontal,
|
||||
faSearch,
|
||||
faTimes,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
faQuestionCircle,
|
||||
faUser,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faQuestionCircle,
|
||||
faUserCog,
|
||||
faGripHorizontal,
|
||||
faUser,
|
||||
faSearch,
|
||||
faTimes,
|
||||
)
|
||||
8
client/web/one/src/components/index.js
Normal 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)
|
||||
8
client/web/one/src/config-check.js
Normal 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.`)
|
||||
}
|
||||
})
|
||||
6
client/web/one/src/console-splash.js
Normal 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)
|
||||
40
client/web/one/src/dev-env.js
Normal 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',
|
||||
),
|
||||
})
|
||||
4
client/web/one/src/main.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import app from './app'
|
||||
import './themes'
|
||||
|
||||
app()
|
||||
5
client/web/one/src/mixins/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import resourceTranslations from './resource-translations'
|
||||
|
||||
Vue.mixin(resourceTranslations)
|
||||
33
client/web/one/src/mixins/resource-translations.js
Normal 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'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
34
client/web/one/src/plugins/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import Vue from 'vue'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import Router from 'vue-router'
|
||||
import Vuex from 'vuex'
|
||||
import VueTour from 'vue-tour'
|
||||
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',
|
||||
},
|
||||
})
|
||||
|
||||
Vue.use(plugins.Auth(), {
|
||||
app: 'unify',
|
||||
rootApp: true,
|
||||
})
|
||||
|
||||
Vue.use(VueTour)
|
||||
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)
|
||||
7
client/web/one/src/router.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Router from 'vue-router'
|
||||
import routes from './views/routes'
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
60
client/web/one/src/store/applications.js
Normal 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(() => {})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
23
client/web/one/src/store/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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',
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default store
|
||||
105
client/web/one/src/themes/corteza-base/custom.scss
Normal file
@@ -0,0 +1,105 @@
|
||||
html {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline-color: $primary;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b-toaster.b-toaster-bottom-right {
|
||||
bottom: 75px;
|
||||
}
|
||||
|
||||
.grab {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
thead th,
|
||||
legend,
|
||||
label,
|
||||
.btn {
|
||||
font-family: $font-medium;
|
||||
}
|
||||
|
||||
strong,
|
||||
b,
|
||||
.font-weight-bold {
|
||||
font-family: $font-semibold;
|
||||
}
|
||||
|
||||
.v-select {
|
||||
min-width: 250px;
|
||||
|
||||
.vs__search {
|
||||
margin: 0;
|
||||
|
||||
&:focus {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vs__dropdown-toggle {
|
||||
padding: 0.375rem;
|
||||
border-width: 2px;
|
||||
border-color: $light;
|
||||
|
||||
.vs__selected {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vs__clear,
|
||||
.vs__open-indicator {
|
||||
fill: $gray-900;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
// Supporting CSS to improve print-to-PDF option
|
||||
@media print {
|
||||
@page {
|
||||
size: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
header, footer, aside, nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.block {
|
||||
break-inside: avoid;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
679
client/web/one/src/themes/corteza-base/icons.scss
Normal file
@@ -0,0 +1,679 @@
|
||||
//paste the rest of icomoon here
|
||||
|
||||
$icon-grid-interface-close: "\e920";
|
||||
$icon-grid-interface-absent: "\e921";
|
||||
$icon-grid-interface-open: "\e922";
|
||||
$icon-search: "\e91f";
|
||||
$icon-fatlock: "\e91d";
|
||||
$icon-Fichier-2: "\e91e";
|
||||
$icon-settings-horizontal: "\e914";
|
||||
$icon-tabs: "\e915";
|
||||
$icon-menu4: "\e916";
|
||||
$icon-message-circle-left-speak: "\e917";
|
||||
$icon-message-circle-right-speak: "\e918";
|
||||
$icon-message-circle-right: "\e919";
|
||||
$icon-menu3: "\e91a";
|
||||
$icon-menu-dots: "\e900";
|
||||
$icon-images: "\e91b";
|
||||
$icon-camera: "\e91c";
|
||||
$icon-location: "\e947";
|
||||
$icon-location2: "\e948";
|
||||
$icon-undo: "\e965";
|
||||
$icon-redo: "\e966";
|
||||
$icon-bubble1: "\e96c";
|
||||
$icon-bubbles: "\e96d";
|
||||
$icon-bubbles2: "\e96f";
|
||||
$icon-bubble21: "\e970";
|
||||
$icon-bubbles3: "\e972";
|
||||
$icon-bubbles4: "\e973";
|
||||
$icon-zoom: "\e986";
|
||||
$icon-lock: "\e98f";
|
||||
$icon-menu1: "\e9be";
|
||||
$icon-earth: "\e9ca";
|
||||
$icon-cross: "\ea0f";
|
||||
$icon-checkmark: "\ea10";
|
||||
$icon-circle-up: "\ea41";
|
||||
$icon-circle-right: "\ea42";
|
||||
$icon-circle-down: "\ea43";
|
||||
$icon-circle-left: "\ea44";
|
||||
$icon-checkbox-checked: "\ea52";
|
||||
$icon-checkbox-unchecked: "\ea53";
|
||||
$icon-radio-checked: "\ea54";
|
||||
$icon-radio-checked2: "\ea55";
|
||||
$icon-google: "\ea88";
|
||||
$icon-facebook: "\ea90";
|
||||
$icon-youtube: "\ea9d";
|
||||
$icon-files-empty: "\e925";
|
||||
$icon-bell: "\e951";
|
||||
$icon-bubble: "\e96b";
|
||||
$icon-bubble2: "\e96e";
|
||||
$icon-user: "\e971";
|
||||
$icon-equalizer: "\e992";
|
||||
$icon-menu2: "\e9bd";
|
||||
$icon-info2: "\ea0c";
|
||||
$icon-play3: "\ea1c";
|
||||
$icon-indent-decrease: "\ea7c";
|
||||
$icon-align-justify: "\e903";
|
||||
$icon-bell2: "\e901";
|
||||
$icon-chevron-down: "\e904";
|
||||
$icon-chevron-left: "\e905";
|
||||
$icon-chevron-right: "\e906";
|
||||
$icon-chevron-up: "\e907";
|
||||
$icon-copy: "\e908";
|
||||
$icon-film: "\e909";
|
||||
$icon-git-commit: "\e90a";
|
||||
$icon-info: "\e90b";
|
||||
$icon-menu: "\e90c";
|
||||
$icon-message-circle: "\e90d";
|
||||
$icon-more-vertical: "\e90e";
|
||||
$icon-plus: "\e90f";
|
||||
$icon-plus-circle: "\e910";
|
||||
$icon-square: "\e911";
|
||||
$icon-terminal: "\e912";
|
||||
$icon-user2: "\e902";
|
||||
$icon-x: "\e913";
|
||||
|
||||
@font-face
|
||||
{
|
||||
font-family: '#{$icomoon-font-family}';
|
||||
src:
|
||||
url('#{$icomoon-font-path}/#{$icomoon-font-family}.ttf?yjnewp') format('truetype'),
|
||||
url('#{$icomoon-font-path}/#{$icomoon-font-family}.woff?yjnewp') format('woff'),
|
||||
url('#{$icomoon-font-path}/#{$icomoon-font-family}.svg?yjnewp##{$icomoon-font-family}') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[class^="icon-"],
|
||||
[class*=" icon-"]
|
||||
{
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: '#{$icomoon-font-family}' !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
color: $secondary;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-grid-interface-close
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-grid-interface-close;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-grid-interface-absent
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-grid-interface-absent;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-grid-interface-open
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-grid-interface-open;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-search
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-search;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-fatlock
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-fatlock;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-Fichier-2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-Fichier-2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-settings-horizontal
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-settings-horizontal;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-tabs
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-tabs;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-menu4
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-menu4;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-message-circle-left-speak
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-message-circle-left-speak;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-message-circle-right-speak
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-message-circle-right-speak;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-message-circle-right
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-message-circle-right;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-menu3
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-menu3;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-menu-dots
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-menu-dots;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-images
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-images;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-camera
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-camera;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-location
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-location;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-location2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-location2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-undo
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-undo;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-redo
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-redo;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bubble1
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bubble1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bubbles
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bubbles;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bubbles2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bubbles2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bubble21
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bubble21;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bubbles3
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bubbles3;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bubbles4
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bubbles4;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-zoom
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-zoom;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-lock
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-lock;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-menu1
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-menu1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-earth
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-earth;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-cross
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-cross;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-checkmark
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-checkmark;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-circle-up
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-circle-up;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-circle-right
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-circle-right;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-circle-down
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-circle-down;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-circle-left
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-circle-left;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-checkbox-checked
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-checkbox-checked;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-checkbox-unchecked
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-checkbox-unchecked;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-radio-checked
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-radio-checked;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-radio-checked2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-radio-checked2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-google
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-google;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-facebook
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-facebook;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-youtube
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-youtube;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-files-empty
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-files-empty;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bell
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bell;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bubble
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bubble;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bubble2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bubble2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-user
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-user;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-equalizer
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-equalizer;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-menu2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-menu2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-info2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-info2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-play3
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-play3;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-indent-decrease
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-indent-decrease;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-align-justify
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-align-justify;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-bell2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-bell2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-chevron-down
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-chevron-down;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-chevron-left
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-chevron-left;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-chevron-right
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-chevron-right;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-chevron-up
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-chevron-up;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-copy
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-copy;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-film
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-film;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-git-commit
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-git-commit;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-info
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-info;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-menu
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-menu;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-message-circle
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-message-circle;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-more-vertical
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-more-vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-plus
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-plus;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-plus-circle
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-plus-circle;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-square
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-square;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-terminal
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-terminal;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-user2
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-user2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-x
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: $icon-x;
|
||||
}
|
||||
}
|
||||
1
client/web/one/src/themes/corteza-base/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import './index.scss'
|
||||
6
client/web/one/src/themes/corteza-base/index.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
@import "./rtl.scss";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import '~bootstrap-vue/src/index.scss';
|
||||
@import '~vue-select/src/scss/vue-select.scss';
|
||||
@import "./custom";
|
||||
@import "./poppins.scss";
|
||||
143
client/web/one/src/themes/corteza-base/poppins.scss
Normal file
@@ -0,0 +1,143 @@
|
||||
@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;
|
||||
}
|
||||
14
client/web/one/src/themes/corteza-base/rtl.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
[dir] {
|
||||
margin: 0;
|
||||
background-color: $body-bg;
|
||||
}
|
||||
|
||||
[dir="rtl"] {
|
||||
text-align: right;
|
||||
|
||||
.popover,
|
||||
ul.vs__dropdown-menu,
|
||||
.dropdown-menu {
|
||||
right: auto !important;
|
||||
}
|
||||
}
|
||||
68
client/web/one/src/themes/corteza-base/variables.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
$wideminwidth: 768px !default;
|
||||
|
||||
// Paths
|
||||
$fonts_dir : '/fonts/' !default;
|
||||
$icomoon-font-path: $fonts_dir + 'icomoon' !default;
|
||||
$icomoon-font-family: "icomoon" !default;
|
||||
|
||||
// Typography
|
||||
$font-light: 'Poppins-Light' !default;
|
||||
$font-regular: 'Poppins-Regular' !default;
|
||||
$font-medium: 'Poppins-Medium' !default;
|
||||
$font-semibold: 'Poppins-Semibold' !default;
|
||||
$font-bold: 'Poppins-Bold' !default;
|
||||
$font-size-base: 0.9rem !default;
|
||||
|
||||
$font-family-base: $font-regular !default;
|
||||
$headings-font-family: $font-semibold !default;
|
||||
|
||||
// Color system
|
||||
$white: #FFF !default;
|
||||
$black: #000 !default;
|
||||
$primary: #0B344E !default;
|
||||
$secondary: #758D9B !default;
|
||||
$success: #43AA8B !default;
|
||||
$warning: #F5D380 !default;
|
||||
$danger: #E24646 !default;
|
||||
$light: #E4E9EF !default;
|
||||
$dark: #162425 !default;
|
||||
|
||||
$gray-200: #F3F3F5 !default;
|
||||
|
||||
// Options
|
||||
$enable-rounded: true !default;
|
||||
$enable-gradients: false !default;
|
||||
$enable-responsive-font-sizes: true !default;
|
||||
|
||||
// Body
|
||||
$body-bg: $gray-200 !default;
|
||||
|
||||
// Card
|
||||
$card-border-radius: 1rem !default;
|
||||
$card-border-width: 0 !default;
|
||||
$card-cap-bg: $gray-200 !default;
|
||||
|
||||
// Buttons
|
||||
$btn-font-size: $font-size-base !default;
|
||||
$btn-padding-y: 0.25em !default;
|
||||
$btn-padding-x: 1em !default;
|
||||
|
||||
$btn-focus-width: 0.2rem !default;
|
||||
$btn-focus-box-shadow: none !default;
|
||||
$btn-active-box-shadow: none !default;
|
||||
|
||||
$btn-font-size-lg: 16px !default;
|
||||
$btn-font-family: $font-semibold !default;
|
||||
|
||||
$topbar-height: 64px !default;
|
||||
|
||||
|
||||
// Forms
|
||||
$input-font-size: $font-size-base !default;
|
||||
|
||||
$input-border-color: $light !default;
|
||||
$input-border-width: 2px !default;
|
||||
|
||||
$input-btn-focus-width: 0 !default;
|
||||
$input-focus-box-shadow: none !default;
|
||||
$input-focus-border-color: $primary !default;
|
||||
1
client/web/one/src/themes/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import /* webpackChunkName: 'corteza-base' */ './corteza-base'
|
||||
296
client/web/one/src/views/Bridge/Jitsi.vue
Normal 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: $white;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
border: 1px solid $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 $secondary;
|
||||
}
|
||||
|
||||
#roomdropdown::after {
|
||||
border: 4px dashed transparent;
|
||||
border-top: 4px solid $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: $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 $primary;
|
||||
&:hover {
|
||||
border: 1px solid $primary;
|
||||
background: $primary;
|
||||
color: #ffffff;
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: $secondary;
|
||||
border-color: $secondary;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
color: $secondary;
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
border-top: 1px solid $secondary;
|
||||
margin: 0 20px 0 0;
|
||||
flex: 1 0 20px;
|
||||
}
|
||||
&:after {
|
||||
margin: 0 0 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
3
client/web/one/src/views/Bridge/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
123
client/web/one/src/views/Layout.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex flex-column w-100 vh-100"
|
||||
>
|
||||
<header
|
||||
v-show="loaded"
|
||||
class="mw-100"
|
||||
>
|
||||
<c-topbar
|
||||
hide-app-selector
|
||||
:sidebar-pinned="pinned"
|
||||
:settings="$Settings.get('ui.topbar', {})"
|
||||
:labels="{
|
||||
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'),
|
||||
}"
|
||||
>
|
||||
<template #help-dropdown>
|
||||
<portal-target name="topbar-help-dropdown" />
|
||||
</template>
|
||||
</c-topbar>
|
||||
</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
|
||||
:hide-toasts="!loaded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
import CAppSelector from '../components/CAppSelector'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CTopbar, CLoaderLogo, CPrompts } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'navigation',
|
||||
},
|
||||
|
||||
components: {
|
||||
CAppSelector,
|
||||
CTopbar,
|
||||
CLoaderLogo,
|
||||
CPrompts,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
loaded: false,
|
||||
|
||||
pinned: 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 || ''
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
icon: {
|
||||
immediate: true,
|
||||
handler (icon) {
|
||||
if (icon) {
|
||||
const favicon = document.getElementById('favicon')
|
||||
favicon.href = icon
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.preloadApplications()
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
this.loaded = true
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
preloadApplications: 'applications/load',
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
31
client/web/one/src/views/routes.js
Normal 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' } },
|
||||
]
|
||||
70
client/web/one/tests/lib/helpers.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import Vue from 'vue'
|
||||
import { createLocalVue, shallowMount as sm, mount as rm } from '@vue/test-utils'
|
||||
import sinon from 'sinon'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import PortalVue from 'portal-vue'
|
||||
import store from 'corteza-webapp-one/src/store'
|
||||
|
||||
import resourceTranslations from 'corteza-webapp-one/src/mixins/resource-translations'
|
||||
|
||||
// Mixins
|
||||
Vue.mixin(resourceTranslations)
|
||||
|
||||
// Components
|
||||
Vue.config.ignoredElements = [
|
||||
'font-awesome-icon',
|
||||
]
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
Vue.use(PortalVue)
|
||||
|
||||
export const writeableWindowLocation = ({ path: value = '/' } = {}) => Object.defineProperty(window, 'location', { writable: true, value })
|
||||
|
||||
export const mount = (component, params = {}) => shallowMount(component, { ...params })
|
||||
|
||||
export const stdReject = () => sinon.stub().rejects({ message: 'err' })
|
||||
export const stdResolve = (rtr) => sinon.stub().resolves(rtr)
|
||||
export const networkReject = () => sinon.stub().rejects({ message: 'Network Error' })
|
||||
|
||||
export const stdAuth = (mocks = {}) => ({
|
||||
is: sinon.stub().returns(true),
|
||||
check: stdResolve(),
|
||||
goto: (u) => u,
|
||||
open: (u) => u,
|
||||
...mocks,
|
||||
})
|
||||
|
||||
const mounter = (component, { localVue = createLocalVue(), $auth = {}, mocks = {}, stubs = [], ...options } = {}, mount) => {
|
||||
return mount(component, {
|
||||
localVue,
|
||||
stubs: [
|
||||
'router-view',
|
||||
'router-link',
|
||||
...stubs,
|
||||
],
|
||||
mocks: {
|
||||
$t: (e) => e,
|
||||
$i18n: {
|
||||
i18next: {
|
||||
language: 'en',
|
||||
},
|
||||
},
|
||||
$SystemAPI: {},
|
||||
$route: { query: { fullPath: '', token: undefined } },
|
||||
$auth,
|
||||
$store: store,
|
||||
...mocks,
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const shallowMount = (...e) => {
|
||||
return mounter(...e, sm)
|
||||
}
|
||||
|
||||
export const fullMount = (...e) => {
|
||||
return mounter(...e, rm)
|
||||
}
|
||||
|
||||
export default shallowMount
|
||||
7
client/web/one/tests/unit/.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true,
|
||||
},
|
||||
}
|
||||
40
client/web/one/tests/unit/views/layout.spec.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import Layout from 'corteza-webapp-one/src/views/Layout'
|
||||
import { shallowMount } from 'corteza-webapp-one/tests/lib/helpers'
|
||||
import fp from 'flush-promises'
|
||||
|
||||
describe('/views/Layout.vue', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
let $auth, $Settings
|
||||
|
||||
beforeEach(() => {
|
||||
$auth = {
|
||||
user: {},
|
||||
}
|
||||
|
||||
$Settings = {
|
||||
get: () => ({}),
|
||||
attachment: () => '',
|
||||
}
|
||||
})
|
||||
|
||||
const mountLayout = (opt) => shallowMount(Layout, {
|
||||
mocks: { $auth, $Settings },
|
||||
...opt,
|
||||
})
|
||||
|
||||
describe('Init', () => {
|
||||
it('It renders', async () => {
|
||||
const wrapper = mountLayout()
|
||||
|
||||
await fp()
|
||||
expect(wrapper.find('div')).to.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
117
client/web/one/vue.config-builder.js
Normal file
@@ -0,0 +1,117 @@
|
||||
var webpack = require('webpack')
|
||||
var exec = require('child_process').execSync
|
||||
var path = require('path')
|
||||
|
||||
module.exports = ({ appFlavour, appLabel, version, theme, packageAlias, root = path.resolve('.'), env = process.env.NODE_ENV }) => {
|
||||
const isDevelopment = (env === 'development')
|
||||
const isTest = (env === 'test')
|
||||
|
||||
if (isTest) {
|
||||
var Vue = require('vue')
|
||||
Vue.config.devtools = false
|
||||
Vue.config.productionTip = false
|
||||
}
|
||||
|
||||
const optimization = isTest ? {} : {
|
||||
usedExports: true,
|
||||
runtimeChunk: 'single',
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
chunks: 'all',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
publicPath: './',
|
||||
lintOnSave: true,
|
||||
runtimeCompiler: true,
|
||||
|
||||
configureWebpack: {
|
||||
// other webpack options to merge in ...
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
FLAVOUR: JSON.stringify(appFlavour),
|
||||
WEBAPP: JSON.stringify(appLabel),
|
||||
VERSION: JSON.stringify(version || ('' + exec('git describe --always --tags')).trim()),
|
||||
BUILD_TIME: JSON.stringify((new Date()).toISOString()),
|
||||
}),
|
||||
],
|
||||
|
||||
optimization,
|
||||
},
|
||||
|
||||
chainWebpack: config => {
|
||||
// https://cli.vuejs.org/guide/troubleshooting.html#symbolic-links-in-node-modules
|
||||
config.resolve.symlinks(false)
|
||||
|
||||
// Do not copy config files (deployment procedure will do that)
|
||||
config.plugin('copy').tap(options => {
|
||||
options[0][0].ignore.push('config*js')
|
||||
return options
|
||||
})
|
||||
|
||||
// Aliasing 'corteza-webapp-compose' instead of '@' so we do
|
||||
// not break imports on apps that import this code
|
||||
config.resolve.alias.delete('@')
|
||||
if (packageAlias) {
|
||||
config.resolve.alias.set(packageAlias, root)
|
||||
}
|
||||
|
||||
if (isTest) {
|
||||
const scssRule = config.module.rule('scss')
|
||||
scssRule.uses.clear()
|
||||
scssRule
|
||||
.use('null-loader')
|
||||
.loader('null-loader')
|
||||
}
|
||||
|
||||
const scssNormal = config.module.rule('scss').oneOf('normal')
|
||||
|
||||
scssNormal.use('sass-loader')
|
||||
.loader('sass-loader')
|
||||
.tap(options => ({
|
||||
...options,
|
||||
sourceMap: true,
|
||||
}))
|
||||
|
||||
// Load CSS assets according to their location
|
||||
scssNormal.use('resolve-url-loader')
|
||||
.loader('resolve-url-loader').options({
|
||||
keepQuery: true,
|
||||
root: path.join(root, 'src/themes', theme),
|
||||
})
|
||||
.before('sass-loader')
|
||||
},
|
||||
|
||||
devServer: {
|
||||
host: '127.0.0.1',
|
||||
hot: true,
|
||||
disableHostCheck: true,
|
||||
|
||||
watchOptions: {
|
||||
ignored: [
|
||||
// Do not watch for changes under node_modules
|
||||
// (exception is node_modules/@cortezaproject)
|
||||
/node_modules([\\]+|\/)+(?!@cortezaproject)/,
|
||||
],
|
||||
aggregateTimeout: 200,
|
||||
poll: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
css: {
|
||||
sourceMap: isDevelopment,
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
// @todo cleanup all components and remove this global import
|
||||
additionalData: `@import "./src/themes/${theme}/variables.scss";`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
9
client/web/one/vue.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const buildVueConfig = require('./vue.config-builder')
|
||||
|
||||
module.exports = buildVueConfig({
|
||||
appFlavour: 'One',
|
||||
appName: 'one',
|
||||
appLabel: 'Corteza One',
|
||||
theme: 'corteza-base',
|
||||
packageAlias: 'corteza-webapp-one',
|
||||
})
|
||||