Add files from webapp-discovery with client/web/discovery prefix
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
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',
|
||||
'new-cap': 'off',
|
||||
'import/no-named-default': 'off',
|
||||
'vue/component-name-in-template-casing': ['error', 'kebab-case'],
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/order-in-components': ['error'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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-discovery
|
||||
|
||||
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:
|
||||
export BUILD_VERSION=${BUILD_VERSION} && $(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 $@
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
],
|
||||
env: {
|
||||
test: {
|
||||
plugins: [
|
||||
['istanbul', { useInlineSourceMaps: false }],
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"name": "corteza-discovery",
|
||||
"version": "2022.9.2",
|
||||
"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 && stylelint '**/*.{vue,scss}' --fix",
|
||||
"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",
|
||||
"@panter/vue-i18next": "^0.15.0",
|
||||
"axios": "^0.24.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"i18next": "^17.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.2",
|
||||
"leaflet": "^1.7.1",
|
||||
"portal-vue": "^2.1.7",
|
||||
"vue": "2.6.14",
|
||||
"vue-router": "^3.4.9",
|
||||
"vue-text-highlight": "^2.0.10",
|
||||
"vue2-leaflet": "^2.7.1",
|
||||
"vuelayers": "^0.11.37",
|
||||
"vuex": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-unit-mocha": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/eslint-config-standard": "^5.1.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.25",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^7.0.0",
|
||||
"babel-plugin-istanbul": "^5.2.0",
|
||||
"babel-preset-vue": "^2.0.2",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.0.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"flush-promises": "^1.0.2",
|
||||
"null-loader": "^4.0.1",
|
||||
"nyc": "^14.1.1",
|
||||
"resolve-url-loader": "^3.1.2",
|
||||
"sass": "^1.49.9",
|
||||
"sass-loader": "^10",
|
||||
"sinon": "^7.3.2",
|
||||
"stylelint": "^8.0.0",
|
||||
"stylelint-config-standard": "^18.3.0",
|
||||
"stylelint-scss": "^3.14.2",
|
||||
"stylelint-webpack-plugin": "^0.10.5",
|
||||
"vue-template-compiler": "2.6.14"
|
||||
},
|
||||
"stylelint": {
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"color-hex-case": null,
|
||||
"color-hex-length": null,
|
||||
"no-empty-source": null,
|
||||
"selector-list-comma-newline-after": null
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
],
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
config.js
|
||||
@@ -0,0 +1,13 @@
|
||||
// Corteza API location
|
||||
window.CortezaAPI = 'https://api.cortezaproject.your-domain.tld'
|
||||
window.CortezaDiscoveryAPI = 'https://api.searcher.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';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 702 B |
@@ -0,0 +1,30 @@
|
||||
<!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" href="<%= BASE_URL %>favicon32x32.png">
|
||||
<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>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div>
|
||||
<router-view
|
||||
v-if="loaded && i18nLoaded"
|
||||
class="h-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
i18nLoaded: false,
|
||||
}),
|
||||
async created () {
|
||||
this.$i18n.i18next.on('loaded', () => {
|
||||
this.i18nLoaded = true
|
||||
})
|
||||
|
||||
this.$auth.handle().then(({ accessTokenFn, user }) => {
|
||||
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.$Settings.init({ api: this.$SystemAPI }).then(() => {
|
||||
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
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<l-map
|
||||
:zoom="zoom"
|
||||
:center="center"
|
||||
style="height: calc(100vh - 64px);"
|
||||
@click="clearClickedMarker()"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
:attribution="attribution"
|
||||
/>
|
||||
<l-marker
|
||||
v-for="(marker, i) in markers"
|
||||
:key="i"
|
||||
:lat-lng="getLatLng(marker.coordinates)"
|
||||
:opacity="[hoverIndex, clickedMarker].includes(marker.id) ? 1.0 : 0.6"
|
||||
@click="onMarkerClick(marker.id)"
|
||||
/>
|
||||
</l-map>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { latLng } from 'leaflet'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
markers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
hoverIndex: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
zoom: 3,
|
||||
center: [30, 30],
|
||||
rotation: 0,
|
||||
attribution: '© <a target="_blank" rel="noopener noreferrer" href="http://osm.org/copyright">OpenStreetMap</a>',
|
||||
clickedMarker: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
markers: {
|
||||
immediate: true,
|
||||
handler (markers = []) {
|
||||
if (markers.length) {
|
||||
const { coordinates = [30, 30] } = markers[0]
|
||||
this.center = this.getLatLng(coordinates)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
hoverIndex: {
|
||||
handler (hoverIndex) {
|
||||
if (hoverIndex) {
|
||||
const { coordinates } = this.markers.find(({ id }) => id === hoverIndex) || {}
|
||||
if (coordinates) {
|
||||
this.center = this.getLatLng(coordinates)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLatLng (coordinates = [0, 0]) {
|
||||
return latLng(coordinates[0], coordinates[1])
|
||||
},
|
||||
|
||||
onMarkerClick (ID) {
|
||||
this.clickedMarker = ID
|
||||
this.$emit('hover', this.clickedMarker)
|
||||
},
|
||||
|
||||
clearClickedMarker () {
|
||||
this.clickedMarker = undefined
|
||||
this.$emit('hover', this.clickedMarker)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vl-style-text {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="my-2">
|
||||
<h6 class="text-primary mb-2">
|
||||
{{ this.$t('types.title') }}
|
||||
</h6>
|
||||
<b-form-checkbox-group
|
||||
v-model="types"
|
||||
name="types"
|
||||
:disabled="$store.state.processing"
|
||||
stacked
|
||||
class="mt-1"
|
||||
@change="updateTypes()"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
class="ml-2"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-center mb-0"
|
||||
>
|
||||
<span class="d-inline-block text-truncate mr-3">
|
||||
{{ option.text }}
|
||||
</span>
|
||||
</div>
|
||||
</b-form-checkbox>
|
||||
</b-form-checkbox-group>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="agg in aggregationOptions"
|
||||
:key="agg.resource"
|
||||
class="mt-4"
|
||||
>
|
||||
<div
|
||||
v-if="agg.items.length"
|
||||
class="d-flex justify-content-between align-items-center"
|
||||
style="min-height: 25px;"
|
||||
>
|
||||
<h6
|
||||
class="text-primary d-flex mb-0"
|
||||
>
|
||||
{{ agg.name }}
|
||||
<b-badge
|
||||
v-if="groups[agg.name].length"
|
||||
variant="dark"
|
||||
pill
|
||||
class="ml-1 align-self-center"
|
||||
>
|
||||
{{ groups[agg.name].length }}
|
||||
</b-badge>
|
||||
</h6>
|
||||
<b-button
|
||||
v-if="groups[agg.name].length"
|
||||
variant="link"
|
||||
class="text-muted p-0 m-0"
|
||||
size="sm"
|
||||
@click="clearGroup(agg.name)"
|
||||
>
|
||||
{{ $t('reset') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<b-form-checkbox-group
|
||||
v-model="groups[agg.name]"
|
||||
stacked
|
||||
class="mt-1 ml-2"
|
||||
:disabled="$store.state.processing"
|
||||
@change="updateGroup(agg.name)"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-for="(resource, i) in agg.items"
|
||||
:key="i"
|
||||
:value="resource.name"
|
||||
class="mb-1"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<span class="d-inline-block text-truncate">
|
||||
{{ getResourceDisplayName(agg.resource, resource) }}
|
||||
</span>
|
||||
<span
|
||||
class="pl-3 ml-auto text-muted"
|
||||
>
|
||||
{{ resource.hits }}
|
||||
</span>
|
||||
</div>
|
||||
</b-form-checkbox>
|
||||
</b-form-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
types: [],
|
||||
groups: {
|
||||
Module: [],
|
||||
Namespace: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
options () {
|
||||
return [
|
||||
{ text: this.$t('types.namespace'), value: 'compose:namespace' },
|
||||
{ text: this.$t('types.module'), value: 'compose:module' },
|
||||
{ text: this.$t('types.record'), value: 'compose:record' },
|
||||
{ text: this.$t('types.user'), value: 'system:user' },
|
||||
]
|
||||
},
|
||||
|
||||
aggregationOptions () {
|
||||
let namespaceOptions = this.$store.state.aggregations.find(({ resource }) => resource === 'compose:namespace') || {}
|
||||
let moduleOptions = this.$store.state.aggregations.find(({ resource }) => resource === 'compose:module') || {}
|
||||
|
||||
namespaceOptions = {
|
||||
resource: 'compose:namespace',
|
||||
name: 'Namespace',
|
||||
hits: namespaceOptions.hits || 0,
|
||||
items: namespaceOptions.resource_name || [],
|
||||
}
|
||||
|
||||
// Get all modules that are missing from store aggregations but are in filter
|
||||
const missingModuleOptions = this.groups.Module.filter(name => !(moduleOptions.resource_name || []).some(o => o.name === name))
|
||||
.map(name => ({ name }))
|
||||
|
||||
moduleOptions = {
|
||||
resource: 'compose:module',
|
||||
name: 'Module',
|
||||
hits: moduleOptions.hits || 0,
|
||||
items: [
|
||||
...missingModuleOptions,
|
||||
...(moduleOptions.resource_name || []),
|
||||
],
|
||||
}
|
||||
|
||||
return [namespaceOptions, moduleOptions]
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$store.state.namespaces': {
|
||||
immediate: true,
|
||||
handler (namespace) {
|
||||
this.groups.Namespace = namespace
|
||||
},
|
||||
},
|
||||
|
||||
'$store.state.modules': {
|
||||
immediate: true,
|
||||
handler (module) {
|
||||
this.groups.Module = module
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getResourceDisplayName (type, { name, handle, slug }) {
|
||||
if (type === 'compose:namespace') {
|
||||
return name || slug || 'Unnamed namespace'
|
||||
} else if (type === 'compose:module') {
|
||||
return handle || name || 'Unnamed module'
|
||||
}
|
||||
},
|
||||
|
||||
updateTypes () {
|
||||
this.$store.commit('updateTypes', this.types)
|
||||
},
|
||||
|
||||
updateGroup (name) {
|
||||
this.$store.commit(`update${name}s`, this.groups[name])
|
||||
},
|
||||
|
||||
clearGroup (name) {
|
||||
this.groups[name] = []
|
||||
this.$store.commit(`update${name}s`, [])
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header
|
||||
header-bg-variant="white"
|
||||
class="border-bottom"
|
||||
>
|
||||
<div class="d-flex align-items-center mb-3 justify-content-between">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mr-2 mb-0"
|
||||
>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
>
|
||||
{{ hit.value.namespace.name || hit.value.namespace.handle }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
class="mx-2"
|
||||
>
|
||||
<b-icon
|
||||
icon="arrow-right"
|
||||
font-scale="1"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
{{ hit.value.name || hit.value.handle }}
|
||||
</span>
|
||||
</h5>
|
||||
|
||||
<span class="text-nowrap">
|
||||
<b-badge
|
||||
v-if="Object.keys(hit.value.labels || { }).includes('federation')"
|
||||
variant="light"
|
||||
class="mr-1 h5 p-2 mb-0"
|
||||
>
|
||||
{{ $t('general:federated') }}
|
||||
</b-badge>
|
||||
<b-avatar
|
||||
v-b-tooltip.hover
|
||||
:title="$t('types.module')"
|
||||
size="sm"
|
||||
icon="list-ul"
|
||||
class="align-center bg-light text-dark"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body class="pb-0">
|
||||
<div
|
||||
v-for="(value, name, i) in limitData()"
|
||||
:key="i"
|
||||
class="d-flex flex-column mb-3"
|
||||
>
|
||||
<label
|
||||
class="text-capitalize text-primary mb-0"
|
||||
>
|
||||
{{ name }}
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<text-highlight
|
||||
:queries="query"
|
||||
highlight-style="padding: 0 0.05rem;"
|
||||
>
|
||||
{{ value }}
|
||||
</text-highlight>
|
||||
</div>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'fields',
|
||||
'namespace',
|
||||
'labels',
|
||||
'module',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header
|
||||
header-bg-variant="white"
|
||||
class="border-bottom"
|
||||
>
|
||||
<div class="d-flex align-items-center mb-3 justify-content-between">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mr-2 mb-0"
|
||||
>
|
||||
{{ hit.value.name || hit.value.slug }}
|
||||
</h5>
|
||||
<b-avatar
|
||||
v-b-tooltip.hover
|
||||
:title="$t('types.namespace')"
|
||||
size="sm"
|
||||
icon="code-square"
|
||||
class="align-center bg-light text-dark"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body class="pb-0">
|
||||
<div
|
||||
v-for="(item, name, i) in limitData()"
|
||||
:key="i"
|
||||
class="d-flex flex-column mb-3"
|
||||
>
|
||||
<label
|
||||
class="text-capitalize text-primary mb-0"
|
||||
>
|
||||
{{ name }}
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<text-highlight
|
||||
:queries="query"
|
||||
highlight-style="padding: 0 0.05rem;"
|
||||
>
|
||||
{{ item }}
|
||||
</text-highlight>
|
||||
</div>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'namespace',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header
|
||||
header-bg-variant="white"
|
||||
class="border-bottom"
|
||||
>
|
||||
<div class="d-flex align-items-center mb-3 justify-content-between">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mr-2 mb-0"
|
||||
>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
>
|
||||
{{ hit.value.namespace.name || hit.value.namespace.handle }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
class="mx-2"
|
||||
>
|
||||
<b-icon
|
||||
icon="arrow-right"
|
||||
font-scale="1"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
{{ hit.value.module.name || hit.value.module.handle }}
|
||||
</span>
|
||||
</h5>
|
||||
|
||||
<span class="text-nowrap">
|
||||
<b-badge
|
||||
v-if="Object.keys(hit.value.labels || { }).includes('federation')"
|
||||
variant="light"
|
||||
class="mr-1 h5 p-2 mb-0"
|
||||
>
|
||||
{{ $t('general:federated') }}
|
||||
</b-badge>
|
||||
<b-avatar
|
||||
v-b-tooltip.hover
|
||||
:title="$t('filters:types.record')"
|
||||
size="sm"
|
||||
icon="file-earmark-text"
|
||||
class="align-center bg-light text-dark"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body class="pb-0">
|
||||
<div
|
||||
v-if="limitData().length"
|
||||
>
|
||||
<div
|
||||
v-for="(item, i) in limitData()"
|
||||
:key="i"
|
||||
class="d-flex flex-column mb-3"
|
||||
>
|
||||
<label
|
||||
class="text-capitalize text-primary mb-0"
|
||||
>
|
||||
{{ item.label || item.name }}
|
||||
</label>
|
||||
<p class="multiline mt-1 mb-0">
|
||||
<text-highlight
|
||||
:queries="query"
|
||||
highlight-style="padding: 0 0.05rem;"
|
||||
>
|
||||
{{ item.value }}
|
||||
</text-highlight>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-else
|
||||
>
|
||||
{{ $t('general:no-values') }}
|
||||
</p>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
extends: base,
|
||||
|
||||
methods: {
|
||||
limitData () {
|
||||
const { values = [] } = this.hit.value
|
||||
|
||||
return (values || []).map(({ name, label, value = [] }) => {
|
||||
if (value) {
|
||||
value = value.map(v => {
|
||||
return v.toString().includes('{"coordinates":[') ? ((JSON.parse(v || '{}') || {}).coordinates || []).join(', ') : v
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
return { name, label, value }
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header
|
||||
header-bg-variant="white"
|
||||
class="border-bottom"
|
||||
>
|
||||
<div class="d-flex align-items-center mb-3 justify-content-between">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mr-2 mb-0"
|
||||
>
|
||||
{{ $t('types.user') }}
|
||||
</h5>
|
||||
|
||||
<b-avatar
|
||||
v-b-tooltip.hover
|
||||
:title="$t('types.user')"
|
||||
size="sm"
|
||||
class="align-center bg-light text-dark"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body class="pb-0">
|
||||
<div
|
||||
v-for="(value, name, i) in limitData()"
|
||||
:key="i"
|
||||
class="d-flex flex-column mb-3"
|
||||
>
|
||||
<label
|
||||
class="text-capitalize text-primary mb-0"
|
||||
>
|
||||
{{ name }}
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<text-highlight
|
||||
:queries="[$route.query.query]"
|
||||
highlight-style="padding: 0 0.05rem;"
|
||||
>
|
||||
{{ value }}
|
||||
</text-highlight>
|
||||
</div>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'fields',
|
||||
'namespace',
|
||||
'labels',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import TextHighlight from 'vue-text-highlight'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TextHighlight,
|
||||
},
|
||||
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showMap: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
defaultBlacklistedFields: ['deleted', 'created', 'updated', 'security'],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return this.defaultBlacklistedFields
|
||||
},
|
||||
|
||||
query () {
|
||||
return this.$route.query.query || ''
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
limitData () {
|
||||
const out = {}
|
||||
const limit = 5
|
||||
|
||||
if (this.hit.value) {
|
||||
let counter = 0
|
||||
|
||||
for (const key in this.hit.value) {
|
||||
const value = this.hit.value[key]
|
||||
|
||||
if (counter < limit && !!value && this.blacklistedFields.indexOf(key) < 0) {
|
||||
out[key] = value
|
||||
counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<b-card
|
||||
no-body
|
||||
class="shadow-sm h-100"
|
||||
:class="{ 'hover-effect': hit.value.url }"
|
||||
@mouseover="$emit('hover', hit.value.recordID)"
|
||||
@mouseleave="$emit('hover', undefined)"
|
||||
>
|
||||
<a
|
||||
v-if="hit.value.url"
|
||||
:href="hit.value.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="stretched-link"
|
||||
/>
|
||||
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="$props"
|
||||
>
|
||||
<template #header>
|
||||
<span
|
||||
v-if="createdBy"
|
||||
class="text-truncate text-nowrap mr-1"
|
||||
>
|
||||
<b-icon-person />
|
||||
{{ createdBy }}
|
||||
</span>
|
||||
<span
|
||||
v-if="createdAt"
|
||||
class="text-nowrap mr-1"
|
||||
>
|
||||
<b-icon-calendar />
|
||||
{{ createdAt }}
|
||||
</span>
|
||||
<span
|
||||
v-if="updatedAt"
|
||||
class="text-nowrap"
|
||||
>
|
||||
<b-icon-pencil-square />
|
||||
{{ updatedAt }}
|
||||
</span>
|
||||
</template>
|
||||
</component>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import * as Results from './loader'
|
||||
|
||||
export default {
|
||||
extends: base,
|
||||
|
||||
props: {
|
||||
hit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
component () {
|
||||
let { type } = this.hit
|
||||
type = type.split(':')[1]
|
||||
|
||||
const keys = Object.keys(Results)
|
||||
const i = keys.map(c => c.toLocaleLowerCase()).findIndex(c => c === type)
|
||||
|
||||
return Results[keys[i]]
|
||||
},
|
||||
|
||||
createdBy () {
|
||||
const { by } = this.hit.value.created || {}
|
||||
return by
|
||||
},
|
||||
|
||||
createdAt () {
|
||||
const { at } = this.hit.value.created || {}
|
||||
return at ? new Date(at).toLocaleDateString() : at
|
||||
},
|
||||
|
||||
updatedAt () {
|
||||
const { at } = this.hit.value.updated || {}
|
||||
return at ? new Date(at).toLocaleDateString() : at
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hover-effect {
|
||||
&:hover {
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 8px rgba(38, 38, 38, 0.2) !important;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as Module } from './Module'
|
||||
export { default as Namespace } from './Namespace'
|
||||
export { default as Record } from './Record'
|
||||
export { default as User } from './User'
|
||||
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<b-container
|
||||
fluid
|
||||
class="h-100 mh-100 p-0"
|
||||
>
|
||||
<b-row
|
||||
no-gutters
|
||||
>
|
||||
<b-col
|
||||
md="12"
|
||||
:lg="map.show ? '7' : '12'"
|
||||
:xl="map.show ? '8' : '12'"
|
||||
class="results-container"
|
||||
:class="{ 'with-map': map.show }"
|
||||
>
|
||||
<b-form
|
||||
@submit.prevent="onQuerySubmit()"
|
||||
>
|
||||
<b-form-group class="px-3">
|
||||
<c-input-search
|
||||
v-model="query"
|
||||
:placeholder="$t('input-placeholder')"
|
||||
:autocomplete="'off'"
|
||||
submittable
|
||||
@search="onQuerySubmit()"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="d-flex align-items-center justify-content-between px-1 mt-1 text-muted"
|
||||
>
|
||||
<span
|
||||
:class="{ 'discovering': $store.state.processing }"
|
||||
>
|
||||
{{ searchDescription }}
|
||||
</span>
|
||||
<span>
|
||||
Use <samp>"text"</samp> for exact match
|
||||
</span>
|
||||
</div>
|
||||
</b-form-group>
|
||||
</b-form>
|
||||
|
||||
<b-row
|
||||
class="results w-100 m-0 mh-100 overflow-auto"
|
||||
>
|
||||
<div
|
||||
v-if="$store.state.processing || !total.actual"
|
||||
class="position-absolute d-flex align-items-center justify-content-center w-100 h-100"
|
||||
style="opacity: 0.8; z-index: 1; background-color: #F3F3F5;"
|
||||
>
|
||||
<h5
|
||||
class="mb-5"
|
||||
>
|
||||
<b-spinner
|
||||
v-if="$store.state.processing"
|
||||
variant="primary"
|
||||
class="p-4"
|
||||
/>
|
||||
<span
|
||||
v-else-if="!total.actual"
|
||||
>
|
||||
{{ $t('no-results') }}
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
<b-col
|
||||
v-for="(hit, i) in filteredHits"
|
||||
:key="i"
|
||||
sm="12"
|
||||
md="6"
|
||||
:lg="map.show ? '6': '4'"
|
||||
class="py-3"
|
||||
>
|
||||
<result
|
||||
:id="hit.value.recordID || hit.value.moduleID"
|
||||
:index="i"
|
||||
:hit="hit"
|
||||
:show-map="map.show"
|
||||
:class="{ 'border-primary border shadow': map.clickedMarker && [hit.value.recordID, hit.value.moduleID].includes(map.clickedMarker) }"
|
||||
@hover="map.hoverIndex = $event"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div
|
||||
class="position-fixed map-button"
|
||||
>
|
||||
<b-button
|
||||
variant="warning"
|
||||
class="rounded-circle p-3"
|
||||
:title="$t('tooltip.map')"
|
||||
@click="toggleMap"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'map-marked-alt']"
|
||||
class="h3 mb-0"
|
||||
/>
|
||||
</b-button>
|
||||
</div>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
v-if="map.show"
|
||||
md="12"
|
||||
lg="5"
|
||||
xl="4"
|
||||
>
|
||||
<discovery-map
|
||||
:markers="map.markers"
|
||||
:hover-index="map.hoverIndex"
|
||||
@hover="markerHovered"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Result from './Results'
|
||||
import DiscoveryMap from './DiscoveryMap.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CInputSearch } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'search',
|
||||
},
|
||||
|
||||
components: {
|
||||
Result,
|
||||
DiscoveryMap,
|
||||
CInputSearch,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
query: '',
|
||||
|
||||
hits: [],
|
||||
filteredHits: [],
|
||||
|
||||
pagination: {
|
||||
limit: 50,
|
||||
from: 0,
|
||||
size: 0,
|
||||
},
|
||||
|
||||
total: {
|
||||
all: 0,
|
||||
actual: 0,
|
||||
},
|
||||
|
||||
initial: false,
|
||||
|
||||
map: {
|
||||
show: false,
|
||||
markers: [],
|
||||
clickedMarker: undefined,
|
||||
hoverIndex: undefined,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
searchDescription () {
|
||||
if (this.$store.state.processing) {
|
||||
return this.$t('discovering')
|
||||
}
|
||||
|
||||
if (this.total.all > 0) {
|
||||
return this.$t('range', { actual: this.total.actual, all: this.total.all })
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$store.state.types': {
|
||||
handler: function () {
|
||||
this.getFilteredData()
|
||||
},
|
||||
},
|
||||
|
||||
'$store.state.modules': {
|
||||
handler () {
|
||||
if (this.initial) return
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData(this.query)
|
||||
},
|
||||
},
|
||||
|
||||
'$store.state.namespaces': {
|
||||
handler () {
|
||||
if (this.initial) return
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData(this.query)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.initial = true
|
||||
|
||||
const { query = '', modules = [], namespaces = [], size = 0 } = this.$route.query
|
||||
|
||||
this.query = query
|
||||
this.pagination.size = size
|
||||
|
||||
this.$store.commit('updateModules', Array.isArray(modules) ? modules : [modules])
|
||||
this.$store.commit('updateNamespaces', Array.isArray(namespaces) ? namespaces : [namespaces])
|
||||
|
||||
this.getSearchData(this.query)
|
||||
|
||||
setTimeout(() => {
|
||||
this.initial = false
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
mounted () {
|
||||
const listElm = document.querySelector('.results')
|
||||
listElm.addEventListener('scroll', e => {
|
||||
if (listElm.scrollTop + listElm.clientHeight >= listElm.scrollHeight - 10) {
|
||||
if (!this.$store.state.processing && this.total.actual < this.total.all) {
|
||||
this.getSearchData(this.query, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
getSearchData (query = '', append = false) {
|
||||
this.$store.commit('updateProcessing', true)
|
||||
|
||||
if (!append) {
|
||||
this.map.markers = []
|
||||
}
|
||||
|
||||
// Filters
|
||||
const modules = this.$store.state.modules
|
||||
const namespaces = this.$store.state.namespaces
|
||||
|
||||
// Pagination
|
||||
if (append) {
|
||||
this.pagination.size += this.pagination.limit
|
||||
}
|
||||
|
||||
const { size } = this.pagination
|
||||
|
||||
this.updateRouteQuery({ query, modules, namespaces, size })
|
||||
|
||||
this.$DiscoveryAPI.query({ query, modules, namespaces, size })
|
||||
.then((response = {}) => {
|
||||
if (response) {
|
||||
this.hits = (response.hits || [])
|
||||
|
||||
this.total.all = response.total_results || 0
|
||||
|
||||
this.getFilteredData()
|
||||
|
||||
this.pagination = {
|
||||
...this.pagination,
|
||||
from: response.from || 0,
|
||||
size: response.size || 0,
|
||||
}
|
||||
|
||||
this.$store.commit('updateAggregations', response.aggregations)
|
||||
|
||||
this.getMarkers()
|
||||
}
|
||||
}).catch(e => {
|
||||
this.toastErrorHandler(this.$t('notification:search.failed'))(e)
|
||||
this.hits = []
|
||||
this.filteredHits.splice(0, this.filteredHits.length)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$store.commit('updateProcessing', false)
|
||||
})
|
||||
},
|
||||
|
||||
getFilteredData () {
|
||||
let filteredHits = this.hits
|
||||
|
||||
if (this.$store.state.types.length > 0 && this.hits.length) {
|
||||
filteredHits = this.hits.filter(hit => this.$store.state.types.includes(hit.type))
|
||||
}
|
||||
|
||||
this.filteredHits.splice(0, this.filteredHits.length, ...filteredHits)
|
||||
this.total.actual = this.filteredHits.length
|
||||
},
|
||||
|
||||
onQuerySubmit () {
|
||||
if (!this.$store.state.processing) {
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData(this.query)
|
||||
}
|
||||
},
|
||||
|
||||
getMarkers () {
|
||||
const markers = []
|
||||
|
||||
this.filteredHits.forEach(({ type, value }) => {
|
||||
if (type === 'compose:record' && Array.isArray(value.values)) {
|
||||
const id = value.recordID
|
||||
value.values.forEach(({ value = [] }) => {
|
||||
const isGeometry = value && value.find(v => {
|
||||
return v.toString().includes('{"coordinates":[')
|
||||
})
|
||||
|
||||
if (isGeometry) {
|
||||
value.forEach(coordinates => {
|
||||
coordinates = JSON.parse(coordinates || '{}').coordinates
|
||||
if (coordinates) {
|
||||
markers.push({ id, coordinates })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.map.markers = markers
|
||||
},
|
||||
|
||||
markerHovered (ID) {
|
||||
if (ID) {
|
||||
document.getElementById(ID).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
}
|
||||
|
||||
this.map.clickedMarker = ID
|
||||
},
|
||||
|
||||
toggleMap () {
|
||||
this.map.show = !this.map.show
|
||||
},
|
||||
|
||||
updateRouteQuery ({ query = undefined, modules = [], namespaces = [], size = 0 }) {
|
||||
if (JSON.stringify(this.$route.query) !== JSON.stringify({ query, modules, namespaces })) {
|
||||
this.$router.push({ query: { query: query || undefined, modules, namespaces, size } })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.results-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.results-container.with-map {
|
||||
height: calc(60vh - 64px);
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.results-container {
|
||||
height: calc(100vh - 64px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.clear-query {
|
||||
z-index: 3 !important;
|
||||
right: 52px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.map-button {
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/40991531/17926309
|
||||
.discovering::after {
|
||||
display: inline-block;
|
||||
animation: discovering steps(1, end) 1s infinite;
|
||||
content: '';
|
||||
}
|
||||
|
||||
@keyframes discovering {
|
||||
0% { content: ''; }
|
||||
25% { content: '.'; }
|
||||
50% { content: '..'; }
|
||||
75% { content: '...'; }
|
||||
100% { content: ''; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faGripHorizontal, faThumbtack, faBars, faChevronUp, faChevronDown, faSearch, faMapMarkedAlt, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faQuestionCircle, faUser } from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faUser,
|
||||
faQuestionCircle,
|
||||
faGripHorizontal,
|
||||
faThumbtack,
|
||||
faBars,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faSearch,
|
||||
faMapMarkedAlt,
|
||||
faTimes,
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
import Vue from 'vue'
|
||||
import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import PortalVue from 'portal-vue'
|
||||
import './faIcons'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
import { LMap, LTileLayer, LMarker } from 'vue2-leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import { Icon } from 'leaflet'
|
||||
|
||||
Vue.use(PortalVue)
|
||||
|
||||
Vue.component('l-map', LMap)
|
||||
Vue.component('l-tile-layer', LTileLayer)
|
||||
Vue.component('l-marker', LMarker)
|
||||
|
||||
delete Icon.Default.prototype._getIconUrl
|
||||
Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||
})
|
||||
|
||||
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(BootstrapVueIcons)
|
||||
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
||||
Vue.component('font-awesome-layers', FontAwesomeLayers)
|
||||
Vue.component('c-permissions-button', components.CPermissionsButton)
|
||||
Vue.component('c-input-confirm', components.CInputConfirm)
|
||||
Vue.component('c-input-processing', components.CInputProcessing)
|
||||
Vue.component('c-input-search', components.CInputSearch)
|
||||
@@ -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.`)
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
@@ -0,0 +1,26 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import { i18n } from '@cortezaproject/corteza-vue'
|
||||
|
||||
import './themes'
|
||||
import './config-check'
|
||||
import './console-splash'
|
||||
import './plugins'
|
||||
import './mixins'
|
||||
import './components'
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-discovery' },
|
||||
'general',
|
||||
'notification',
|
||||
'navigation',
|
||||
'filters',
|
||||
'search',
|
||||
),
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
@@ -0,0 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import toast from './toast'
|
||||
|
||||
Vue.mixin(toast)
|
||||
@@ -0,0 +1,46 @@
|
||||
export default {
|
||||
methods: {
|
||||
toastSuccess (message, title = undefined) {
|
||||
if (title === undefined) {
|
||||
title = this.$t('notification:general:success')
|
||||
}
|
||||
|
||||
this.toast(message, { title, variant: 'success' })
|
||||
},
|
||||
|
||||
toastWarning (message, title = undefined) {
|
||||
if (title === undefined) {
|
||||
title = this.$t('notification:general:warning')
|
||||
}
|
||||
|
||||
this.toast(message, { title, variant: 'warning' })
|
||||
},
|
||||
|
||||
toastDanger (message, title = undefined) {
|
||||
if (title === undefined) {
|
||||
title = this.$t('notification:general:error')
|
||||
}
|
||||
|
||||
this.toast(message, { title, variant: 'danger' })
|
||||
},
|
||||
|
||||
toast (msg, opt = { variant: 'success' }) {
|
||||
this.$root.$bvToast.toast(msg, opt)
|
||||
},
|
||||
|
||||
toastErrorHandler (opt) {
|
||||
if (typeof opt === 'string') {
|
||||
opt = { prefix: opt }
|
||||
}
|
||||
|
||||
const { prefix, title } = opt
|
||||
|
||||
return (err = {}) => {
|
||||
/* eslint-disable no-console */
|
||||
console.error(err)
|
||||
const msg = err.message ? (prefix + ': ' + this.$t(err.message)) : prefix
|
||||
this.toastDanger(msg, title)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import { plugins } from '@cortezaproject/corteza-vue'
|
||||
import DiscoveryAPI from './searcher.js'
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(plugins.Auth(), { app: 'discovery' })
|
||||
Vue.use(plugins.CortezaAPI('system'))
|
||||
Vue.use(plugins.Settings, { api: Vue.prototype.$SystemAPI })
|
||||
Vue.use(DiscoveryAPI())
|
||||
@@ -0,0 +1,128 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export default function (service, opt = {}) {
|
||||
if (!opt.baseURL) {
|
||||
// @ts-ignore
|
||||
if (!window.CortezaDiscoveryAPI) {
|
||||
throw new Error('config.js missing or window.CortezaDiscoveryAPI not set')
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
opt.baseURL = `${window.CortezaDiscoveryAPI}/`
|
||||
}
|
||||
|
||||
return function (Vue) {
|
||||
if (!opt.accessTokenFn) {
|
||||
/**
|
||||
* Checking if auth plugin was initialized before and
|
||||
* hooking on to it's accessTokenFn
|
||||
*/
|
||||
opt.accessTokenFn = Vue.prototype.$auth.accessTokenFn
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// makes Vue.$<service>API (Vue.$SystemAPI, Vue.$ComposeAPI, Vue.$FederationAPI, Vue.$AutomationAPI) available
|
||||
Vue.prototype.$DiscoveryAPI = new Searcher(opt)
|
||||
}
|
||||
}
|
||||
|
||||
function stdResolve (response) {
|
||||
if (response.data.error) {
|
||||
return Promise.reject(response.data.error)
|
||||
} else {
|
||||
return response.data.response
|
||||
}
|
||||
}
|
||||
|
||||
class Searcher {
|
||||
baseURL;
|
||||
accessTokenFn;
|
||||
headers = {};
|
||||
|
||||
constructor ({ baseURL, headers, accessTokenFn }) {
|
||||
this.baseURL = baseURL
|
||||
this.accessTokenFn = accessTokenFn
|
||||
this.headers = {
|
||||
/**
|
||||
* All we send is JSON
|
||||
*/
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
this.setHeaders(headers)
|
||||
}
|
||||
|
||||
setAccessTokenFn (fn) {
|
||||
this.accessTokenFn = fn
|
||||
return this
|
||||
}
|
||||
|
||||
setHeaders (headers) {
|
||||
if (typeof headers === 'object') {
|
||||
this.headers = headers
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setHeader (name, value) {
|
||||
if (value === undefined) {
|
||||
delete this.headers[name]
|
||||
} else {
|
||||
this.headers[name] = value
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
api () {
|
||||
const headers = { ...this.headers }
|
||||
const accessToken = this.accessTokenFn ? this.accessTokenFn() : undefined
|
||||
if (accessToken) {
|
||||
headers.Authorization = 'Bearer ' + accessToken
|
||||
}
|
||||
|
||||
return axios.create({
|
||||
withCredentials: true,
|
||||
baseURL: this.baseURL,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
// List namespaces
|
||||
async query (a, extra = {}) {
|
||||
const {
|
||||
modules,
|
||||
namespaces,
|
||||
from,
|
||||
size,
|
||||
} = a || {}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
// Filter
|
||||
if (modules?.length > 0) modules.forEach(m => params.append('moduleAggs', m))
|
||||
if (namespaces?.length > 0) namespaces.forEach(n => params.append('namespaceAggs', n))
|
||||
|
||||
// Pagination
|
||||
if (from) params.append('from', from)
|
||||
if (size) params.append('size', size)
|
||||
|
||||
const cfg = {
|
||||
...extra,
|
||||
method: 'get',
|
||||
url: this.queryEndpoint(a),
|
||||
params,
|
||||
}
|
||||
|
||||
return this.api().request(cfg).then(result => stdResolve(result))
|
||||
}
|
||||
|
||||
queryEndpoint (a) {
|
||||
const {
|
||||
query = '',
|
||||
} = a || {}
|
||||
|
||||
return `/?q=${query}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import Router from 'vue-router'
|
||||
import routes from './views/routes'
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
component: () => import('../views/Layout'),
|
||||
},
|
||||
{ path: '*', redirect: { name: 'root' } },
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,36 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
processing: false,
|
||||
types: [],
|
||||
aggregations: [],
|
||||
modules: [],
|
||||
namespaces: [],
|
||||
},
|
||||
mutations: {
|
||||
updateProcessing (state, value = false) {
|
||||
state.processing = value
|
||||
},
|
||||
|
||||
updateTypes (state, types) {
|
||||
state.types = types
|
||||
},
|
||||
updateAggregations (state, aggs) {
|
||||
state.aggregations = aggs
|
||||
},
|
||||
updateModules (state, value) {
|
||||
state.modules = value
|
||||
},
|
||||
updateNamespaces (state, value) {
|
||||
state.namespaces = value
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,242 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
html {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.grab {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
thead th,
|
||||
legend,
|
||||
label,
|
||||
.btn {
|
||||
font-family: $font-medium;
|
||||
}
|
||||
|
||||
strong,
|
||||
b,
|
||||
.font-weight-bold {
|
||||
font-family: $font-semibold;
|
||||
}
|
||||
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vue-grid-layout {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.vue-grid-item {
|
||||
transition: none !important;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-select {
|
||||
min-width: 250px;
|
||||
|
||||
.vs__search {
|
||||
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;
|
||||
}
|
||||
|
||||
.ProseMirror.ProseMirror-focused {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
.page-item {
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Rich text content styling
|
||||
.rt-content {
|
||||
&.editor {
|
||||
border-radius: 4px;
|
||||
border: 2px solid $light;
|
||||
}
|
||||
|
||||
.editor__content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// blockquote
|
||||
blockquote {
|
||||
border-left: 3px solid rgba($black, 0.1);
|
||||
padding-left: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Code
|
||||
pre {
|
||||
background-color: #23241f;
|
||||
color: #f8f8f2;
|
||||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
// todo list - https://github.com/scrumpy/tiptap/blob/master/examples/Components/Routes/TodoList/index.vue
|
||||
li[data-type="todo_item"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.todo-checkbox {
|
||||
border: 2px solid $black;
|
||||
height: 0.9em;
|
||||
width: 0.9em;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10px;
|
||||
margin-top: 0.3rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.2em;
|
||||
background-color: transparent;
|
||||
transition: 0.4s background;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
|
||||
> p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> ul[data-type="todo_list"] {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
li[data-done="true"] {
|
||||
> .todo-content {
|
||||
> p {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
> .todo-checkbox {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
li[data-done="false"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset.required {
|
||||
&.error {
|
||||
legend::before {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
label::after {
|
||||
content: "*";
|
||||
display: inline-block;
|
||||
color: $danger;
|
||||
width: 10px;
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-holder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
.alert {
|
||||
z-index: 1040;
|
||||
box-shadow: 0 0 2px 0 rgba($secondary, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
// Over-ride Bootstrap clear search input icon
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
background: url("data:image/svg+xml;charset=UTF-8,%3csvg viewPort='0 0 12 12' version='1.1' xmlns='http://www.w3.org/2000/svg'%3e%3cline x1='1' y1='11' x2='11' y2='1' stroke='black' stroke-width='2'/%3e%3cline x1='1' y1='1' x2='11' y2='11' stroke='black' stroke-width='2'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.b-toaster.b-toaster-bottom-right {
|
||||
bottom: 75px;
|
||||
}
|
||||
|
||||
.vs__spinner, .vs__spinner::after {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
.wrap-with-vertical-gutters {
|
||||
margin-top: -0.25rem;
|
||||
|
||||
> * {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-label {
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
import './index.scss'
|
||||
@@ -0,0 +1,6 @@
|
||||
@import "./variables";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import '~bootstrap-vue/src/index.scss';
|
||||
@import "./custom";
|
||||
@import "./poppins.scss";
|
||||
@import "~vuelayers/lib/style.css";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
$wideminwidth:767px !default;
|
||||
$confortableminwidth:1020px !default;
|
||||
|
||||
// Paths
|
||||
$fonts_dir : './fonts/' !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;
|
||||
|
||||
// Grid - maybe not the smartest idea to change this but let's see
|
||||
$grid-breakpoints: (
|
||||
xs: 0,
|
||||
sm: 576px,
|
||||
md: 768px,
|
||||
lg: 992px,
|
||||
xl: 1500px
|
||||
) !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;
|
||||
|
||||
// Tables
|
||||
$table-cell-padding-sm: 0.25rem !default;
|
||||
$table-hover-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;
|
||||
|
||||
// Navbar
|
||||
$navbar-bg: $white !default;
|
||||
$navbar-color: $black !default;
|
||||
$navbar-border: $primary !default;
|
||||
$navbar-active-bg: $white !default;
|
||||
$navbar-active-color: $primary !default;
|
||||
$navbar-active-border: $primary !default;
|
||||
$navbar-hover-bg: $white !default;
|
||||
$navbar-hover-color: $primary !default;
|
||||
$navbar-hover-border: $primary !default;
|
||||
$navbar-height: 50px !default;
|
||||
$navbar-padding-y: 0 !default;
|
||||
$navbar-padding-x: 0 !default;
|
||||
|
||||
$navbar-toggler-font-size: 24px !default;
|
||||
$sidebar-width: 320px !default;
|
||||
$sidebar-bg: #F4F7FA;
|
||||
|
||||
// Pagination
|
||||
$pagination-color: $dark !default;
|
||||
$pagination-disabled-bg: transparent !default;
|
||||
$pagination-bg: transparent !default;
|
||||
$pagination-active-bg: $dark !default;
|
||||
$pagination-border-width: 0 !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;
|
||||
|
||||
/* stylelint-disable */
|
||||
// This is used within corteza-webapp-compose/src/lib/block/Calendar/feedLoader/
|
||||
// to determine default event colors
|
||||
:export {
|
||||
primary: $primary;
|
||||
danger: $danger;
|
||||
secondary: $secondary;
|
||||
}
|
||||
/* stylelint-enable */
|
||||
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
import(/* webpackChunkName: 'corteza-base' */ './corteza-base')
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column w-100 vh-100">
|
||||
<header>
|
||||
<c-topbar
|
||||
: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 #title>
|
||||
{{ $t('discovery') }}
|
||||
</template>
|
||||
</c-topbar>
|
||||
</header>
|
||||
|
||||
<aside>
|
||||
<c-sidebar
|
||||
:expanded.sync="expanded"
|
||||
:pinned.sync="pinned"
|
||||
:icon="icon"
|
||||
:logo="logo"
|
||||
:disabled-routes="['report.list', 'report.create', 'report.edit']"
|
||||
expand-on-hover
|
||||
>
|
||||
<template #body-expanded>
|
||||
<filters />
|
||||
</template>
|
||||
</c-sidebar>
|
||||
</aside>
|
||||
|
||||
<main class="d-inline-flex h-100">
|
||||
<!--
|
||||
Content spacer
|
||||
Large and xl screens should push in content when the nav is expanded
|
||||
-->
|
||||
<template>
|
||||
<div
|
||||
class="spacer"
|
||||
:class="{
|
||||
'expanded': expanded && pinned,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<search />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from '../components/Search.vue'
|
||||
import Filters from '../components/Filters.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CTopbar, CSidebar } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'navigation',
|
||||
},
|
||||
|
||||
components: {
|
||||
CTopbar,
|
||||
CSidebar,
|
||||
Search,
|
||||
Filters,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
expanded: undefined,
|
||||
pinned: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
user () {
|
||||
const { user } = this.$auth
|
||||
return user.name || user.handle || user.email || ''
|
||||
},
|
||||
|
||||
icon () {
|
||||
return this.$Settings.attachment('ui.iconLogo')
|
||||
},
|
||||
|
||||
logo () {
|
||||
return this.$Settings.attachment('ui.mainLogo')
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
icon: {
|
||||
immediate: true,
|
||||
handler (icon) {
|
||||
if (icon) {
|
||||
const favicon = document.getElementById('favicon')
|
||||
favicon.href = icon
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$root.$on('alert', this.displayToast)
|
||||
},
|
||||
|
||||
methods: {
|
||||
displayToast ({ title, message, variant, countdown }) {
|
||||
this.$bvToast.toast(message, {
|
||||
title,
|
||||
variant,
|
||||
solid: true,
|
||||
autoHideDelay: countdown,
|
||||
toaster: 'b-toaster-bottom-right',
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.spacer {
|
||||
min-width: 0;
|
||||
-webkit-transition: min-width 0.2s ease-in-out;
|
||||
-moz-transition: min-width 0.2s ease-in-out;
|
||||
-o-transition: min-width 0.2s ease-in-out;
|
||||
transition: min-width 0.2s ease-in-out;
|
||||
|
||||
&.expanded {
|
||||
min-width: $sidebar-width;
|
||||
-webkit-transition: min-width 0.2s ease-in-out;
|
||||
-moz-transition: min-width 0.2s ease-in-out;
|
||||
-o-transition: min-width 0.2s ease-in-out;
|
||||
transition: min-width 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
export default [
|
||||
{
|
||||
name: 'root',
|
||||
path: '',
|
||||
component: () => import('./Layout.vue'),
|
||||
},
|
||||
|
||||
// When everything else fails, go to root
|
||||
{ path: '*', redirect: { name: 'root' } },
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
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'
|
||||
|
||||
// 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,
|
||||
...mocks,
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const shallowMount = (...e) => {
|
||||
return mounter(...e, sm)
|
||||
}
|
||||
|
||||
export const fullMount = (...e) => {
|
||||
return mounter(...e, rm)
|
||||
}
|
||||
|
||||
export default shallowMount
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true,
|
||||
},
|
||||
}
|
||||
@@ -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 '../../../src/views/Layout'
|
||||
import { shallowMount } from '../../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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,127 @@
|
||||
const webpack = require('webpack')
|
||||
const exec = require('child_process').execSync
|
||||
const path = require('path')
|
||||
|
||||
module.exports = ({ appFlavour, appLabel, version = process.env.BUILD_VERSION, theme, packageAlias, root = path.resolve('.'), env = process.env.NODE_ENV }) => {
|
||||
const isDevelopment = (env === 'development')
|
||||
const isTest = (env === 'test')
|
||||
|
||||
if (isTest || isDevelopment) {
|
||||
const Vue = require('vue')
|
||||
|
||||
if (isTest) {
|
||||
Vue.config.devtools = false
|
||||
Vue.config.productionTip = false
|
||||
}
|
||||
|
||||
if (isDevelopment) {
|
||||
Vue.config.devtools = true
|
||||
Vue.config.performance = true
|
||||
}
|
||||
}
|
||||
|
||||
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 ...
|
||||
|
||||
resolve: { symlinks: false },
|
||||
|
||||
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 => {
|
||||
// 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 full package name 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";`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const buildVueConfig = require('./vue.config-builder')
|
||||
|
||||
module.exports = buildVueConfig({
|
||||
appFlavour: 'Discovery',
|
||||
appName: 'discovery',
|
||||
appLabel: 'Corteza Discovery Discovery',
|
||||
theme: 'corteza-base',
|
||||
packageAlias: 'corteza-webapp-discovery',
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user