Add files from webapp-privacy with client/web/privacy 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-privacy
|
||||
|
||||
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,109 @@
|
||||
{
|
||||
"name": "corteza-privacy",
|
||||
"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",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"i18next": "^17.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.2",
|
||||
"leaflet": "^1.7.1",
|
||||
"portal-vue": "^2.1.7",
|
||||
"vue": "2.6.14",
|
||||
"vue-grid-layout": "^2.3.12",
|
||||
"vue-router": "3.1.5",
|
||||
"vue-select": "^3.12.2",
|
||||
"vue2-leaflet": "^2.7.1",
|
||||
"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.13",
|
||||
"@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.0.3",
|
||||
"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,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';
|
||||
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">
|
||||
<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,89 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import './config-check'
|
||||
import './console-splash'
|
||||
|
||||
import './plugins'
|
||||
import './mixins'
|
||||
import './components'
|
||||
import store from './store'
|
||||
|
||||
import { i18n } from '@cortezaproject/corteza-vue'
|
||||
|
||||
import router from './router'
|
||||
|
||||
export default (options = {}) => {
|
||||
options = {
|
||||
el: '#app',
|
||||
name: 'privacy',
|
||||
|
||||
template: '<div v-if="loaded && i18nLoaded" class="h-100"><router-view/></div>',
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Load effective permissions
|
||||
this.$store.dispatch('rbac/load')
|
||||
|
||||
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
|
||||
})
|
||||
},
|
||||
|
||||
router,
|
||||
store,
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-privacy' },
|
||||
'dashboard',
|
||||
'data-overview',
|
||||
'general',
|
||||
'map',
|
||||
'module-records',
|
||||
'navigation',
|
||||
'notification',
|
||||
'request',
|
||||
'sensitive-data',
|
||||
),
|
||||
// Any additional options we want to merge
|
||||
...options,
|
||||
}
|
||||
|
||||
return new Vue(options)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<b-container
|
||||
fluid
|
||||
class="bg-white shadow-sm border-top p-3"
|
||||
>
|
||||
<b-row
|
||||
no-gutters
|
||||
class="align-items-center"
|
||||
>
|
||||
<b-col>
|
||||
<b-button
|
||||
v-if="backLink"
|
||||
variant="link"
|
||||
size="lg"
|
||||
:to="backLink"
|
||||
class="d-flex align-items-center text-dark back"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'chevron-left']"
|
||||
class="back-icon mr-1"
|
||||
/>
|
||||
{{ $t('general:label.back') }}
|
||||
</b-button>
|
||||
|
||||
<slot name="left" />
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<slot name="middle" />
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
class="d-flex justify-content-end"
|
||||
>
|
||||
<template v-if="deleteShow">
|
||||
<c-input-confirm
|
||||
v-if="deleteConfirm"
|
||||
:disabled="deleteDisabled || processing"
|
||||
:borderless="false"
|
||||
variant="danger"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
class="ml-1"
|
||||
@confirmed="$emit('delete')"
|
||||
>
|
||||
{{ deleteLabel }}
|
||||
</c-input-confirm>
|
||||
|
||||
<b-button
|
||||
v-else
|
||||
:disabled="deleteDisabled || processing"
|
||||
variant="danger"
|
||||
size="lg"
|
||||
class="ml-1"
|
||||
@click="$emit('delete')"
|
||||
>
|
||||
{{ deleteLabel }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<b-button
|
||||
v-if="submitShow"
|
||||
:disabled="submitDisabled || processing"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
class="ml-1"
|
||||
@click="$emit('submit')"
|
||||
>
|
||||
{{ submitLabel }}
|
||||
</b-button>
|
||||
|
||||
<slot />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
processing: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
|
||||
backLink: {
|
||||
type: Object,
|
||||
default: () => ({ name: 'root' }),
|
||||
},
|
||||
|
||||
deleteShow: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
|
||||
deleteDisabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
|
||||
deleteConfirm: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
deleteLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
submitShow: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
|
||||
submitDisabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
|
||||
submitLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.back {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
.back-icon {
|
||||
transition: transform 0.3s ease-out;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-auto"
|
||||
>
|
||||
<b-card
|
||||
v-for="m in modules"
|
||||
:key="m.moduleID"
|
||||
header-class="d-flex justify-content-between bg-white border-bottom"
|
||||
class="border shadow-sm mb-3"
|
||||
>
|
||||
<template #header>
|
||||
<b-form-group
|
||||
:label="$t('module')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ m.module }}
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
:label="$t('namespace')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ m.namespace }}
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<h6
|
||||
v-if="!m.records.length"
|
||||
class="text-center"
|
||||
>
|
||||
{{ $t('no-records') }}
|
||||
</h6>
|
||||
|
||||
<div
|
||||
v-for="(r, ri) in m.records"
|
||||
:key="r.recordID"
|
||||
class="mb-0"
|
||||
>
|
||||
<b-form-group
|
||||
label="RecordID"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ r.recordID }}
|
||||
</b-form-group>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
v-for="value in r.values"
|
||||
:key="value.name"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="value.name"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<slot
|
||||
:namespace="{ namespaceID: m.namespaceID, name: m.namespace }"
|
||||
:module="{ moduleID: m.moduleID, name: m.module }"
|
||||
:record="r"
|
||||
:value="value"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr
|
||||
v-if="ri < m.records.length - 1"
|
||||
>
|
||||
</div>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'module-records',
|
||||
},
|
||||
|
||||
props: {
|
||||
modules: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<l-map
|
||||
:zoom="zoom"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
:attribution="attribution"
|
||||
/>
|
||||
|
||||
<l-marker
|
||||
v-for="connection in validMarkers"
|
||||
:key="connection.name"
|
||||
:lat-lng="getLocationCoordinates(connection)"
|
||||
>
|
||||
<l-tooltip
|
||||
:options="{
|
||||
offset: [-14, 10],
|
||||
direction: 'bottom',
|
||||
}"
|
||||
>
|
||||
<h5
|
||||
class="text-primary"
|
||||
>
|
||||
{{ $t('server-details') }}
|
||||
</h5>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ connection.meta.name }}
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('location')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ getLocationName(connection) }}
|
||||
</b-form-group>
|
||||
</l-tooltip>
|
||||
</l-marker>
|
||||
</l-map>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { latLng } from 'leaflet'
|
||||
import { LMap, LTileLayer, LMarker, LTooltip } from 'vue2-leaflet'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'map',
|
||||
},
|
||||
|
||||
components: {
|
||||
LMap,
|
||||
LTileLayer,
|
||||
LMarker,
|
||||
LTooltip,
|
||||
},
|
||||
|
||||
props: {
|
||||
connections: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
zoom: 2,
|
||||
center: [47.313220, -1.319482],
|
||||
rotation: 0,
|
||||
attribution: '© <a target="_blank" rel="noopener noreferrer" href="http://osm.org/copyright">OpenStreetMap</a>',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
validMarkers () {
|
||||
return this.connections.filter(({ meta = {} }) => {
|
||||
const { location = {} } = meta
|
||||
const { geometry = {} } = location
|
||||
const { coordinates = [] } = geometry
|
||||
|
||||
return coordinates && !!coordinates.length
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLocationCoordinates ({ meta = {} }) {
|
||||
const { location = {} } = meta
|
||||
const { geometry = {} } = location
|
||||
return this.getLatLng(geometry.coordinates)
|
||||
},
|
||||
|
||||
getLocationName (connection) {
|
||||
return connection.meta.location.properties.name || this.$t('unnamed-location')
|
||||
},
|
||||
|
||||
getLatLng (coordinates = [0, 0]) {
|
||||
return latLng(coordinates[0], coordinates[1])
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vl-style-text {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="bg-white border-bottom"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<template #header>
|
||||
<h5 class="d-flex align-items-center justify-content-between mb-0">
|
||||
{{ $t('label') }}
|
||||
|
||||
<b-dropdown
|
||||
variant="link"
|
||||
toggle-class="text-muted text-decoration-none"
|
||||
:text="sort.includes('DESC') ? $t('sort.first.newest') : $t('sort.first.oldest')"
|
||||
>
|
||||
<b-dropdown-item
|
||||
:disabled="sort.includes('DESC')"
|
||||
@click="$emit('sort', 'createdAt DESC')"
|
||||
>
|
||||
{{ $t('sort.first.newest') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
:disabled="!sort.includes('DESC')"
|
||||
@click="$emit('sort', 'createdAt')"
|
||||
>
|
||||
{{ $t('sort.first.oldest') }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</h5>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<b-form-textarea
|
||||
id="textarea"
|
||||
v-model="comment"
|
||||
:placeholder="$t('enter')"
|
||||
rows="2"
|
||||
class="mb-2"
|
||||
/>
|
||||
<b-button
|
||||
variant="primary"
|
||||
:disabled="!comment"
|
||||
class="ml-auto"
|
||||
@click="submitComment()"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<hr v-if="comments.length || processing">
|
||||
|
||||
<div
|
||||
v-if="processing"
|
||||
class="d-flex align-items-center justify-content-center py-3"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(c, index) in comments"
|
||||
:key="c.commentID"
|
||||
:class="{ 'mt-3': index }"
|
||||
class="overflow-auto"
|
||||
>
|
||||
<div class="d-flex align-items-center flex-wrap border p-2">
|
||||
<h6 class="text-primary mb-0">
|
||||
<b-spinner
|
||||
v-if="formatting"
|
||||
small
|
||||
/>
|
||||
|
||||
<span v-else>
|
||||
{{ formattedUsers[c.createdBy] || $t('unknown.user') }}
|
||||
</span>
|
||||
</h6>
|
||||
<span class="ml-auto text-muted">
|
||||
{{ formatDate(c.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="border p-3"
|
||||
>
|
||||
{{ c.comment }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fmt, NoID } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'comments',
|
||||
},
|
||||
|
||||
props: {
|
||||
comments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
processing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
sort: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
comment: '',
|
||||
|
||||
formatting: false,
|
||||
formattedUsers: {},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
comments: {
|
||||
immediate: true,
|
||||
handler (comments) {
|
||||
this.formatUsers(comments)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
submitComment () {
|
||||
this.$emit('submit', this.comment)
|
||||
this.comment = ''
|
||||
},
|
||||
|
||||
formatDate (date) {
|
||||
return date ? fmt.fullDateTime(date.toLocaleString()) : this.$t('unknown.date')
|
||||
},
|
||||
|
||||
formatUsers (comments = []) {
|
||||
const userID = []
|
||||
|
||||
comments.forEach(({ createdBy }) => {
|
||||
if (createdBy !== NoID && !this.formattedUsers[createdBy]) {
|
||||
userID.push(createdBy)
|
||||
}
|
||||
})
|
||||
|
||||
if (userID.length) {
|
||||
this.formatting = true
|
||||
|
||||
this.$SystemAPI.userList({ userID })
|
||||
.then(({ set }) => {
|
||||
set.forEach(({ userID, name, username, email, handle }) => {
|
||||
this.$set(this.formattedUsers, userID, name || username || email || handle || userID || '')
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.formatting = false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex flex-column h-100"
|
||||
>
|
||||
<b-card
|
||||
class="shadow-sm mb-4"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('connection.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<vue-select
|
||||
v-model="connection"
|
||||
:disabled="processing.connections"
|
||||
:options="connections"
|
||||
:clearable="false"
|
||||
:get-option-label="({ handle, meta }) => meta.name || handle"
|
||||
:placeholder="$t('connection.placeholder')"
|
||||
class="h-100 bg-white"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
|
||||
<div
|
||||
v-if="processing.sensitiveData"
|
||||
class="d-flex align-items-center justify-content-center h-100"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<h5
|
||||
v-else-if="!(connection && modules[connection.connectionID])"
|
||||
class="text-center mt-5"
|
||||
>
|
||||
{{ $t('no-data-available') }}
|
||||
</h5>
|
||||
|
||||
<module-records
|
||||
v-else
|
||||
v-slot="{ namespace, module, record, value }"
|
||||
:modules="modules[connection.connectionID]"
|
||||
>
|
||||
<template v-if="value.value.length">
|
||||
<b-form-input
|
||||
v-for="(v, vi) in value.value"
|
||||
:key="vi"
|
||||
:value="v"
|
||||
class="mb-1"
|
||||
@update="updateValue({ namespace, module, recordID: record.recordID, field: value.name, value: $event, orgValue: v })"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<b-form-input
|
||||
v-else
|
||||
:value="value.value[0]"
|
||||
class="mb-1"
|
||||
@update="updateValue({ namespace, module, recordID: record.recordID, field: value.name, value: $event, orgValue: '' })"
|
||||
/>
|
||||
</module-records>
|
||||
|
||||
<portal to="editor-toolbar">
|
||||
<editor-toolbar
|
||||
:processing="processing.connections || processing.sensitiveData"
|
||||
:back-link="{ name: 'data-overview.application' }"
|
||||
submit-show
|
||||
:submit-label="$t('submit')"
|
||||
:submit-disabled="!valid || !connection"
|
||||
@submit="$emit('submit', { kind: 'correct', payload })"
|
||||
/>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditorToolbar from 'corteza-webapp-privacy/src/components/Common/EditorToolbar'
|
||||
import ModuleRecords from 'corteza-webapp-privacy/src/components/Common/ModuleRecords'
|
||||
import VueSelect from 'vue-select'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'edit.correct',
|
||||
},
|
||||
|
||||
components: {
|
||||
VueSelect,
|
||||
EditorToolbar,
|
||||
ModuleRecords,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: {
|
||||
connections: true,
|
||||
sensitiveData: true,
|
||||
},
|
||||
|
||||
connection: undefined,
|
||||
|
||||
connections: [],
|
||||
|
||||
modules: {},
|
||||
|
||||
payload: {},
|
||||
valid: false,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
connection: {
|
||||
handler ({ connectionID } = {}) {
|
||||
this.fetchSensitiveData(connectionID)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchConnections()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchConnections () {
|
||||
this.processing.connections = true
|
||||
|
||||
this.$SystemAPI.dataPrivacyConnectionList()
|
||||
.then(({ set = [] }) => {
|
||||
this.connections = set
|
||||
if (!this.$route.params.connection) {
|
||||
this.connection = set[0]
|
||||
} else {
|
||||
this.connection = this.$route.params.connection
|
||||
}
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:connection-load-failed')))
|
||||
.finally(() => {
|
||||
this.processing.connections = false
|
||||
})
|
||||
},
|
||||
|
||||
fetchSensitiveData (connectionID) {
|
||||
if (connectionID) {
|
||||
this.processing.sensitiveData = true
|
||||
|
||||
this.$ComposeAPI.dataPrivacyRecordList({ connectionID: [connectionID] })
|
||||
.then(({ set = [] }) => {
|
||||
if (set.length) {
|
||||
this.$set(this.modules, connectionID, set)
|
||||
}
|
||||
|
||||
// Reset payload
|
||||
this.payload = {
|
||||
connectionID,
|
||||
modules: {},
|
||||
}
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:sensitive-data-fetch-failed')))
|
||||
.finally(() => {
|
||||
this.processing.sensitiveData = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
updateValue ({ namespace, module, recordID, field, value, orgValue }) {
|
||||
const { connectionID } = this.connection
|
||||
const { namespaceID, name: namespaceName } = namespace
|
||||
const { moduleID, name: moduleName } = module
|
||||
|
||||
if (value === orgValue) {
|
||||
delete this.payload.modules[moduleID].records[recordID].values[field]
|
||||
|
||||
if (Object.keys(this.payload.modules[moduleID].records[recordID].values).length === 0) {
|
||||
delete this.payload.modules[moduleID].records[recordID]
|
||||
|
||||
if (Object.keys(this.payload.modules[moduleID].records).length === 0) {
|
||||
delete this.payload.modules[moduleID]
|
||||
}
|
||||
}
|
||||
|
||||
this.valid = Object.keys(this.payload.modules).length > 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (connectionID) {
|
||||
if (!this.payload.modules[moduleID]) {
|
||||
this.payload.modules[moduleID] = {
|
||||
namespace: namespaceName,
|
||||
namespaceID,
|
||||
module: moduleName,
|
||||
moduleID,
|
||||
records: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.payload.modules[moduleID].records[recordID]) {
|
||||
this.payload.modules[moduleID].records[recordID] = {
|
||||
values: {},
|
||||
}
|
||||
}
|
||||
|
||||
this.payload.modules[moduleID].records[recordID].values[field] = [value]
|
||||
this.valid = true
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex flex-column h-100"
|
||||
>
|
||||
<b-card
|
||||
class="shadow-sm mb-3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('connection.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<vue-select
|
||||
v-model="connection"
|
||||
:disabled="processing.connections"
|
||||
:options="connections"
|
||||
:clearable="false"
|
||||
:get-option-label="({ handle, meta }) => meta.name || handle"
|
||||
:placeholder="$t('connection.placeholder')"
|
||||
class="h-100 bg-white"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
|
||||
<div
|
||||
v-if="processing.sensitiveData"
|
||||
class="d-flex align-items-center justify-content-center h-100"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<h5
|
||||
v-else-if="!(connection && modules[connection.connectionID])"
|
||||
class="text-center mt-5"
|
||||
>
|
||||
{{ $t('no-data-available') }}
|
||||
</h5>
|
||||
|
||||
<module-records
|
||||
v-else
|
||||
v-slot="{ namespace, module, record, value }"
|
||||
:modules="modules[connection.connectionID]"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-if="value.value.length"
|
||||
v-model="value.selected"
|
||||
@change="updateValue({ namespace, module, recordID: record.recordID, field: value.name, value: $event, orgValue: value.value })"
|
||||
>
|
||||
<span
|
||||
v-for="(v, vi) in value.value"
|
||||
:key="vi"
|
||||
class="py-2 mb-0"
|
||||
>
|
||||
<del
|
||||
v-if="value.selected"
|
||||
>
|
||||
{{ v }}
|
||||
</del>
|
||||
<span
|
||||
v-else
|
||||
>
|
||||
{{ v }}
|
||||
</span>
|
||||
</span>
|
||||
</b-form-checkbox>
|
||||
</module-records>
|
||||
|
||||
<portal to="editor-toolbar">
|
||||
<editor-toolbar
|
||||
:processing="processing.connections || processing.sensitiveData"
|
||||
:back-link="{ name: 'data-overview.application' }"
|
||||
submit-show
|
||||
:submit-label="$t('submit')"
|
||||
:submit-disabled="!valid || !connection"
|
||||
@submit="$emit('submit', { kind: 'delete', payload })"
|
||||
/>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditorToolbar from 'corteza-webapp-privacy/src/components/Common/EditorToolbar'
|
||||
import ModuleRecords from 'corteza-webapp-privacy/src/components/Common/ModuleRecords'
|
||||
import VueSelect from 'vue-select'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'edit.delete',
|
||||
},
|
||||
|
||||
components: {
|
||||
VueSelect,
|
||||
EditorToolbar,
|
||||
ModuleRecords,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: {
|
||||
connections: true,
|
||||
sensitiveData: true,
|
||||
},
|
||||
|
||||
connection: undefined,
|
||||
|
||||
connections: [],
|
||||
|
||||
modules: {},
|
||||
|
||||
payload: {},
|
||||
valid: false,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
connection: {
|
||||
handler ({ connectionID } = {}) {
|
||||
this.fetchSensitiveData(connectionID)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchConnections()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchConnections () {
|
||||
this.processing.connections = true
|
||||
|
||||
this.$SystemAPI.dataPrivacyConnectionList()
|
||||
.then(({ set = [] }) => {
|
||||
this.connections = set
|
||||
if (!this.$route.params.connection) {
|
||||
this.connection = set[0]
|
||||
} else {
|
||||
this.connection = this.$route.params.connection
|
||||
}
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:connection-load-failed')))
|
||||
.finally(() => {
|
||||
this.processing.connections = false
|
||||
})
|
||||
},
|
||||
|
||||
fetchSensitiveData (connectionID) {
|
||||
if (connectionID) {
|
||||
this.processing.sensitiveData = true
|
||||
|
||||
this.$ComposeAPI.dataPrivacyRecordList({ connectionID: [connectionID] })
|
||||
.then(({ set = [] }) => {
|
||||
if (set.length) {
|
||||
this.$set(this.modules, connectionID, set)
|
||||
}
|
||||
|
||||
// Reset payload
|
||||
this.payload = {
|
||||
connectionID,
|
||||
modules: {},
|
||||
}
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:sensitive-data-fetch-failed')))
|
||||
.finally(() => {
|
||||
this.processing.sensitiveData = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
updateValue ({ namespace, module, recordID, field, value, orgValue = [] }) {
|
||||
const { connectionID } = this.connection
|
||||
const { namespaceID, name: namespaceName } = namespace
|
||||
const { moduleID, name: moduleName } = module
|
||||
|
||||
if (!value) {
|
||||
delete this.payload.modules[moduleID].records[recordID].values[field]
|
||||
|
||||
if (Object.keys(this.payload.modules[moduleID].records[recordID].values).length === 0) {
|
||||
delete this.payload.modules[moduleID].records[recordID]
|
||||
|
||||
if (Object.keys(this.payload.modules[moduleID].records).length === 0) {
|
||||
delete this.payload.modules[moduleID]
|
||||
}
|
||||
}
|
||||
|
||||
this.valid = Object.keys(this.payload.modules).length > 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (connectionID) {
|
||||
if (!this.payload.modules[moduleID]) {
|
||||
this.payload.modules[moduleID] = {
|
||||
namespace: namespaceName,
|
||||
namespaceID,
|
||||
module: moduleName,
|
||||
moduleID,
|
||||
records: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.payload.modules[moduleID].records[recordID]) {
|
||||
this.payload.modules[moduleID].records[recordID] = {
|
||||
values: {},
|
||||
}
|
||||
}
|
||||
|
||||
this.payload.modules[moduleID].records[recordID].values[field] = orgValue
|
||||
this.valid = true
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex flex-column h-100"
|
||||
>
|
||||
<b-card
|
||||
class="shadow-sm mb-3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('data-type.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="payload.profile"
|
||||
class="ml-2 mb-1"
|
||||
>
|
||||
{{ $t('data-type.profile-information') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="payload.application"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ $t('data-type.application-data') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('date-range.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="payload.range"
|
||||
:options="rangeOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('file-format.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="payload.format"
|
||||
:options="formatOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
|
||||
<portal to="editor-toolbar">
|
||||
<editor-toolbar
|
||||
:processing="processing"
|
||||
:back-link="{ name: 'data-overview.application' }"
|
||||
submit-show
|
||||
:submit-label="$t('submit')"
|
||||
:submit-disabled="!(payload.profile || payload.application)"
|
||||
@submit="$emit('submit', { kind: 'export', payload })"
|
||||
/>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditorToolbar from 'corteza-webapp-privacy/src/components/Common/EditorToolbar'
|
||||
|
||||
export default {
|
||||
name: 'ApplicationDataDelete',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'edit.export',
|
||||
},
|
||||
|
||||
components: {
|
||||
EditorToolbar,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
|
||||
payload: {
|
||||
profile: false,
|
||||
application: false,
|
||||
range: 'all',
|
||||
format: 'json',
|
||||
},
|
||||
|
||||
rangeOptions: [
|
||||
{ text: this.$t('date-range.all'), value: 'all' },
|
||||
],
|
||||
|
||||
formatOptions: [
|
||||
{ text: this.$t('file-format.json'), value: 'json' },
|
||||
{ text: this.$t('file-format.csv'), value: 'csv' },
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex flex-column h-100"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t(`request:kind.${kind}`) }}
|
||||
</portal>
|
||||
|
||||
<component
|
||||
:is="kind"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Correct from './Correct'
|
||||
import Delete from './Delete'
|
||||
import Export from './Export'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'edit',
|
||||
},
|
||||
|
||||
components: {
|
||||
Correct,
|
||||
Delete,
|
||||
Export,
|
||||
},
|
||||
|
||||
props: {
|
||||
kind: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<module-records
|
||||
v-slot="{ value }"
|
||||
:modules="payloadValues"
|
||||
>
|
||||
<p
|
||||
v-for="(v, vi) in value.value"
|
||||
:key="vi"
|
||||
class="mb-0"
|
||||
:class="{ 'mt-1': vi > 0 }"
|
||||
>
|
||||
{{ v }}
|
||||
</p>
|
||||
</module-records>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ModuleRecords from 'corteza-webapp-privacy/src/components/Common/ModuleRecords'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'view.correct',
|
||||
},
|
||||
|
||||
components: {
|
||||
ModuleRecords,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
payloadValues () {
|
||||
const { modules = {} } = this.payload || {}
|
||||
|
||||
return Object.entries(modules).map(([moduleID, { module, namespace, records = {} }]) => {
|
||||
records = Object.entries(records).map(([recordID, { values = {} }]) => {
|
||||
values = Object.entries(values).map(([name, value = []]) => {
|
||||
return { name, value }
|
||||
})
|
||||
return { recordID, values }
|
||||
})
|
||||
return { module, namespace, moduleID, records }
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<module-records
|
||||
v-slot="{ value }"
|
||||
:modules="payloadValues"
|
||||
>
|
||||
<p
|
||||
v-for="(v, vi) in value.value"
|
||||
:key="vi"
|
||||
class="mb-0"
|
||||
:class="{ 'mt-1': vi > 0 }"
|
||||
>
|
||||
{{ v }}
|
||||
</p>
|
||||
</module-records>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import ModuleRecords from 'corteza-webapp-privacy/src/components/Common/ModuleRecords'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'view.delete',
|
||||
},
|
||||
|
||||
components: {
|
||||
ModuleRecords,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
payloadValues () {
|
||||
const { modules = {} } = this.payload || {}
|
||||
|
||||
return Object.entries(modules).map(([moduleID, { module, namespace, records = {} }]) => {
|
||||
records = Object.entries(records).map(([recordID, { values = {} }]) => {
|
||||
values = Object.entries(values).map(([name, value = []]) => {
|
||||
return { name, value }
|
||||
})
|
||||
return { recordID, values }
|
||||
})
|
||||
return { module, namespace, moduleID, records }
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div
|
||||
class="p-3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('data-type.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<span
|
||||
class="ml-2"
|
||||
>
|
||||
{{ dataType }}
|
||||
</span>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('date-range.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<span
|
||||
class="ml-2"
|
||||
>
|
||||
{{ $t(`date-range.${payload.range}`) }}
|
||||
</span>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('file-format.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<span
|
||||
class="ml-2"
|
||||
>
|
||||
{{ $t(`file-format.${payload.format}`) }}
|
||||
</span>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
extends: base,
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'view.export',
|
||||
},
|
||||
|
||||
computed: {
|
||||
dataType () {
|
||||
const { profile = false, application = false } = this.payload
|
||||
return [
|
||||
{ label: this.$t('data-type.profile-information'), include: profile },
|
||||
{ label: this.$t('data-type.application-data'), include: application },
|
||||
].filter(({ include }) => include)
|
||||
.map(({ label }) => label)
|
||||
.join(', ')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
request: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
payload () {
|
||||
const { payload = [] } = this.request || {}
|
||||
return payload[0]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<b-card
|
||||
v-if="!processing"
|
||||
header-class="bg-white border-bottom"
|
||||
body-class="p-2"
|
||||
class="shadow-sm"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t(`request:kind.${request.kind}`) }}
|
||||
</portal>
|
||||
|
||||
<template #header>
|
||||
<h5
|
||||
class="d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<span>
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
<b-badge
|
||||
:variant="statusVariants[request.status]"
|
||||
pill
|
||||
class="px-2 py-1"
|
||||
>
|
||||
{{ $t(`request:status.${request.status}`) }}
|
||||
</b-badge>
|
||||
</h5>
|
||||
|
||||
<b-card-text class="text-primary">
|
||||
{{ formattedUsers[request.requestedBy] || 'Unknown user' }}
|
||||
</b-card-text>
|
||||
</template>
|
||||
|
||||
<component
|
||||
:is="request.kind"
|
||||
:request="request"
|
||||
/>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fmt, NoID } from '@cortezaproject/corteza-js'
|
||||
import base from './base'
|
||||
import Correct from './Correct'
|
||||
import Delete from './Delete'
|
||||
import Export from './Export'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'view',
|
||||
},
|
||||
|
||||
components: {
|
||||
Correct,
|
||||
Delete,
|
||||
Export,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
|
||||
formattedUsers: {},
|
||||
|
||||
statusVariants: {
|
||||
canceled: 'secondary',
|
||||
pending: 'warning',
|
||||
rejected: 'danger',
|
||||
approved: 'success',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
formattedDate () {
|
||||
return this.request ? fmt.fullDateTime(this.request.requestedAt.toLocaleString()) : 'Unknown date'
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'request.requestedBy': {
|
||||
immediate: true,
|
||||
handler (userID) {
|
||||
if (userID === this.$auth.user.userID) {
|
||||
const { name, username, email, handle } = this.$auth.user
|
||||
this.formattedUsers[userID] = name || username || email || handle || userID || ''
|
||||
return
|
||||
}
|
||||
|
||||
if (userID !== NoID && !this.formattedUsers[userID]) {
|
||||
this.processing = true
|
||||
|
||||
this.$SystemAPI.userRead({ userID })
|
||||
.then(({ name, username, email, handle }) => {
|
||||
this.formattedUsers[userID] = name || username || email || handle || userID || ''
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGripHorizontal,
|
||||
faThumbtack,
|
||||
faHome,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faThLarge,
|
||||
faCheck,
|
||||
faTimes,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faAngleDoubleLeft,
|
||||
faAngleDoubleRight,
|
||||
faSearch,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
faUser,
|
||||
faQuestionCircle,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faHome,
|
||||
faThLarge,
|
||||
faUser,
|
||||
faThumbtack,
|
||||
faQuestionCircle,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faGripHorizontal,
|
||||
faCheck,
|
||||
faTimes,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faAngleDoubleLeft,
|
||||
faAngleDoubleRight,
|
||||
faSearch,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
import Vue from 'vue'
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import PortalVue from 'portal-vue'
|
||||
import './faIcons'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
import { Icon } from 'leaflet'
|
||||
|
||||
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(PortalVue)
|
||||
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)
|
||||
@@ -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,33 @@
|
||||
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'
|
||||
|
||||
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',
|
||||
template: '<router-view/>',
|
||||
router: new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
}),
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-privacy' },
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
import app from './app'
|
||||
import './themes'
|
||||
|
||||
app()
|
||||
@@ -0,0 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import toast from './toast'
|
||||
|
||||
Vue.mixin(toast)
|
||||
@@ -0,0 +1,197 @@
|
||||
import { debounce } from 'lodash'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
/**
|
||||
* Placeholder, if component does not define own filter
|
||||
*/
|
||||
filter: {},
|
||||
|
||||
pagination: {
|
||||
limit: 10,
|
||||
pageCursor: undefined,
|
||||
prevPage: '',
|
||||
nextPage: '',
|
||||
total: 0,
|
||||
page: 1,
|
||||
},
|
||||
|
||||
// Used to save query when fetching for total if we came to the page with a pageCursor in URL
|
||||
tempQuery: undefined,
|
||||
|
||||
sorting: {},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* When fullPath for this component changes, we most likely should update our
|
||||
* filters.
|
||||
* @todo make this.filter reactive
|
||||
*/
|
||||
'$route.fullPath': {
|
||||
handler () {
|
||||
this.handleQueryParams()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.handleQueryParams(true)
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
incLoader: 'ui/incLoader',
|
||||
decLoader: 'ui/decLoader',
|
||||
}),
|
||||
|
||||
/**
|
||||
* Parses query params into list filtering params.
|
||||
* @param initial {Boolean} - used to determine it this is the initial fetch
|
||||
*/
|
||||
handleQueryParams (initial = false) {
|
||||
// Pagination
|
||||
let {
|
||||
limit = this.pagination.limit,
|
||||
pageCursor = this.pagination.pageCursor,
|
||||
prevPage = this.pagination.prevCursor,
|
||||
nextPage = this.pagination.nextCursor,
|
||||
total = this.pagination.total,
|
||||
page = this.pagination.page,
|
||||
...r1
|
||||
} = this.$route.query
|
||||
|
||||
limit = parseInt(limit)
|
||||
total = parseInt(total)
|
||||
page = parseInt(page)
|
||||
|
||||
// If we came to the page with a pageCursor in the URL
|
||||
if (initial && pageCursor) {
|
||||
this.tempQuery = this.$route.query
|
||||
// Fetch replace query to trigger fetch of total number of items
|
||||
this.$router.replace({ query: { ...this.$route.query, limit: 1, pageCursor: undefined } })
|
||||
return
|
||||
}
|
||||
|
||||
/// To prevent extra list fetch, check if pageCursor is defined (not first page)
|
||||
const refresh = this.$route.query.pageCursor !== this.pagination.pageCursor
|
||||
this.pagination = { limit, pageCursor, prevPage, nextPage, total, page }
|
||||
|
||||
// Sorting
|
||||
let { sortBy = this.sorting.sortBy, sortDesc = this.sorting.sortDesc, ...r2 } = r1
|
||||
|
||||
sortDesc = sortDesc === true || sortDesc === 'true'
|
||||
|
||||
// Reset pageCursor when sort changes, except on first fetch (so we use the pageCursor from url)
|
||||
if (!initial && (sortBy !== this.sorting.sortBy || sortDesc !== this.sorting.sortDesc)) {
|
||||
this.pagination.pageCursor = ''
|
||||
this.pagination.page = 1
|
||||
}
|
||||
this.sorting = { sortBy, sortDesc }
|
||||
|
||||
// Filtering
|
||||
// make sure filter fields are of the right type
|
||||
for (const key in r2) {
|
||||
if (typeof this.filter[key] === 'boolean') {
|
||||
r2[key] = r2[key] === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
this.filter = { ...this.filter, ...r2 }
|
||||
|
||||
// Only refresh if pageCursor actually changed,
|
||||
if (refresh) {
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
}
|
||||
},
|
||||
|
||||
filterList: debounce(function () {
|
||||
// reset pagination when filtering changes
|
||||
//
|
||||
// we want to prevent situations with page is preset to a number that
|
||||
// exceeds the number of pages of a filtered results
|
||||
this.pagination.pageCursor = ''
|
||||
this.pagination.page = 1
|
||||
|
||||
// notify b-table about the change
|
||||
//
|
||||
// this effectively calls items()/procListResults()
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
}, 300),
|
||||
|
||||
encodeListParams () {
|
||||
const { sortBy, sortDesc } = this.sorting
|
||||
const { limit, pageCursor } = this.pagination
|
||||
|
||||
const sort = sortBy ? `${sortBy} ${sortDesc ? 'DESC' : 'ASC'}` : undefined
|
||||
|
||||
return {
|
||||
limit,
|
||||
sort: pageCursor ? undefined : sort,
|
||||
...this.filter,
|
||||
pageCursor,
|
||||
incTotal: !pageCursor || this.tempQuery,
|
||||
}
|
||||
},
|
||||
|
||||
encodeRouteParams () {
|
||||
const { limit, pageCursor, page } = this.pagination
|
||||
|
||||
return {
|
||||
query: {
|
||||
limit,
|
||||
...this.sorting,
|
||||
...this.filter,
|
||||
page,
|
||||
pageCursor,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param p {Promise}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
procListResults (p, updateQuery = true) {
|
||||
// Push new router/params to cause URL change
|
||||
//
|
||||
// We want this because in case when user refreshes or shares URL
|
||||
// he needs to land on the same page with the same parameters
|
||||
if (updateQuery && !this.tempQuery) {
|
||||
this.$router.replace(this.encodeRouteParams())
|
||||
}
|
||||
|
||||
return p.then(async ({ set, filter } = {}) => {
|
||||
if (filter.incTotal) {
|
||||
this.pagination.total = filter.total
|
||||
}
|
||||
|
||||
// This was a fetch of total number of items
|
||||
if (this.tempQuery) {
|
||||
const query = this.tempQuery
|
||||
this.tempQuery = undefined
|
||||
|
||||
this.$router.replace({ query })
|
||||
return []
|
||||
}
|
||||
|
||||
this.pagination.pageCursor = undefined
|
||||
this.pagination.nextPage = filter.nextPage
|
||||
this.pagination.prevPage = filter.prevPage
|
||||
|
||||
return set
|
||||
}).catch(this.toastErrorHandler(this.$t('notification:list.load.error')))
|
||||
.finally(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
})
|
||||
},
|
||||
|
||||
genericRowClass (item) {
|
||||
return { 'text-secondary': item && !!item.deletedAt }
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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 = {}) => {
|
||||
// only messages starting with 'notification:' or 'notification.' should be translated
|
||||
if (err.message && err.message.startsWith('notification')) {
|
||||
err.message = this.$t(`notification:${err.message.substring('notification.'.length)}`)
|
||||
}
|
||||
/* eslint-disable no-console */
|
||||
console.error(err)
|
||||
// all other messages should be shown as they are
|
||||
const msg = err.message ? `${prefix}: ${err.message}` : prefix
|
||||
this.toastDanger(msg, title)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import { plugins } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
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.use(plugins.Auth(), { app: 'privacy' })
|
||||
|
||||
Vue.use(plugins.CortezaAPI('system'))
|
||||
Vue.use(plugins.CortezaAPI('compose'))
|
||||
|
||||
Vue.use(plugins.Settings, { api: Vue.prototype.$SystemAPI })
|
||||
@@ -0,0 +1,7 @@
|
||||
import Router from 'vue-router'
|
||||
import routes from './views/routes.js'
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import { store as cvStore } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
rbac: {
|
||||
namespaced: true,
|
||||
...cvStore.RBAC(Vue.prototype.$SystemAPI),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default store
|
||||
@@ -0,0 +1,249 @@
|
||||
/* 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;
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.card-hover-popup {
|
||||
transition: all 0.1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(38, 38, 38, 0.2);
|
||||
transform: translate3d(0, -2px, 0);
|
||||
}
|
||||
}
|
||||
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.
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,8 @@
|
||||
@import "./variables";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import '~bootstrap-vue/src/index.scss';
|
||||
@import '~vue-select/src/scss/vue-select.scss';
|
||||
@import "./custom";
|
||||
@import "./poppins.scss";
|
||||
@import '~leaflet/dist/leaflet.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,147 @@
|
||||
<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('navigation:help.forum'),
|
||||
helpDocumentation: $t('navigation:help.documentation'),
|
||||
helpFeedback: $t('navigation:help.feedback'),
|
||||
helpVersion: $t('navigation:help.version'),
|
||||
userSettingsLoggedInAs: $t('navigation:userSettings.loggedInAs', { user }),
|
||||
userSettingsProfile: $t('navigation:userSettings.profile'),
|
||||
userSettingsChangePassword: $t('navigation:userSettings.changePassword'),
|
||||
userSettingsLogout: $t('navigation:userSettings.logout'),
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<portal-target
|
||||
name="topbar-title"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #tools>
|
||||
<portal-target
|
||||
name="topbar-tools"
|
||||
/>
|
||||
</template>
|
||||
</c-topbar>
|
||||
</header>
|
||||
|
||||
<aside>
|
||||
<c-sidebar
|
||||
:expanded.sync="expanded"
|
||||
:pinned.sync="pinned"
|
||||
:icon="icon"
|
||||
:logo="logo"
|
||||
:disabled-routes="['dashboard']"
|
||||
>
|
||||
<template #header-expanded>
|
||||
<portal-target name="sidebar-header-expanded" />
|
||||
</template>
|
||||
|
||||
<template #body-expanded>
|
||||
<portal-target name="sidebar-body-expanded" />
|
||||
</template>
|
||||
|
||||
<template #footer-expanded>
|
||||
<portal-target name="sidebar-footer-expanded" />
|
||||
</template>
|
||||
</c-sidebar>
|
||||
</aside>
|
||||
|
||||
<main class="d-inline-flex h-100 overflow-auto">
|
||||
<!--
|
||||
Content spacer
|
||||
Large and xl screens should push in content when the nav is expanded
|
||||
-->
|
||||
<template>
|
||||
<div
|
||||
class="spacer"
|
||||
:class="{
|
||||
'expanded': expanded && pinned,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="d-flex flex-column w-100"
|
||||
>
|
||||
<router-view
|
||||
class="flex-grow-1 overflow-auto"
|
||||
/>
|
||||
|
||||
<portal-target
|
||||
name="editor-toolbar"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CTopbar, CSidebar } = components
|
||||
|
||||
export default {
|
||||
name: 'Layout',
|
||||
|
||||
components: {
|
||||
CTopbar,
|
||||
CSidebar,
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</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,161 @@
|
||||
<template>
|
||||
<b-container
|
||||
v-if="isDC !== null"
|
||||
fluid
|
||||
class="d-flex flex-column p-3"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t('title') }}
|
||||
</portal>
|
||||
|
||||
<div
|
||||
class="flex-shrink-1"
|
||||
>
|
||||
<p>
|
||||
{{ $t('description.first') }}<br>
|
||||
{{ $t('description.second') }}
|
||||
</p>
|
||||
|
||||
<b-row
|
||||
align-v="stretch"
|
||||
>
|
||||
<b-col
|
||||
v-for="option in options"
|
||||
:key="option.title"
|
||||
cols="12"
|
||||
md="6"
|
||||
xl="3"
|
||||
class="mb-3"
|
||||
>
|
||||
<b-card
|
||||
:title="option.title"
|
||||
class="card-hover-popup shadow-sm h-100"
|
||||
body-class="d-flex flex-column"
|
||||
>
|
||||
<b-card-text class="flex-grow-1">
|
||||
{{ option.description }}
|
||||
</b-card-text>
|
||||
|
||||
<b-button
|
||||
:variant="option.button.variant || 'light'"
|
||||
:to="option.button.to"
|
||||
>
|
||||
{{ option.button.label }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-flex flex-column h-100"
|
||||
>
|
||||
<h6
|
||||
class="text-primary"
|
||||
>
|
||||
{{ $t('connection-location') }}
|
||||
</h6>
|
||||
|
||||
<connection-map
|
||||
:connections="connections"
|
||||
class="align-self-center justify-self-center flex-grow-1 rounded-lg shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConnectionMap from 'corteza-webapp-privacy/src/components/ConnectionMap'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'dashboard',
|
||||
},
|
||||
|
||||
components: {
|
||||
ConnectionMap,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
|
||||
isDC: null,
|
||||
|
||||
connections: [],
|
||||
|
||||
userOptions: [
|
||||
{
|
||||
title: this.$t('user-options.data-overview.title'),
|
||||
description: this.$t('user-options.data-overview.description'),
|
||||
button: { label: this.$t('user-options.data-overview.button-label'), to: { name: 'data-overview' } },
|
||||
},
|
||||
{
|
||||
title: this.$t('user-options.privacy-requests.title'),
|
||||
description: this.$t('user-options.privacy-requests.description'),
|
||||
button: { label: this.$t('user-options.privacy-requests.button-label'), to: { name: 'request.list' } },
|
||||
},
|
||||
{
|
||||
title: this.$t('user-options.export.title'),
|
||||
description: this.$t('user-options.export.description'),
|
||||
button: { label: this.$t('user-options.export.button-label'), to: { name: 'request.create', params: { kind: 'export' } } },
|
||||
},
|
||||
|
||||
{
|
||||
title: this.$t('user-options.delete.title'),
|
||||
description: this.$t('user-options.delete.description'),
|
||||
button: { label: this.$t('user-options.delete.button-label'), variant: 'danger', to: { name: 'request.create', params: { kind: 'delete' } } },
|
||||
},
|
||||
],
|
||||
|
||||
dcOptions: [
|
||||
{
|
||||
title: this.$t('dc-options.privacy-requests.title'),
|
||||
description: this.$t('dc-options.privacy-requests.description'),
|
||||
button: { label: this.$t('dc-options.privacy-requests.button-label'), to: { name: 'request.list' } },
|
||||
},
|
||||
{
|
||||
title: this.$t('dc-options.sensitive-data.title'),
|
||||
description: this.$t('dc-options.sensitive-data.description'),
|
||||
button: { label: this.$t('dc-options.sensitive-data.button-label'), to: { name: 'sensitive-data' } },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
options () {
|
||||
return this.isDC ? this.dcOptions : this.userOptions
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchConnections()
|
||||
this.checkIsDC()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchConnections () {
|
||||
this.processing = true
|
||||
|
||||
this.$SystemAPI.dataPrivacyConnectionList()
|
||||
.then(({ set = [] }) => {
|
||||
this.connections = set
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:connection-load-failed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
|
||||
checkIsDC () {
|
||||
this.$SystemAPI.roleList({ query: 'data-privacy-officer', memberID: this.$auth.user.userID })
|
||||
.then(({ set = [] }) => {
|
||||
this.isDC = !!set.length
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<b-container
|
||||
class="d-flex flex-column p-3"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t('title') }}
|
||||
</portal>
|
||||
|
||||
<b-card
|
||||
class="shadow-sm mb-4"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('connection.label')"
|
||||
label-class="text-primary"
|
||||
class="mb-0"
|
||||
>
|
||||
<vue-select
|
||||
v-model="connection"
|
||||
:disabled="processing.connections"
|
||||
:options="connections"
|
||||
:clearable="false"
|
||||
:placeholder="$t('connection.placeholder')"
|
||||
:get-option-label="({ handle, meta }) => meta.name || handle"
|
||||
class="h-100 bg-white"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
|
||||
<div
|
||||
v-if="processing.sensitiveData"
|
||||
class="d-flex align-items-center justify-content-center h-100"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<h5
|
||||
v-else-if="!(connection && modules[connection.connectionID])"
|
||||
class="text-center mt-5"
|
||||
>
|
||||
{{ $t('no-data-available') }}
|
||||
</h5>
|
||||
|
||||
<module-records
|
||||
v-else
|
||||
v-slot="{ value }"
|
||||
:modules="modules[connection.connectionID]"
|
||||
>
|
||||
<p
|
||||
v-for="(v, vi) in value.value"
|
||||
:key="vi"
|
||||
class="mb-0"
|
||||
:class="{ 'mt-1': vi > 0 }"
|
||||
>
|
||||
{{ v }}
|
||||
</p>
|
||||
</module-records>
|
||||
|
||||
<portal to="editor-toolbar">
|
||||
<editor-toolbar
|
||||
:processing="processing.connections || processing.sensitiveData"
|
||||
:back-link="{ name: 'data-overview' }"
|
||||
>
|
||||
<b-button
|
||||
:disabled="processing.connections || processing.sensitiveData"
|
||||
variant="light"
|
||||
size="lg"
|
||||
class="ml-1"
|
||||
@click="$router.push({ name: 'request.create', params: { kind: 'delete', connection } })"
|
||||
>
|
||||
{{ $t('request-deletion') }}
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
:disabled="processing.connections || processing.sensitiveData"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
class="ml-1"
|
||||
@click="$router.push({ name: 'request.create', params: { kind: 'correct', connection } })"
|
||||
>
|
||||
{{ $t('request-correction') }}
|
||||
</b-button>
|
||||
</editor-toolbar>
|
||||
</portal>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditorToolbar from 'corteza-webapp-privacy/src/components/Common/EditorToolbar'
|
||||
import ModuleRecords from 'corteza-webapp-privacy/src/components/Common/ModuleRecords'
|
||||
import VueSelect from 'vue-select'
|
||||
|
||||
export default {
|
||||
name: 'ApplicationDataOverview',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'data-overview',
|
||||
keyPrefix: 'application',
|
||||
},
|
||||
|
||||
components: {
|
||||
VueSelect,
|
||||
EditorToolbar,
|
||||
ModuleRecords,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: {
|
||||
connections: true,
|
||||
sensitiveData: true,
|
||||
},
|
||||
|
||||
connection: undefined,
|
||||
|
||||
connections: [],
|
||||
|
||||
modules: {},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
connection: {
|
||||
handler ({ connectionID } = {}) {
|
||||
this.fetchSensitiveData(connectionID)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchConnections()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchConnections () {
|
||||
this.processing.connections = true
|
||||
|
||||
this.$SystemAPI.dataPrivacyConnectionList()
|
||||
.then(({ set = [] }) => {
|
||||
this.connections = set
|
||||
this.connection = set[0]
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:connection-load-failed')))
|
||||
.finally(() => {
|
||||
this.processing.connections = false
|
||||
})
|
||||
},
|
||||
|
||||
fetchSensitiveData (connectionID) {
|
||||
if (connectionID) {
|
||||
this.processing.sensitiveData = true
|
||||
|
||||
this.$ComposeAPI.dataPrivacyRecordList({ connectionID: [connectionID] })
|
||||
.then(({ set = [] }) => {
|
||||
if (set.length) {
|
||||
this.$set(this.modules, connectionID, set)
|
||||
}
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:sensitive-data-fetch-failed')))
|
||||
.finally(() => {
|
||||
this.processing.sensitiveData = false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<b-container
|
||||
fluid
|
||||
class="d-flex flex-column p-3"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t('title') }}
|
||||
</portal>
|
||||
|
||||
<b-row
|
||||
v-for="type in dataTypes"
|
||||
:key="type.title"
|
||||
>
|
||||
<b-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
xl="5"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
class="mb-3"
|
||||
>
|
||||
<b-card
|
||||
no-body
|
||||
class="card-hover-popup shadow-sm pointer"
|
||||
>
|
||||
<b-row no-gutters>
|
||||
<b-col
|
||||
cols="2"
|
||||
align-self="center"
|
||||
class="p-2 text-center"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="type.icon"
|
||||
class="text-primary h2 mb-0"
|
||||
/>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="9"
|
||||
>
|
||||
<b-card-body
|
||||
:title="type.title"
|
||||
class="px-2"
|
||||
>
|
||||
<b-card-text>
|
||||
{{ type.description }}
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="1"
|
||||
align-self="center"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<a
|
||||
v-if="type.href"
|
||||
:href="type.href"
|
||||
target="_blank"
|
||||
class="pointer stretched-link"
|
||||
/>
|
||||
|
||||
<router-link
|
||||
v-else-if="type.to"
|
||||
:to="type.to"
|
||||
class="pointer stretched-link"
|
||||
/>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DataOverview',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'data-overview',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
dataTypes: [
|
||||
{
|
||||
title: this.$t('data-types.profile-information.title'),
|
||||
description: this.$t('data-types.profile-information.description'),
|
||||
icon: ['far', 'user'],
|
||||
href: this.$auth.cortezaAuthURL,
|
||||
},
|
||||
{
|
||||
title: this.$t('data-types.application-data.title'),
|
||||
description: this.$t('data-types.application-data.description'),
|
||||
icon: ['fas', 'th-large'],
|
||||
to: { name: 'data-overview.application' },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<b-container
|
||||
class="d-flex flex-column p-3"
|
||||
>
|
||||
<request-editor
|
||||
v-if="kind"
|
||||
:kind="kind"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RequestEditor from 'corteza-webapp-privacy/src/components/Requests/Editor'
|
||||
|
||||
export default {
|
||||
name: 'RequestView',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'create',
|
||||
},
|
||||
|
||||
components: {
|
||||
RequestEditor,
|
||||
},
|
||||
|
||||
props: {
|
||||
kind: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit ({ kind, payload }) {
|
||||
this.processing = true
|
||||
|
||||
payload = [payload]
|
||||
|
||||
return this.$SystemAPI.dataPrivacyRequestCreate({ kind, payload })
|
||||
.then(({ requestID, kind } = {}) => {
|
||||
this.$router.push({ name: 'request.view', params: { requestID, kind } })
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:list.load.error')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<b-container
|
||||
v-if="isDC !== null"
|
||||
fluid
|
||||
class="d-flex flex-column p-3"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t('title') }}
|
||||
</portal>
|
||||
|
||||
<c-resource-list
|
||||
primary-key="requestID"
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
:filter="filter"
|
||||
:sorting="sorting"
|
||||
:pagination="pagination"
|
||||
:translations="{
|
||||
notFound: $t('general:resourceList.notFound'),
|
||||
noItems: $t('general:resourceList.noItems'),
|
||||
loading: $t('general:resourceList.loading'),
|
||||
searchPlaceholder: $t('general:resourceList.search.placeholder'),
|
||||
showingPagination: 'general:resourceList.pagination.showing',
|
||||
singlePluralPagination: 'general:resourceList.pagination.single',
|
||||
prevPagination: $t('general:resourceList.pagination.prev'),
|
||||
nextPagination: $t('general:resourceList.pagination.next'),
|
||||
}"
|
||||
:is-item-selectable="isItemSelectable"
|
||||
selectable
|
||||
clickable
|
||||
hide-total
|
||||
class="flex-grow-1"
|
||||
@search="filterList"
|
||||
@row-clicked="rowClicked"
|
||||
>
|
||||
<template #header="{ selected = [] }">
|
||||
<template v-if="isDC">
|
||||
<c-input-confirm
|
||||
v-if="isDC"
|
||||
:disabled="processing || !selected.length"
|
||||
:borderless="false"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
@confirmed="handleSelectedRequests(selected, 'approved')"
|
||||
>
|
||||
{{ $t('request.approve') }}
|
||||
</c-input-confirm>
|
||||
<c-input-confirm
|
||||
v-if="isDC"
|
||||
:disabled="processing || !selected.length"
|
||||
:borderless="false"
|
||||
variant="danger"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
class="ml-1"
|
||||
@confirmed="handleSelectedRequests(selected, 'rejected')"
|
||||
>
|
||||
{{ $t('request.reject') }}
|
||||
</c-input-confirm>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<c-input-confirm
|
||||
:borderless="false"
|
||||
:disabled="processing || !selected.length"
|
||||
variant="light"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
@confirmed="handleSelectedRequests(selected, 'canceled')"
|
||||
>
|
||||
{{ $t('request.cancel') }}
|
||||
</c-input-confirm>
|
||||
|
||||
<!-- <b-button
|
||||
:disabled="processing"
|
||||
variant="light"
|
||||
size="lg"
|
||||
class="ml-1"
|
||||
@click="exportRequests()"
|
||||
>
|
||||
{{ $t('export') }}
|
||||
</b-button> -->
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #status="{ item }">
|
||||
<div
|
||||
class="d-flex align-items-baseline"
|
||||
>
|
||||
<span
|
||||
class="d-inline-block rounded-circle mr-1"
|
||||
:class="`bg-${statusVariants[item.status]}`"
|
||||
style="width: 0.6rem; height: 0.6rem;"
|
||||
/>
|
||||
{{ $t(`request:status.${item.status}`) }}
|
||||
</div>
|
||||
</template>
|
||||
</c-resource-list>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import listHelpers from 'corteza-webapp-privacy/src/mixins/listHelpers'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CResourceList } = components
|
||||
|
||||
export default {
|
||||
name: 'RequestList',
|
||||
|
||||
components: {
|
||||
CResourceList,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
listHelpers,
|
||||
],
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'list',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
isDC: null,
|
||||
|
||||
users: {},
|
||||
|
||||
filter: {
|
||||
requestedBy: [],
|
||||
},
|
||||
|
||||
sorting: {
|
||||
sortBy: 'requestedAt',
|
||||
sortDesc: true,
|
||||
},
|
||||
|
||||
statusVariants: {
|
||||
canceled: 'secondary',
|
||||
pending: 'warning',
|
||||
rejected: 'danger',
|
||||
approved: 'success',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fields () {
|
||||
return [
|
||||
{
|
||||
key: 'kind',
|
||||
sortable: true,
|
||||
formatter: kind => this.$t(`request:kind.${kind}`),
|
||||
},
|
||||
{
|
||||
key: 'requestedAt',
|
||||
sortable: true,
|
||||
formatter: requestedAt => moment(requestedAt).fromNow(),
|
||||
},
|
||||
{
|
||||
hide: !this.isDC,
|
||||
key: 'requestedBy',
|
||||
sortable: false,
|
||||
formatter: requestedBy => this.formatUser(requestedBy),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
sortable: true,
|
||||
},
|
||||
].filter(({ hide }) => !hide)
|
||||
.map(c => ({
|
||||
...c,
|
||||
// Generate column label translation key
|
||||
label: c.label || this.$t(`columns.${c.key}`),
|
||||
}))
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.checkIsDC()
|
||||
},
|
||||
|
||||
methods: {
|
||||
checkIsDC () {
|
||||
this.$SystemAPI.roleList({ query: 'data-privacy-officer', memberID: this.$auth.user.userID })
|
||||
.then(({ set = [] }) => {
|
||||
if (!set.length) {
|
||||
this.filter.requestedBy = [this.$auth.user.userID]
|
||||
}
|
||||
|
||||
this.isDC = !!set.length
|
||||
})
|
||||
},
|
||||
|
||||
encodeRouteParams () {
|
||||
const { query } = this.filter
|
||||
const { limit, pageCursor, page } = this.pagination
|
||||
|
||||
return {
|
||||
query: {
|
||||
limit,
|
||||
...this.sorting,
|
||||
query,
|
||||
page,
|
||||
pageCursor,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
items () {
|
||||
return this.procListResults(this.$SystemAPI.dataPrivacyRequestList(this.encodeListParams())
|
||||
.then(async ({ filter, set }) => {
|
||||
if (this.isDC) {
|
||||
await this.fetchUsers(set.map(({ requestedBy }) => requestedBy))
|
||||
}
|
||||
return { filter, set }
|
||||
}))
|
||||
},
|
||||
|
||||
handleSelectedRequests (selected, status) {
|
||||
this.processing = true
|
||||
|
||||
Promise.all(selected.map(requestID => {
|
||||
return this.$SystemAPI.dataPrivacyRequestUpdateStatus({ requestID, status })
|
||||
}))
|
||||
.then(() => {
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = true
|
||||
})
|
||||
},
|
||||
|
||||
fetchUsers (userID = []) {
|
||||
userID = [...new Set(userID)]
|
||||
return this.$SystemAPI.userList({ userID })
|
||||
.then(({ set }) => {
|
||||
set.forEach(user => {
|
||||
this.users[user.userID] = user
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
isItemSelectable (item) {
|
||||
return item.status === 'pending'
|
||||
},
|
||||
|
||||
formatUser (userID) {
|
||||
const { name, username, email, handle } = this.users[userID]
|
||||
return name || username || email || handle || userID || ''
|
||||
},
|
||||
|
||||
rowClicked ({ requestID, kind }) {
|
||||
this.$router.push({ name: 'request.view', params: { requestID, kind } })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<b-container
|
||||
class="d-flex flex-column p-3"
|
||||
>
|
||||
<div
|
||||
v-if="processing.request"
|
||||
class="d-flex align-items-center justify-content-center h-100"
|
||||
>
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<template v-else-if="request">
|
||||
<request-viewer
|
||||
:request="request"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<request-comments
|
||||
:comments="comments"
|
||||
:processing="processing.comments"
|
||||
:sort="sort"
|
||||
@sort="sort = $event"
|
||||
@submit="submitComment"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<portal to="editor-toolbar">
|
||||
<editor-toolbar
|
||||
:processing="processing.toolbar"
|
||||
:back-link="{ name: 'request.list' }"
|
||||
:delete-show="isDC"
|
||||
:delete-disabled="!request || !isPending"
|
||||
:delete-label="$t('reject')"
|
||||
@delete="handleRequest('rejected')"
|
||||
>
|
||||
<c-input-confirm
|
||||
v-if="!isDC"
|
||||
:borderless="false"
|
||||
:disabled="!request || !isPending"
|
||||
variant="light"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
@confirmed="handleRequest('canceled')"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</c-input-confirm>
|
||||
|
||||
<c-input-confirm
|
||||
v-else
|
||||
:borderless="false"
|
||||
:disabled="!request || !isPending"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
class="ml-2"
|
||||
@confirmed="handleRequest('approved')"
|
||||
>
|
||||
{{ $t('approve') }}
|
||||
</c-input-confirm>
|
||||
</editor-toolbar>
|
||||
</portal>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditorToolbar from 'corteza-webapp-privacy/src/components/Common/EditorToolbar'
|
||||
import RequestViewer from 'corteza-webapp-privacy/src/components/Requests/Viewer'
|
||||
import RequestComments from 'corteza-webapp-privacy/src/components/Requests/Comments'
|
||||
|
||||
export default {
|
||||
name: 'RequestView',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'view',
|
||||
},
|
||||
|
||||
components: {
|
||||
EditorToolbar,
|
||||
RequestViewer,
|
||||
RequestComments,
|
||||
},
|
||||
|
||||
props: {
|
||||
requestID: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: {
|
||||
comments: false,
|
||||
request: false,
|
||||
toolbar: false,
|
||||
},
|
||||
|
||||
isDC: null,
|
||||
|
||||
sort: 'createdAt DESC',
|
||||
|
||||
request: undefined,
|
||||
comments: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isPending () {
|
||||
return this.request.status === 'pending'
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
requestID: {
|
||||
immediate: true,
|
||||
handler () {
|
||||
if (this.requestID) {
|
||||
this.fetchRequest()
|
||||
this.fetchComments()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
sort: {
|
||||
handler () {
|
||||
if (this.requestID) {
|
||||
this.fetchComments()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.checkIsDC()
|
||||
},
|
||||
|
||||
methods: {
|
||||
checkIsDC () {
|
||||
this.$SystemAPI.roleList({ query: 'data-privacy-officer', memberID: this.$auth.user.userID })
|
||||
.then(({ set = [] }) => {
|
||||
this.isDC = !!set.length
|
||||
})
|
||||
},
|
||||
|
||||
fetchRequest (requestID = this.requestID) {
|
||||
this.processing.request = true
|
||||
|
||||
return this.$SystemAPI.dataPrivacyRequestRead({ requestID })
|
||||
.then(request => {
|
||||
this.request = request
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:list.load.error')))
|
||||
.finally(() => {
|
||||
this.processing.request = false
|
||||
})
|
||||
},
|
||||
|
||||
fetchComments (requestID = this.requestID) {
|
||||
this.processing.comments = true
|
||||
|
||||
return this.$SystemAPI.dataPrivacyRequestCommentList({ requestID, sort: this.sort })
|
||||
.then(({ set }) => {
|
||||
this.comments = set
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:list.load.error')))
|
||||
.finally(() => {
|
||||
this.processing.comments = false
|
||||
})
|
||||
},
|
||||
|
||||
handleRequest (status) {
|
||||
this.processing.toolbar = true
|
||||
|
||||
this.$SystemAPI.dataPrivacyRequestUpdateStatus({ requestID: this.requestID, status })
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'request.list' })
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing.toolbar = false
|
||||
})
|
||||
},
|
||||
|
||||
submitComment (comment) {
|
||||
this.processing.comments = true
|
||||
|
||||
this.$SystemAPI.dataPrivacyRequestCommentCreate({ requestID: this.requestID, comment })
|
||||
.then(() => {
|
||||
return this.fetchComments()
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing.comments = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<b-container
|
||||
fluid
|
||||
class="d-flex flex-column p-3"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t('title') }}
|
||||
</portal>
|
||||
|
||||
<c-resource-list
|
||||
primary-key="moduleID"
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
:filter="filter"
|
||||
:sorting="sorting"
|
||||
:pagination="pagination"
|
||||
:selectable="false"
|
||||
:translations="{
|
||||
noItems: $t('general:resourceList.noItems'),
|
||||
loading: $t('general:resourceList.loading'),
|
||||
searchPlaceholder: $t('general:resourceList.search.placeholder'),
|
||||
showingPagination: 'general:resourceList.pagination.showing',
|
||||
singlePluralPagination: 'general:resourceList.pagination.single',
|
||||
prevPagination: $t('general:resourceList.pagination.prev'),
|
||||
nextPagination: $t('general:resourceList.pagination.next'),
|
||||
}"
|
||||
clickable
|
||||
hide-total
|
||||
class="flex-grow-1"
|
||||
@search="filterList"
|
||||
@row-clicked="rowClicked"
|
||||
/>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import listHelpers from 'corteza-webapp-privacy/src/mixins/listHelpers'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CResourceList } = components
|
||||
|
||||
export default {
|
||||
name: 'SensitiveData',
|
||||
|
||||
components: {
|
||||
CResourceList,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
listHelpers,
|
||||
],
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'sensitive-data',
|
||||
keyPrefix: 'list',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
fields: [
|
||||
{
|
||||
key: 'module',
|
||||
formatter: module => {
|
||||
return module ? module.name : ''
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'namespace',
|
||||
formatter: namespace => {
|
||||
return namespace ? namespace.name : ''
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'connection',
|
||||
formatter: connection => {
|
||||
const { name } = connection.meta || {}
|
||||
return name
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'location',
|
||||
formatter: (value, key, item) => {
|
||||
const { meta = {} } = item.connection || {}
|
||||
const { properties = {} } = meta.location || {}
|
||||
return properties.name
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ownership',
|
||||
formatter: (value, key, item) => {
|
||||
const { meta = {} } = item.connection || {}
|
||||
const { ownership } = meta || {}
|
||||
return ownership
|
||||
},
|
||||
},
|
||||
].map(c => ({
|
||||
...c,
|
||||
// Generate column label translation key
|
||||
label: c.label || this.$t(`columns.${c.key}`),
|
||||
})),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
items () {
|
||||
return this.procListResults(this.$ComposeAPI.dataPrivacyModuleList(this.encodeListParams()))
|
||||
},
|
||||
|
||||
rowClicked ({ namespace, module }) {
|
||||
window.open(`/compose/ns/${namespace.slug}/admin/modules/${module.moduleID}/edit`, '_blank')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
name: 'root',
|
||||
redirect: 'dashboard',
|
||||
component: () => import('./Layout'),
|
||||
children: [
|
||||
{ name: 'dashboard', path: '/dashboard', component: () => import('./Privacy/Dashboard') },
|
||||
{ name: 'sensitive-data', path: '/sensitive-data', component: () => import('./Privacy/SensitiveData') },
|
||||
{ name: 'data-overview', path: '/data-overview', component: () => import('./Privacy/DataOverview/') },
|
||||
{ name: 'data-overview.application', path: '/data-overview/application', component: () => import('./Privacy/DataOverview/Application') },
|
||||
{ name: 'request.list', path: '/request/list', component: () => import('./Privacy/Request/List') },
|
||||
{ name: 'request.view', path: '/request/:requestID?', component: () => import('./Privacy/Request/View'), props: true },
|
||||
{ name: 'request.create', path: '/request/:kind/new', component: () => import('./Privacy/Request/Create'), props: true },
|
||||
],
|
||||
},
|
||||
|
||||
{ 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,128 @@
|
||||
const webpack = require('webpack')
|
||||
const exec = require('child_process').execSync
|
||||
const 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 || 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')
|
||||
options[0][0].ignore.push('*gitignore')
|
||||
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: 'Privacy',
|
||||
appName: 'privacy',
|
||||
appLabel: 'Corteza Privacy',
|
||||
theme: 'corteza-base',
|
||||
packageAlias: 'corteza-webapp-privacy',
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user