snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
150
client/web/privacy/src/app.js
Normal file
150
client/web/privacy/src/app.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import './config-check'
|
||||
import './console-splash'
|
||||
|
||||
import './plugins'
|
||||
import './mixins'
|
||||
import './components'
|
||||
import './filters'
|
||||
|
||||
import store from './store'
|
||||
|
||||
import { i18n, websocket } from '@cortezaproject/corteza-vue'
|
||||
|
||||
import router from './router'
|
||||
|
||||
export default (options = {}) => {
|
||||
options = {
|
||||
el: '#app',
|
||||
name: 'privacy',
|
||||
|
||||
template: '<router-view v-if="loaded && i18nLoaded" />',
|
||||
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
i18nLoaded: false,
|
||||
}),
|
||||
|
||||
async created () {
|
||||
this.$i18n.i18next.on('initialized', () => {
|
||||
this.i18nLoaded = true
|
||||
})
|
||||
|
||||
this.websocket()
|
||||
|
||||
return this.$auth.vue(this).handle().then(async ({ user }) => {
|
||||
// switch the favicon based on the settings
|
||||
await this.$Settings.init({ api: this.$SystemAPI }).then(() => {
|
||||
const icon = this.$Settings.attachment('ui.iconLogo') || '/icon.svg'
|
||||
|
||||
const favicon = document.getElementById('favicon')
|
||||
|
||||
if (favicon) {
|
||||
favicon.href = icon
|
||||
}
|
||||
})
|
||||
|
||||
if (user.meta.preferredLanguage) {
|
||||
// After user is authenticated, get his preferred language
|
||||
// and instruct i18next to change it
|
||||
this.$i18n.i18next.changeLanguage(user.meta.preferredLanguage)
|
||||
}
|
||||
|
||||
// switch the webapp theme based on user preference
|
||||
if (user.meta.theme) {
|
||||
document.getElementsByTagName('html')[0].setAttribute('data-color-mode', user.meta.theme)
|
||||
}
|
||||
|
||||
// Load effective permissions
|
||||
this.$store.dispatch('rbac/load')
|
||||
|
||||
// Initialize notifications
|
||||
this.$store.dispatch('notifications/fetchNotifications')
|
||||
|
||||
this.loaded = true
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err instanceof Error && err.message === 'Unauthenticated') {
|
||||
// user not logged-in,
|
||||
// start with authentication flow
|
||||
this.$auth.startAuthenticationFlow()
|
||||
return
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Registers event listener for websocket messages and
|
||||
* routes them depending on their type
|
||||
*/
|
||||
websocket () {
|
||||
// cross-link auth & websocket so that ws can use the right access token
|
||||
websocket.init(this)
|
||||
|
||||
// register event listener for messages
|
||||
this.$on('websocket-message', ({ data }) => {
|
||||
const msg = JSON.parse(data)
|
||||
switch (msg['@type']) {
|
||||
case 'notification':
|
||||
this.$store.dispatch('notifications/addNotification', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.read':
|
||||
this.$store.dispatch('notifications/updateReadNotification', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.unread':
|
||||
this.$store.dispatch('notifications/updateUnreadNotification', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.read.all':
|
||||
this.$store.dispatch('notifications/updateAllReadNotifications', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.unread.all':
|
||||
this.$store.dispatch('notifications/updateAllUnreadNotifications', msg['@value'])
|
||||
break
|
||||
|
||||
case 'notification.delete':
|
||||
this.$store.dispatch('notifications/removeNotification', msg['@value'])
|
||||
break
|
||||
|
||||
case 'error':
|
||||
this.toastDanger('Websocket message with error', msg['@value'])
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
router,
|
||||
store,
|
||||
i18n: i18n(Vue,
|
||||
{ app: 'corteza-webapp-privacy' },
|
||||
'dashboard',
|
||||
'data-overview',
|
||||
'general',
|
||||
'map',
|
||||
'module-records',
|
||||
'navigation',
|
||||
'notification',
|
||||
'notifications',
|
||||
'request',
|
||||
'sensitive-data',
|
||||
),
|
||||
// Any additional options we want to merge
|
||||
...options,
|
||||
}
|
||||
|
||||
const app = new Vue(options)
|
||||
|
||||
// Simple HMR acceptance
|
||||
if (module.hot) {
|
||||
module.hot.accept()
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
149
client/web/privacy/src/components/Common/EditorToolbar.vue
Normal file
149
client/web/privacy/src/components/Common/EditorToolbar.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<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"
|
||||
data-test-id="button-back"
|
||||
variant="link"
|
||||
size="lg"
|
||||
:to="backLink"
|
||||
class="d-flex align-items-center text-dark back text-decoration-none gap-1"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'chevron-left']"
|
||||
class="back-icon"
|
||||
/>
|
||||
{{ $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"
|
||||
:processing="processingDelete"
|
||||
:text="deleteLabel"
|
||||
variant="danger"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
class="ml-1"
|
||||
@confirmed="$emit('delete')"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
v-else
|
||||
:data-test-id="buttonLabelCypressId(deleteLabel)"
|
||||
:disabled="deleteDisabled || processing"
|
||||
variant="danger"
|
||||
size="lg"
|
||||
class="ml-1"
|
||||
@click="$emit('delete')"
|
||||
>
|
||||
{{ deleteLabel }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<c-button-submit
|
||||
v-if="submitShow"
|
||||
:data-test-id="buttonLabelCypressId(submitLabel)"
|
||||
:disabled="submitDisabled"
|
||||
:processing="processing"
|
||||
:text="submitLabel"
|
||||
size="lg"
|
||||
class="ml-1"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
processing: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
processingDelete: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
backLink: {
|
||||
type: Object,
|
||||
default: () => ({ name: 'root' }),
|
||||
},
|
||||
|
||||
deleteShow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
deleteDisabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
deleteConfirm: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
deleteLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
submitShow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
submitDisabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
submitLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
buttonLabelCypressId (label) {
|
||||
return `button-${label.toLowerCase().split(' ').join('-')}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.back {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
.back-icon {
|
||||
transition: transform 0.3s ease-out;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
client/web/privacy/src/components/Common/ModuleRecords.vue
Normal file
89
client/web/privacy/src/components/Common/ModuleRecords.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-auto"
|
||||
>
|
||||
<b-card
|
||||
v-for="m in modules"
|
||||
:key="m.moduleID"
|
||||
header-class="d-flex justify-content-between 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"
|
||||
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>
|
||||
99
client/web/privacy/src/components/ConnectionMap.vue
Normal file
99
client/web/privacy/src/components/ConnectionMap.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<c-map
|
||||
:map="{
|
||||
zoom,
|
||||
}"
|
||||
hide-geo-search
|
||||
hide-current-location-button
|
||||
:markers="validMarkerValues"
|
||||
style="min-height: 400px; height: 100% !important;"
|
||||
>
|
||||
<template #marker-tooltip="{ marker }">
|
||||
<h5
|
||||
class="text-primary"
|
||||
>
|
||||
{{ $t('server-details') }}
|
||||
</h5>
|
||||
<b-form-group
|
||||
:label="$t('name')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ marker.meta.name }}
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('location')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
{{ getLocationName(marker) }}
|
||||
</b-form-group>
|
||||
</template>
|
||||
</c-map>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CMap } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'map',
|
||||
},
|
||||
|
||||
components: {
|
||||
CMap,
|
||||
},
|
||||
|
||||
props: {
|
||||
connections: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
zoom: 2,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
validMarkerValues () {
|
||||
return this.connections
|
||||
.filter(({ meta = {} }) => {
|
||||
const { location = {} } = meta
|
||||
const { geometry = {} } = location
|
||||
const { coordinates = [] } = geometry
|
||||
|
||||
return coordinates && !!coordinates.length
|
||||
})
|
||||
.map((connection) => {
|
||||
return {
|
||||
id: connection.id,
|
||||
value: this.getLocationCoordinates(connection),
|
||||
...connection,
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLocationCoordinates ({ meta = {} }) {
|
||||
const { location = {} } = meta
|
||||
const { geometry = {} } = location
|
||||
return geometry.coordinates
|
||||
},
|
||||
|
||||
getLocationName (connection) {
|
||||
return connection.meta.location.properties.name || this.$t('unnamed-location')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vl-style-text {
|
||||
color: var(--white);
|
||||
}
|
||||
</style>
|
||||
182
client/web/privacy/src/components/Requests/Comments.vue
Normal file
182
client/web/privacy/src/components/Requests/Comments.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-class="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-button
|
||||
:disabled="sort.includes('DESC')"
|
||||
@click="$emit('sort', 'createdAt DESC')"
|
||||
>
|
||||
{{ $t('sort.first.newest') }}
|
||||
</b-dropdown-item-button>
|
||||
|
||||
<b-dropdown-item-button
|
||||
:disabled="!sort.includes('DESC')"
|
||||
@click="$emit('sort', 'createdAt')"
|
||||
>
|
||||
{{ $t('sort.first.oldest') }}
|
||||
</b-dropdown-item-button>
|
||||
</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"
|
||||
>
|
||||
<p
|
||||
class="mb-0 multiline"
|
||||
>
|
||||
{{ c.comment }}
|
||||
</p>
|
||||
</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, suspended: 1, deleted: 1 })
|
||||
.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>
|
||||
.multiline {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
210
client/web/privacy/src/components/Requests/Editor/Correct.vue
Normal file
210
client/web/privacy/src/components/Requests/Editor/Correct.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<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"
|
||||
>
|
||||
<c-input-select
|
||||
v-model="connection"
|
||||
:disabled="processing.connections"
|
||||
:options="connections"
|
||||
:clearable="false"
|
||||
:get-option-label="({ handle, meta }) => meta.name || handle"
|
||||
:get-option-key="getOptionKey"
|
||||
:placeholder="$t('connection.placeholder')"
|
||||
/>
|
||||
</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'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'edit.correct',
|
||||
},
|
||||
|
||||
components: {
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
getOptionKey ({ connectionID }) {
|
||||
return connectionID
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
216
client/web/privacy/src/components/Requests/Editor/Delete.vue
Normal file
216
client/web/privacy/src/components/Requests/Editor/Delete.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<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"
|
||||
>
|
||||
<c-input-select
|
||||
v-model="connection"
|
||||
:disabled="processing.connections"
|
||||
:options="connections"
|
||||
:clearable="false"
|
||||
:get-option-label="({ handle, meta }) => meta.name || handle"
|
||||
:get-option-key="getOptionKey"
|
||||
:placeholder="$t('connection.placeholder')"
|
||||
/>
|
||||
</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'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'request',
|
||||
keyPrefix: 'edit.delete',
|
||||
},
|
||||
|
||||
components: {
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
getOptionKey ({ connectionID }) {
|
||||
return connectionID
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
111
client/web/privacy/src/components/Requests/Editor/Export.vue
Normal file
111
client/web/privacy/src/components/Requests/Editor/Export.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<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"
|
||||
data-test-id="checkbox-profile-information"
|
||||
class="ml-2 mb-1"
|
||||
>
|
||||
{{ $t('data-type.profile-information') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox
|
||||
v-model="payload.application"
|
||||
data-test-id="checkbox-application-data"
|
||||
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"
|
||||
data-test-id="select-file-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>
|
||||
40
client/web/privacy/src/components/Requests/Editor/index.vue
Normal file
40
client/web/privacy/src/components/Requests/Editor/index.vue
Normal file
@@ -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>
|
||||
49
client/web/privacy/src/components/Requests/Viewer/Delete.vue
Normal file
49
client/web/privacy/src/components/Requests/Viewer/Delete.vue
Normal file
@@ -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>
|
||||
66
client/web/privacy/src/components/Requests/Viewer/Export.vue
Normal file
66
client/web/privacy/src/components/Requests/Viewer/Export.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div
|
||||
class="p-3"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('data-type.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<span
|
||||
data-test-id="span-data-type"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ dataType }}
|
||||
</span>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
:label="$t('date-range.label')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<span
|
||||
data-test-id="span-date-range"
|
||||
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
|
||||
data-test-id="span-file-format"
|
||||
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>
|
||||
17
client/web/privacy/src/components/Requests/Viewer/base.vue
Normal file
17
client/web/privacy/src/components/Requests/Viewer/base.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
request: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
payload () {
|
||||
const { payload = [] } = this.request || {}
|
||||
return payload[0]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
111
client/web/privacy/src/components/Requests/Viewer/index.vue
Normal file
111
client/web/privacy/src/components/Requests/Viewer/index.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<b-card
|
||||
v-if="!processing"
|
||||
header-class="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
|
||||
:data-test-id="`badge-${request.status}`"
|
||||
:variant="statusVariants[request.status]"
|
||||
pill
|
||||
class="px-2 py-1"
|
||||
>
|
||||
{{ $t(`request:status.${request.status}`) }}
|
||||
</b-badge>
|
||||
</h5>
|
||||
|
||||
<b-card-text
|
||||
data-test-id="request-author"
|
||||
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>
|
||||
59
client/web/privacy/src/components/faIcons.js
Normal file
59
client/web/privacy/src/components/faIcons.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGripHorizontal,
|
||||
faThumbtack,
|
||||
faHome,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faThLarge,
|
||||
faCheck,
|
||||
faCheckDouble,
|
||||
faTimes,
|
||||
faAngleUp,
|
||||
faAngleDown,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faAngleDoubleLeft,
|
||||
faAngleDoubleRight,
|
||||
faSearch,
|
||||
faBellSlash,
|
||||
faBell as faBellSolid,
|
||||
faEllipsisV,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
faUser,
|
||||
faQuestionCircle,
|
||||
faBell,
|
||||
faTrashAlt,
|
||||
faEnvelope,
|
||||
faEnvelopeOpen,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faHome,
|
||||
faThLarge,
|
||||
faUser,
|
||||
faThumbtack,
|
||||
faQuestionCircle,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faGripHorizontal,
|
||||
faCheck,
|
||||
faCheckDouble,
|
||||
faTimes,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faAngleDoubleLeft,
|
||||
faAngleDoubleRight,
|
||||
faSearch,
|
||||
faAngleUp,
|
||||
faAngleDown,
|
||||
faBell,
|
||||
faBellSolid,
|
||||
faBellSlash,
|
||||
faEllipsisV,
|
||||
faTrashAlt,
|
||||
faEnvelope,
|
||||
faEnvelopeOpen,
|
||||
)
|
||||
13
client/web/privacy/src/components/index.js
Normal file
13
client/web/privacy/src/components/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import PortalVue from 'portal-vue'
|
||||
import './faIcons'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
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)
|
||||
Vue.component('c-button-submit', components.CButtonSubmit)
|
||||
Vue.component('c-input-select', components.CInputSelect)
|
||||
8
client/web/privacy/src/config-check.js
Normal file
8
client/web/privacy/src/config-check.js
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
'CortezaAPI',
|
||||
].forEach((cfg) => {
|
||||
if (window[cfg] === undefined) {
|
||||
throw new Error(`Missing or invalid configuration.
|
||||
Make sure there is a public/config.js configuration file with window.${cfg} entry.`)
|
||||
}
|
||||
})
|
||||
6
client/web/privacy/src/console-splash.js
Normal file
6
client/web/privacy/src/console-splash.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable no-undef */
|
||||
const text = `%c${WEBAPP || 'Corteza Webapp'}, version: ${VERSION}, build time: ${BUILD_TIME}`
|
||||
const style = 'background-color: #1397CB; color: white; padding: 3px 10px; border: 1px solid black; font: Courier'
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log(text, style)
|
||||
33
client/web/privacy/src/dev-env.js
Normal file
33
client/web/privacy/src/dev-env.js
Normal file
@@ -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' },
|
||||
),
|
||||
})
|
||||
6
client/web/privacy/src/filters/index.js
Normal file
6
client/web/privacy/src/filters/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import { filters } from '@cortezaproject/corteza-vue'
|
||||
|
||||
for (const n in filters) {
|
||||
Vue.filter(n, filters[n])
|
||||
}
|
||||
4
client/web/privacy/src/main.js
Normal file
4
client/web/privacy/src/main.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import app from './app'
|
||||
import './themes'
|
||||
|
||||
app()
|
||||
5
client/web/privacy/src/mixins/index.js
Normal file
5
client/web/privacy/src/mixins/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import { mixins } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.mixin(mixins.toast)
|
||||
223
client/web/privacy/src/mixins/listHelpers.js
Normal file
223
client/web/privacy/src/mixins/listHelpers.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import axios from 'axios'
|
||||
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: {},
|
||||
|
||||
abortableRequests: [],
|
||||
cancelled: false,
|
||||
}
|
||||
},
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.abortRequests()
|
||||
},
|
||||
|
||||
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 () {
|
||||
// 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.abortRequests()
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
},
|
||||
|
||||
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) {
|
||||
this.abortRequests()
|
||||
|
||||
const { response, cancel } = p
|
||||
this.abortableRequests.push(cancel)
|
||||
|
||||
// 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 Promise.all([response(), new Promise(resolve => setTimeout(resolve, 300))])
|
||||
.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(error => {
|
||||
if (!axios.isCancel(error)) {
|
||||
this.toastErrorHandler(this.$t('notification:list.load.error'))(error)
|
||||
} else {
|
||||
this.cancelled = true
|
||||
}
|
||||
}).finally(() => {
|
||||
this.cancelled = false
|
||||
})
|
||||
},
|
||||
|
||||
genericRowClass (item) {
|
||||
return { 'text-secondary': item && !!item.deletedAt }
|
||||
},
|
||||
|
||||
abortRequests () {
|
||||
this.abortableRequests.forEach((cancel) => {
|
||||
cancel()
|
||||
})
|
||||
this.abortableRequests = []
|
||||
},
|
||||
},
|
||||
}
|
||||
28
client/web/privacy/src/plugins/index.js
Normal file
28
client/web/privacy/src/plugins/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
|
||||
import VueNativeSock from 'vue-native-websocket'
|
||||
|
||||
import { plugins, websocket } 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',
|
||||
},
|
||||
BModal: {
|
||||
noEnforceFocus: true,
|
||||
},
|
||||
})
|
||||
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 })
|
||||
Vue.use(VueNativeSock, websocket.endpoint(), websocket.config)
|
||||
7
client/web/privacy/src/router.js
Normal file
7
client/web/privacy/src/router.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Router from 'vue-router'
|
||||
import routes from './views/routes.js'
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
routes,
|
||||
})
|
||||
23
client/web/privacy/src/store/index.js
Normal file
23
client/web/privacy/src/store/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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),
|
||||
},
|
||||
notifications: {
|
||||
namespaced: true,
|
||||
...cvStore.notifications({
|
||||
api: Vue.prototype.$SystemAPI,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default store
|
||||
8
client/web/privacy/src/themes/corteza-base/custom.scss
Normal file
8
client/web/privacy/src/themes/corteza-base/custom.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
1
client/web/privacy/src/themes/corteza-base/index.js
Normal file
1
client/web/privacy/src/themes/corteza-base/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import './index.scss'
|
||||
2
client/web/privacy/src/themes/corteza-base/index.scss
Normal file
2
client/web/privacy/src/themes/corteza-base/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "./custom";
|
||||
@import "./poppins.scss";
|
||||
146
client/web/privacy/src/themes/corteza-base/poppins.scss
Normal file
146
client/web/privacy/src/themes/corteza-base/poppins.scss
Normal file
@@ -0,0 +1,146 @@
|
||||
// Fonts Path
|
||||
$fonts_dir : './fonts/' !default;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Bold';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Bold.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-BlackItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-BlackItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Italic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Italic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-BoldItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-BoldItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ExtraBoldItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ExtraBoldItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ExtraLightItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ExtraLightItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Black';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Black.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ExtraBold';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ExtraBold.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ExtraLight';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ExtraLight.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-MediumItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-MediumItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-SemiBoldItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-SemiBoldItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-SemiBold';
|
||||
src: url($fonts_dir + 'poppins/Poppins-SemiBold.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-LightItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-LightItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Regular';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Regular.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Medium';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Medium.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Light';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Light.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-ThinItalic';
|
||||
src: url($fonts_dir + 'poppins/Poppins-ThinItalic.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins-Thin';
|
||||
src: url($fonts_dir + 'poppins/Poppins-Thin.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
2
client/web/privacy/src/themes/index.js
Normal file
2
client/web/privacy/src/themes/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
import(/* webpackChunkName: 'corteza-base' */ './corteza-base')
|
||||
142
client/web/privacy/src/views/Layout.vue
Normal file
142
client/web/privacy/src/views/Layout.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column w-100 h-viewport">
|
||||
<header>
|
||||
<c-topbar
|
||||
:expanded="expanded"
|
||||
:settings="$Settings.get('ui.topbar', {})"
|
||||
:labels="{
|
||||
appMenu: $t('navigation:appMenu'),
|
||||
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'),
|
||||
userSettingsTheme: $t('navigation:userSettings.theme'),
|
||||
lightTheme: $t('general:themes.labels.light'),
|
||||
darkTheme: $t('general:themes.labels.dark'),
|
||||
}"
|
||||
>
|
||||
<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"
|
||||
: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="sidebar-spacer d-print-none"
|
||||
:class="{
|
||||
'expanded': expanded,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="d-flex flex-column w-100"
|
||||
>
|
||||
<router-view
|
||||
class="flex-grow-1 overflow-auto"
|
||||
/>
|
||||
|
||||
<portal-target
|
||||
name="editor-toolbar"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<c-extend-session
|
||||
v-if="isAutoLogoutEnabled"
|
||||
:timeout="$Settings.get('auth.autoLogout.timeout')"
|
||||
:labels="{
|
||||
extend: $t('general:extendSession.labels.extend'),
|
||||
warning: (countdownTime) => $t('general:extendSession.labels.warning', { countdownTime }),
|
||||
}"
|
||||
/>
|
||||
<c-notification-sidebar v-if="!$Settings.get('ui.topbar', {}).hideNotifications" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
const { CTopbar, CSidebar, CExtendSession, CNotificationSidebar } = components
|
||||
|
||||
export default {
|
||||
name: 'Layout',
|
||||
|
||||
components: {
|
||||
CTopbar,
|
||||
CSidebar,
|
||||
CExtendSession,
|
||||
CNotificationSidebar,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
expanded: false,
|
||||
}
|
||||
},
|
||||
|
||||
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')
|
||||
},
|
||||
|
||||
isAutoLogoutEnabled () {
|
||||
return this.$Settings.get('auth.autoLogout.enabled')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.h-viewport {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
}
|
||||
</style>
|
||||
162
client/web/privacy/src/views/Privacy/Dashboard.vue
Normal file
162
client/web/privacy/src/views/Privacy/Dashboard.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<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"
|
||||
style="min-height: 400px;"
|
||||
class="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,178 @@
|
||||
<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"
|
||||
>
|
||||
<c-input-select
|
||||
v-model="connectionID"
|
||||
:disabled="processing.connections"
|
||||
:options="connections"
|
||||
:clearable="false"
|
||||
:reduce="o => o.connectionID"
|
||||
:placeholder="$t('connection.placeholder')"
|
||||
:get-option-label="({ handle, meta }) => meta.name || handle"
|
||||
:get-option-key="getOptionKey"
|
||||
/>
|
||||
</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="!(connectionID && modules[connectionID])"
|
||||
class="text-center mt-5"
|
||||
>
|
||||
{{ $t('no-data-available') }}
|
||||
</h5>
|
||||
|
||||
<module-records
|
||||
v-else
|
||||
v-slot="{ value }"
|
||||
:modules="modules[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
|
||||
data-test-id="button-request-deletion"
|
||||
: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
|
||||
data-test-id="button-request-correction"
|
||||
: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'
|
||||
|
||||
export default {
|
||||
name: 'ApplicationDataOverview',
|
||||
|
||||
i18nOptions: {
|
||||
namespaces: 'data-overview',
|
||||
keyPrefix: 'application',
|
||||
},
|
||||
|
||||
components: {
|
||||
EditorToolbar,
|
||||
ModuleRecords,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
processing: {
|
||||
connections: true,
|
||||
sensitiveData: true,
|
||||
},
|
||||
|
||||
connectionID: undefined,
|
||||
|
||||
connections: [],
|
||||
|
||||
modules: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
connection () {
|
||||
return this.connections.find(({ connectionID }) => connectionID === this.connectionID) || {}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
connectionID: {
|
||||
handler (connectionID = '') {
|
||||
this.fetchSensitiveData(connectionID)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchConnections()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchConnections () {
|
||||
this.processing.connections = true
|
||||
|
||||
this.$SystemAPI.dataPrivacyConnectionList()
|
||||
.then(({ set = [] }) => {
|
||||
this.connections = set
|
||||
const { connectionID } = set[0] || {}
|
||||
this.connectionID = connectionID
|
||||
})
|
||||
.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
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
getOptionKey ({ connectionID }) {
|
||||
return connectionID
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
106
client/web/privacy/src/views/Privacy/DataOverview/index.vue
Normal file
106
client/web/privacy/src/views/Privacy/DataOverview/index.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
58
client/web/privacy/src/views/Privacy/Request/Create.vue
Normal file
58
client/web/privacy/src/views/Privacy/Request/Create.vue
Normal file
@@ -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>
|
||||
281
client/web/privacy/src/views/Privacy/Request/List.vue
Normal file
281
client/web/privacy/src/views/Privacy/Request/List.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<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'),
|
||||
resourceSingle: $t('general:label.privacy_request.single'),
|
||||
resourcePlural: $t('general:label.privacy_request.plural'),
|
||||
}"
|
||||
: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"
|
||||
:processing="processingApprove"
|
||||
:text="$t('request.approve')"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
@confirmed="handleSelectedRequests(selected, 'approved')"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
v-if="isDC"
|
||||
:disabled="processing || !selected.length"
|
||||
:processing="processingReject"
|
||||
:text="$t('request.reject')"
|
||||
variant="danger"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
@confirmed="handleSelectedRequests(selected, 'rejected')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<c-input-confirm
|
||||
:disabled="processing || !selected.length"
|
||||
:processing="processingCancel"
|
||||
:text="$t('request.cancel')"
|
||||
variant="light"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
@confirmed="handleSelectedRequests(selected, 'canceled')"
|
||||
/>
|
||||
|
||||
<!-- <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 {
|
||||
processing: false,
|
||||
processingApprove: false,
|
||||
processingReject: false,
|
||||
processingCancel: false,
|
||||
|
||||
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.dataPrivacyRequestListCancellable(this.encodeListParams()))
|
||||
.then(async (set) => {
|
||||
if (this.isDC && set && set.length) {
|
||||
await this.fetchUsers(set.map(({ requestedBy }) => requestedBy))
|
||||
}
|
||||
|
||||
return set
|
||||
})
|
||||
},
|
||||
|
||||
handleSelectedRequests (selected, status) {
|
||||
this.processing = true
|
||||
|
||||
if (status === 'approved') {
|
||||
this.processingApprove = true
|
||||
} else if (status === 'rejected') {
|
||||
this.processingReject = true
|
||||
} else {
|
||||
this.processingCancel = true
|
||||
}
|
||||
|
||||
Promise.all(selected.map(requestID => {
|
||||
return this.$SystemAPI.dataPrivacyRequestUpdateStatus({ requestID, status })
|
||||
}))
|
||||
.then(() => {
|
||||
this.$root.$emit('bv::refresh::table', 'resource-list')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
if (status === 'approved') {
|
||||
this.processingApprove = false
|
||||
} else if (status === 'rejected') {
|
||||
this.processingReject = false
|
||||
} else {
|
||||
this.processingCancel = false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
fetchUsers (userID = []) {
|
||||
userID = [...new Set(userID)]
|
||||
return this.$SystemAPI.userList({ userID, suspended: 1, deleted: 1 })
|
||||
.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>
|
||||
218
client/web/privacy/src/views/Privacy/Request/View.vue
Normal file
218
client/web/privacy/src/views/Privacy/Request/View.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<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"
|
||||
:processing-confirm="processingReject"
|
||||
: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"
|
||||
:disabled="!request || !isPending"
|
||||
:processing="processingCancel"
|
||||
:text="$t('cancel')"
|
||||
variant="light"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
@confirmed="handleRequest('canceled')"
|
||||
/>
|
||||
|
||||
<c-input-confirm
|
||||
v-else
|
||||
:disabled="!request || !isPending"
|
||||
:processing="processingApprove"
|
||||
:text="$t('approve')"
|
||||
variant="primary"
|
||||
variant-ok="primary"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
class="ml-2"
|
||||
@confirmed="handleRequest('approved')"
|
||||
/>
|
||||
</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,
|
||||
},
|
||||
|
||||
processingApprove: false,
|
||||
processingReject: false,
|
||||
processingCancel: 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
|
||||
|
||||
if (status === 'approved') {
|
||||
this.processingApprove = true
|
||||
} else if (status === 'rejected') {
|
||||
this.processingReject = true
|
||||
} else {
|
||||
this.processingCancel = true
|
||||
}
|
||||
|
||||
this.$SystemAPI.dataPrivacyRequestUpdateStatus({ requestID: this.requestID, status })
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'request.list' })
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing.toolbar = false
|
||||
|
||||
if (status === 'approved') {
|
||||
this.processingApprove = false
|
||||
} else if (status === 'rejected') {
|
||||
this.processingReject = false
|
||||
} else {
|
||||
this.processingCancel = false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
submitComment (comment) {
|
||||
this.processing.comments = true
|
||||
|
||||
this.$SystemAPI.dataPrivacyRequestCommentCreate({ requestID: this.requestID, comment })
|
||||
.then(() => {
|
||||
return this.fetchComments()
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing.comments = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
116
client/web/privacy/src/views/Privacy/SensitiveData.vue
Normal file
116
client/web/privacy/src/views/Privacy/SensitiveData.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<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'),
|
||||
resourceSingle: $t('general:label.sensitive_module.single'),
|
||||
resourcePlural: $t('general:label.sensitive_module.plural'),
|
||||
}"
|
||||
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.dataPrivacyModuleListCancellable(this.encodeListParams()))
|
||||
},
|
||||
|
||||
rowClicked ({ namespace, module }) {
|
||||
const { namespaceID, slug } = namespace
|
||||
window.open(`/compose/ns/${slug || namespaceID}/admin/modules/${module.moduleID}/edit`, '_blank')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
19
client/web/privacy/src/views/routes.js
Normal file
19
client/web/privacy/src/views/routes.js
Normal file
@@ -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' } },
|
||||
]
|
||||
Reference in New Issue
Block a user