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

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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.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>

View 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>

View 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>

View File

@@ -0,0 +1,17 @@
<script>
export default {
props: {
request: {
type: Object,
required: true,
},
},
computed: {
payload () {
const { payload = [] } = this.request || {}
return payload[0]
},
},
}
</script>

View 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>

View 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,
)

View 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)