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:
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)
|
||||
Reference in New Issue
Block a user