3
0

Extend filter fields functionality with c-ace editor and autocomplete

This commit is contained in:
Kelani Tolulope
2024-06-10 12:18:20 +01:00
committed by Jože Fortun
parent af8216bdc3
commit 641ba76dd6
25 changed files with 976 additions and 107 deletions

View File

@@ -95,9 +95,11 @@
:label="$t('calendar.recordFeed.prefilterLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model="feed.options.prefilter"
:value="true"
height="3.688rem"
lang="javascript"
:suggestion-params="recordAutoCompleteParams"
:placeholder="$t('calendar.recordFeed.prefilterPlaceholder')"
/>
@@ -142,7 +144,10 @@
<script>
import base from './base'
import { components } from '@cortezaproject/corteza-vue'
const { CInputColorPicker } = components
import { compose, NoID } from '@cortezaproject/corteza-js'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
const { CInputColorPicker, CInputExpression } = components
export default {
i18nOptions: {
@@ -151,10 +156,26 @@ export default {
components: {
CInputColorPicker,
CInputExpression,
},
extends: base,
mixins: [autocomplete],
props: {
record: {
type: compose.Record,
required: false,
default: undefined,
},
page: {
type: compose.Page,
required: true,
},
},
computed: {
/**
* Finds the module, this feed configurator should use
@@ -213,6 +234,14 @@ export default {
themeSettings () {
return this.$Settings.get('ui.studio.themes', [])
},
isRecordPage () {
return this.page && this.page.moduleID !== NoID
},
recordAutoCompleteParams () {
return this.processRecordAutoCompleteParams({ operators: true })
},
},
methods: {

View File

@@ -39,6 +39,9 @@
v-if="feed.resource && configurator(feed)"
:feed="feed"
:modules="modules"
:page="page"
:record="record"
:module="module"
/>
</b-card>
</b-col>
@@ -65,11 +68,15 @@ export default {
namespaces: 'block',
},
components: {
},
extends: base,
props: {
page: {
type: compose.Page,
required: true,
},
},
computed: {
...mapGetters({
modules: 'module/set',

View File

@@ -44,9 +44,11 @@
:label="$t('recordList.record.prefilterLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model.trim="options.filter"
:value="true"
height="3.688rem"
lang="javascript"
:suggestion-params="recordAutoCompleteParams"
:placeholder="$t('recordList.record.prefilterPlaceholder')"
/>
@@ -152,8 +154,12 @@
</template>
<script>
import { mapGetters } from 'vuex'
import { components } from '@cortezaproject/corteza-vue'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
import base from './base'
const { CInputExpression } = components
export default {
i18nOptions: {
namespaces: 'block',
@@ -161,7 +167,14 @@ export default {
name: 'CommentConfigurator',
components: {
CInputExpression,
},
extends: base,
mixins: [autocomplete],
data () {
return {
referenceList: [
@@ -220,6 +233,10 @@ export default {
}
return []
},
recordAutoCompleteParams () {
return this.processRecordAutoCompleteParams({ module: this.selectedModule, operators: true })
},
},
watch: {

View File

@@ -17,10 +17,15 @@
label-class="text-primary"
>
<b-input-group>
<b-form-input
<c-input-expression
id="title"
v-model="block.title"
auto-complete
lang="javascript"
:placeholder="$t('general.titlePlaceholder')"
:suggestion-params="recordAutoCompleteParams"
height="2.375rem"
class="flex-grow-1"
/>
<b-input-group-append>
@@ -53,10 +58,15 @@
label-class="text-primary"
>
<b-input-group>
<b-form-textarea
<c-input-expression
id="description"
v-model="block.description"
auto-complete
lang="javascript"
:placeholder="$t('general.descriptionPlaceholder')"
:suggestion-params="recordAutoCompleteParams"
height="3.448rem"
class="flex-grow-1"
/>
<b-input-group-append>
<page-translator
@@ -257,9 +267,15 @@
ƒ
</b-button>
</b-input-group-prepend>
<b-form-input
<c-input-expression
id="visibility-fields"
v-model="block.meta.visibility.expression"
auto-complete
lang="javascript"
:placeholder="$t('general.visibility.condition.placeholder')"
:suggestion-params="visibilityAutoCompleteParams"
height="2.375rem"
class="flex-grow-1"
/>
<b-input-group-append>
<b-button
@@ -342,12 +358,16 @@
</template>
</b-tabs>
</template>
<script>
import { compose, NoID } from '@cortezaproject/corteza-js'
import { handle } from '@cortezaproject/corteza-vue'
import { handle, components } from '@cortezaproject/corteza-vue'
import PageTranslator from 'corteza-webapp-compose/src/components/Admin/Page/PageTranslator'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
import PageBlock from './index'
const { CInputExpression } = components
export default {
i18nOptions: {
namespaces: 'block',
@@ -356,18 +376,38 @@ export default {
components: {
PageBlock,
PageTranslator,
CInputExpression,
},
mixins: [autocomplete],
props: {
block: {
type: compose.PageBlock,
required: true,
},
module: {
type: compose.Module,
required: false,
default: undefined,
},
page: {
type: compose.Page,
required: true,
},
record: {
type: [Object, null],
required: false,
default: null,
},
namespace: {
type: compose.Namespace,
required: true,
},
},
data () {
@@ -494,6 +534,7 @@ export default {
},
}
</script>
<style scoped>
.mh-tab {
max-height: calc(100vh - 16rem);

View File

@@ -66,9 +66,11 @@
:label="$t('calendar.recordFeed.prefilterLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model="feed.options.prefilter"
:value="true"
height="3.688rem"
lang="javascript"
:suggestion-params="recordAutoCompleteParams"
:placeholder="$t('calendar.recordFeed.prefilterPlaceholder')"
/>
@@ -146,7 +148,9 @@
<script>
import base from './base'
import { components } from '@cortezaproject/corteza-vue'
const { CInputColorPicker } = components
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
const { CInputColorPicker, CInputExpression } = components
export default {
i18nOptions: {
@@ -155,10 +159,13 @@ export default {
components: {
CInputColorPicker,
CInputExpression,
},
extends: base,
mixins: [autocomplete],
data () {
return {
checkboxLabel: {

View File

@@ -25,6 +25,8 @@
v-if="feed.resource && configurator(feed)"
:feed="feed"
:modules="modules"
:page="page"
:record="record"
/>
</b-card>
</b-col>

View File

@@ -18,8 +18,12 @@
:description="enableFromRecordURL ? $t('iframe.srcDesc') : ''"
label-class="text-primary"
>
<b-form-input
<c-input-expression
v-model="options.src"
auto-complete
lang="text"
:suggestion-params="recordAutoCompleteParams"
height="2.375rem"
type="url"
/>
<i18next
@@ -37,6 +41,10 @@
</template>
<script>
import base from './base'
import { components } from '@cortezaproject/corteza-vue'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
const { CInputExpression } = components
export default {
i18nOptions: {
@@ -45,8 +53,12 @@ export default {
name: 'IFrame',
components: { CInputExpression },
extends: base,
mixins: [autocomplete],
computed: {
fields () {
if (!this.module) {

View File

@@ -147,10 +147,14 @@
:label="$t('metric.edit.filterLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model="edit.filter"
auto-complete
lang="javascript"
placeholder="(A > B) OR (A < C)"
class="mb-1"
height="3.448rem"
:suggestion-params="recordAutoCompleteParams"
/>
<i18next
@@ -204,10 +208,14 @@
:label="$t('metric.edit.transformFunctionLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model="edit.transformFx"
auto-complete
lang="javascript"
placeholder="v"
class="mb-1"
height="3.448rem"
:suggestion-params="recordAutoCompleteParams"
/>
<small>{{ $t('metric.edit.transformFunctionDescription') }}</small>
@@ -350,7 +358,11 @@ import MStyle from './MStyle'
import { mapGetters } from 'vuex'
import MetricBase from '../MetricBase'
import ColumnPicker from 'corteza-webapp-compose/src/components/Admin/Module/Records/ColumnPicker'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
import { compose, NoID } from '@cortezaproject/corteza-js'
import { components } from '@cortezaproject/corteza-vue'
const { CInputExpression } = components
export default {
i18nOptions: {
@@ -362,9 +374,12 @@ export default {
MStyle,
MetricBase,
ColumnPicker,
CInputExpression,
},
extends: base,
mixins: [autocomplete],
data () {
return {
edit: undefined,
@@ -436,6 +451,10 @@ export default {
return this.getModuleByID(this.edit.moduleID)
},
recordAutoCompleteParams () {
return this.processRecordAutoCompleteParams({ module: this.selectedMetricModule })
},
},
watch: {

View File

@@ -50,10 +50,13 @@
:label="$t('metric.edit.filterLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model="options.value.filter"
height="3.688rem"
lang="javascript"
placeholder="(A > B) OR (A < C)"
class="mb-1"
:suggestion-params="recordAutoCompleteParams.value"
/>
<i18next
path="metric.edit.filterFootnote"
@@ -160,10 +163,13 @@
:label="$t('metric.edit.filterLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model="options.minValue.filter"
height="3.688rem"
lang="javascript"
placeholder="(A > B) OR (A < C)"
class="mb-1"
:suggestion-params="recordAutoCompleteParams.min"
/>
<i18next
path="metric.edit.filterFootnote"
@@ -270,10 +276,13 @@
:label="$t('metric.edit.filterLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model="options.maxValue.filter"
height="3.688rem"
lang="javascript"
placeholder="(A > B) OR (A < C)"
class="mb-1"
:suggestion-params="recordAutoCompleteParams.max"
/>
<i18next
path="metric.edit.filterFootnote"
@@ -532,9 +541,13 @@
<script>
import base from './base'
import { mapGetters } from 'vuex'
import { components } from '@cortezaproject/corteza-vue'
import { compose, validator } from '@cortezaproject/corteza-js'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
import FieldViewer from '../ModuleFields/Viewer'
const { CInputExpression } = components
export default {
i18nOptions: {
namespaces: 'block',
@@ -544,10 +557,13 @@ export default {
components: {
FieldViewer,
CInputExpression,
},
extends: base,
mixins: [autocomplete],
data () {
return {
aggregationOperations: [
@@ -606,13 +622,33 @@ export default {
return this.returnValueModuleFields(this.options.value.moduleID)
},
valueModule () {
return this.moduleByID(this.options.value.moduleID)
},
minValueModuleFields () {
return this.returnValueModuleFields(this.options.minValue.moduleID)
},
minValueModule () {
return this.moduleByID(this.options.minValue.moduleID)
},
maxValueModuleFields () {
return this.returnValueModuleFields(this.options.maxValue.moduleID)
},
maxValueModule () {
return this.moduleByID(this.options.maxValue.moduleID)
},
recordAutoCompleteParams () {
return {
value: this.processRecordAutoCompleteParams({ module: this.valueModule, operators: true }),
min: this.processRecordAutoCompleteParams({ module: this.minValueModule, operators: true }),
max: this.processRecordAutoCompleteParams({ module: this.maxValueModule, operators: true }),
}
},
},
watch: {

View File

@@ -0,0 +1,213 @@
<template>
<c-form-table-wrapper hide-add-button>
<b-form-group
:label="$t('recordList.record.prefilterCommand')"
label-class="text-primary"
class="m-0"
>
<b-row v-if="textInput">
<b-col>
<b-form-group label-class="text-primary">
<c-input-expression
v-model="options.prefilter"
height="3.688rem"
lang="javascript"
:suggestion-params="recordAutoCompleteParams"
/>
<i18next
path="recordList.record.prefilterFootnote"
tag="small"
class="text-muted"
>
<code>${record.values.fieldName}</code>
<code>${recordID}</code>
<code>${ownerID}</code>
<span><code>${userID}</code>, <code>${user.name}</code></span>
</i18next>
</b-form-group>
</b-col>
</b-row>
<c-form-table-wrapper
v-else
hide-add-button
>
<filter-toolbox
v-model="filterGroup"
:module="module"
:namespace="namespace"
:mock.sync="mock"
reset-filter-on-created
/>
</c-form-table-wrapper>
<div class="mt-1 d-flex align-items-center">
<b-button
variant="link"
size="sm"
class="ml-auto text-decoration-none"
@click="toggleFilterView"
>
{{ $t('recordList.prefilter.toggleInputType') }}
</b-button>
</div>
</b-form-group>
</c-form-table-wrapper>
</template>
<script>
import { components } from '@cortezaproject/corteza-vue'
import { compose, validator } from '@cortezaproject/corteza-js'
import {
getRecordListFilterSql,
trimChar,
} from 'corteza-webapp-compose/src/lib/record-filter.js'
import FilterToolbox from 'corteza-webapp-compose/src/components/Common/FilterToolbox.vue'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
const { CInputExpression } = components
export default {
i18nOptions: {
namespaces: 'block',
},
name: 'RecordListConfiguratorPrefilter',
components: {
FilterToolbox,
CInputExpression,
},
mixins: [autocomplete],
props: {
options: {
type: Object,
required: true,
},
namespace: {
type: compose.Namespace,
required: true,
},
module: {
type: compose.Module,
required: true,
},
record: {
type: [Object, null],
required: false,
default: null,
},
},
data () {
return {
textInput: true,
filterGroup: [],
}
},
computed: {
recordAutoCompleteParams () {
return this.processRecordAutoCompleteParams({ operators: true })
},
},
created () {
// Change all module fields to single value to keep multi value fields and single value
const module = JSON.parse(JSON.stringify(this.module || {}))
module.fields = [
...[...module.fields].map((f) => {
f.multi = f.isMulti
f.isMulti = false
// Disable edge case options
if (f.kind === 'DateTime') {
f.options.onlyFutureValues = false
f.options.onlyPastValues = false
}
return f
}),
...this.module.systemFields().map((sf) => {
return { ...sf, label: this.$t(`field:system.${sf.name}`) }
}),
]
this.mock = {
namespace: this.namespace,
module,
errors: new validator.Validated(),
}
},
methods: {
toggleFilterView () {
if (!this.textInput) {
this.options.prefilter = this.parseFilter()
}
this.textInput = !this.textInput
},
getOptionKey ({ name }) {
return name
},
processFilter (filterGroup = this.value) {
return filterGroup.map(({ groupCondition, filter = [], name }) => {
filter = filter.map(({ record, ...f }) => {
if (record) {
f.value = record[f.name] || record.values[f.name]
}
if (this.isBetweenOperator(f.operator)) {
f.value = {
start: this.getField(f.name).isSystem
? record[`${f.name}-start`]
: record.values[`${f.name}-start`],
end: this.getField(f.name).isSystem
? record[`${f.name}-end`]
: record.values[`${f.name}-end`],
}
}
return f
})
return { groupCondition, filter, name }
})
},
isBetweenOperator (op) {
return ['BETWEEN', 'NOT BETWEEN'].includes(op)
},
parseFilter (filterGroup = this.filterGroup) {
const filter = this.processFilter(filterGroup)
const filterSqlArray = filter
.map(({ groupCondition, filter = [] }) => {
groupCondition = groupCondition ? ` ${groupCondition} ` : ''
filter = getRecordListFilterSql(filter)
return filter ? `${filter}${groupCondition}` : ''
})
.filter((filter) => filter)
const filterSql = trimChar(
trimChar(filterSqlArray.join(''), ' AND '),
' OR '
)
return filterSql
},
},
}
</script>

View File

@@ -264,30 +264,13 @@
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-group
:label="$t('recordList.record.prefilterCommand')"
label-class="text-primary"
>
<b-form-textarea
v-model="options.prefilter"
:placeholder="$t('recordList.record.prefilterPlaceholder')"
/>
<i18next
path="recordList.record.prefilterFootnote"
tag="small"
class="text-muted"
>
<code>${record.values.fieldName}</code>
<code>${recordID}</code>
<code>${ownerID}</code>
<span><code>${userID}</code>, <code>${user.name}</code></span>
</i18next>
</b-form-group>
</b-col>
</b-row>
<prefilter
:record="record"
:module="recordListModule"
:namespace="namespace"
:options="options"
:page="page"
/>
<hr>

View File

@@ -45,9 +45,11 @@
:label="$t('recordList.record.prefilterLabel')"
label-class="text-primary"
>
<b-form-textarea
<c-input-expression
v-model.trim="options.filter"
:value="true"
height="3.688rem"
lang="javascript"
:suggestion-params="recordAutoCompleteParams"
:placeholder="$t('recordList.record.prefilterPlaceholder')"
/>
<i18next
@@ -196,8 +198,13 @@
import FieldEditor from '../ModuleFields/Editor'
import { mapGetters } from 'vuex'
import { compose, validator } from '@cortezaproject/corteza-js'
import { components } from '@cortezaproject/corteza-vue'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
import base from './base'
const { CInputExpression } = components
export default {
i18nOptions: {
namespaces: 'block',
@@ -207,10 +214,13 @@ export default {
components: {
FieldEditor,
CInputExpression,
},
extends: base,
mixins: [autocomplete],
data () {
return {
/*
@@ -275,6 +285,10 @@ export default {
{ value: 'modal', text: this.$t('recordOrganizer.openInModal') },
]
},
recordAutoCompleteParams () {
return this.processRecordAutoCompleteParams({ module: this.selectedModule })
},
},
watch: {

View File

@@ -59,6 +59,8 @@
:button="currentButton"
:script="currentScript"
:trigger="currentTrigger"
:record="record"
:module="module"
@delete="deleteButton(currentButton)"
/>
</b-col>

View File

@@ -8,8 +8,13 @@
:label="$t('buttonLabel')"
label-class="text-primary"
>
<b-form-input
<c-input-expression
v-model="button.label"
auto-complete
lang="javascript"
:suggestion-params="recordAutoCompleteParams"
:page="page"
height="2.375rem"
/>
<i18next
path="block:interpolationFootnote"
@@ -94,6 +99,10 @@
</template>
<script>
import { compose, NoID } from '@cortezaproject/corteza-js'
import { components } from '@cortezaproject/corteza-vue'
import autocomplete from 'corteza-webapp-compose/src/mixins/autocomplete.js'
const { CInputExpression } = components
export default {
i18nOptions: {
@@ -101,6 +110,12 @@ export default {
keyPrefix: 'automation',
},
components: {
CInputExpression,
},
extends: autocomplete,
props: {
button: {
type: Object,
@@ -128,6 +143,12 @@ export default {
type: compose.PageBlock,
required: true,
},
module: {
type: compose.Module,
required: false,
default: undefined,
},
},
computed: {

View File

@@ -1,5 +1,5 @@
<script>
import { compose } from '@cortezaproject/corteza-js'
import { compose, NoID } from '@cortezaproject/corteza-js'
import Wrap from './Wrap'
export default {
@@ -106,6 +106,35 @@ export default {
return !!recordPageID || !!magnifiedBlockID
},
isRecordPage () {
return this.page && this.page.moduleID !== NoID
},
recordAutoCompleteParams () {
const { fields = [] } = this.module || {}
const moduleFields = fields.map(({ name }) => name)
const userProperties = this.$auth.user.properties() || []
const recordSuggestions = this.isRecordPage ? [
...(['ownerID', 'recordID'].map(value => ({ interpolate: true, value }))),
{
interpolate: true,
value: 'record',
properties: [
...(this.record.properties || []),
{ value: 'values', properties: Object.keys(this.record.values) || [] },
],
},
] : []
return [
...moduleFields,
...recordSuggestions,
{ interpolate: true, value: 'userID' },
{ interpolate: true, value: 'user', properties: userProperties },
]
},
},
beforeDestroy () {

View File

@@ -0,0 +1,81 @@
import { compose, NoID } from '@cortezaproject/corteza-js'
export default {
props: {
record: {
type: compose.Record,
required: false,
default: undefined,
},
page: {
type: compose.Page,
required: true,
},
},
computed: {
isRecordPage () {
return this.page && this.page.moduleID !== NoID
},
recordAutoCompleteParams () {
return this.processRecordAutoCompleteParams({})
},
visibilityAutoCompleteParams () {
return this.processVisibilityAutoCompleteParams({})
},
},
methods: {
processRecordAutoCompleteParams ({ module = this.module, operators = false }) {
const { fields = [] } = module || {}
const moduleFields = fields.map(({ name }) => name)
const userProperties = this.$auth.user.properties() || []
const recordSuggestions = this.isRecordPage ? [
...(['ownerID', 'recordID'].map(value => ({ interpolate: true, value }))),
{
interpolate: true,
value: 'record',
properties: [
...(this.record.properties || []),
{ value: 'values', properties: Object.keys(this.record.values) || [] },
],
},
] : []
return [
...moduleFields,
...recordSuggestions,
...(operators ? ['AND', 'OR'] : []),
{ interpolate: true, value: 'userID' },
{ interpolate: true, value: 'user', properties: userProperties },
]
},
processVisibilityAutoCompleteParams ({ module = this.module }) {
const { fields = [] } = module || {}
const moduleFields = fields.map(({ name }) => name)
const userProperties = this.$auth.user.properties() || []
const recordSuggestions = this.isRecordPage ? [
{
value: 'record',
properties: [
...(this.record.properties || []),
{ value: 'values', properties: Object.keys(this.record.values) || [] },
],
},
] : []
return [
...moduleFields,
...recordSuggestions,
{ value: 'user', properties: userProperties },
{ value: 'screen', properties: ['width', 'height', 'userAgent', 'breakpoint'] },
]
},
},
}

View File

@@ -8,6 +8,7 @@ module.exports = {
extends: [
'standard',
'plugin:@typescript-eslint/recommended',
'plugin:vue/recommended',
'@vue/standard'
],
rules: {

View File

@@ -447,4 +447,28 @@ export class Record {
get labels (): object {
return this.meta
}
get properties (): string[] {
return [
'recordID',
'moduleID',
'namespaceID',
'revision',
'meta',
'createdAt',
'updatedAt',
'deletedAt',
'ownedBy',
'createdBy',
'updatedBy',
'deletedBy',
'canUpdateRecord',
'canReadRecord',
'canDeleteRecord',
'canUndeleteRecord',
'canManageOwnerOnRecord',
'canSearchRevision',
'canGrant',
]
}
}

View File

@@ -112,4 +112,25 @@ export class User {
clone (): User {
return new User(JSON.parse(JSON.stringify(this)))
}
properties (): string[] {
return [
'userID',
'handle',
'username',
'email',
'name',
'emailConfirmed',
'labels',
'meta',
'canGrant',
'canUpdateUser',
'canDeleteUser',
'createdAt',
'updatedAt',
'deletedAt',
'suspendedAt',
'roles',
]
}
}

View File

@@ -93,7 +93,7 @@
"eslint-plugin-node": "11.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "6.1.2",
"eslint-plugin-vue": "^9.21.1",
"esm": "^3.2.25",
"i18next-pseudo": "^2.2.1",
"mocha": "7.0.0",

View File

@@ -19,6 +19,7 @@ export {
CInputCheckbox,
CInputColorPicker,
CAceEditor,
CInputExpression,
CButtonSubmit,
CInputSelect,
CInputRole,

View File

@@ -3,6 +3,7 @@
class="position-relative"
>
<ace-editor
ref="aceeditor"
v-model="editorValue"
:lang="lang"
:mode="lang"
@@ -30,7 +31,7 @@
<script>
import AceEditor from 'vue2-ace-editor'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faExpandAlt} from '@fortawesome/free-solid-svg-icons'
import { faExpandAlt } from '@fortawesome/free-solid-svg-icons'
library.add(faExpandAlt)
@@ -79,7 +80,7 @@ export default {
type: Boolean,
default: false,
},
autoComplete: {
type: Boolean,
default: false,
@@ -96,9 +97,24 @@ export default {
},
autoCompleteSuggestions: {
type: Array,
default: () => ([])
}
type: [Array, Object],
default: () => ([]),
},
initExpressions: {
type: Boolean,
required: false,
},
fontFamily: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
},
computed: {
@@ -122,7 +138,7 @@ export default {
require('brace/mode/json')
require('brace/mode/javascript')
require('brace/mode/json')
require('brace/snippets/text')
require('brace/snippets/html')
require('brace/snippets/css')
@@ -147,33 +163,140 @@ export default {
useWorker: false,
readOnly: this.readOnly,
highlightActiveLine: this.highlightActiveLine,
cursorStyle: 'smooth',
// // minLines: this.height,
// maxPixelHeight: 100,
...(this.autoComplete && {
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
enableEmmet: true
enableEmmet: true,
}),
})
const self = this;
const staticWordCompleter = {
getCompletions: function (editor, session, pos, prefix, callback) {
var autoCompleteSuggestions = self.autoCompleteSuggestions;
callback(
null,
autoCompleteSuggestions.map(function ({ caption, value, meta }) {
return {
caption,
value,
meta,
};
})
);
},
};
editor.completers.push(staticWordCompleter);
...(this.fontFamily && { fontFamily: this.fontFamily }),
...(this.fontSize && { fontSize: this.fontSize }),
})
editor.on('input', this.updatePlaceholder)
this.updatePlaceholder(undefined, editor)
if (this.initExpressions) {
this.processExpressionAutoComplete(editor)
} else {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this
const staticWordCompleter = {
getCompletions: function (editor, session, pos, prefix, callback) {
const autoCompleteSuggestions = self.autoCompleteSuggestions
callback(
null,
autoCompleteSuggestions.map(function ({ caption, value, meta }) {
return {
caption,
value,
meta,
}
}),
)
},
}
editor.completers.push(staticWordCompleter)
}
},
updatePlaceholder (_, editor) {
if (!this.placeholder) return
const shouldShow = !editor.session.getValue().length
let node = editor.renderer.emptyMessageNode
if (!shouldShow && node) {
editor.renderer.scroller.removeChild(editor.renderer.emptyMessageNode)
editor.renderer.emptyMessageNode = null
} else if (shouldShow && !node) {
node = editor.renderer.emptyMessageNode = document.createElement('div')
node.textContent = this.placeholder
node.className = 'ace_placeholder'
node.style.padding = '7px 10px'
node.style.position = 'absolute'
node.style.zIndex = 9
node.style.opacity = 0.5
editor.renderer.scroller.appendChild(node)
}
},
processExpressionAutoComplete (editor) {
const staticWordCompleter = {
getCompletions: (editor, session, pos, prefix, callback) => {
const context = this.getContext(editor, session, pos)
const suggestions = this.getSuggestionsForContext(context)
callback(null, suggestions.map(suggestion => {
let caption = ''
let value = ''
if (typeof suggestion === 'string') {
caption = suggestion
value = suggestion
} else {
caption = suggestion.caption
value = suggestion.value
}
return {
caption,
value,
meta: 'variable',
completer: {
insertMatch: function (insertEditor, data) {
const insertValue = data.value
insertEditor.jumpToMatching()
const line = session.getLine(pos.row)
let lastSpaceIndex = line.lastIndexOf(' ') >= 0 ? line.lastIndexOf(' ') : 0
if (lastSpaceIndex > 0) {
lastSpaceIndex += 1
}
insertEditor.session.replace({
start: { row: pos.row, column: lastSpaceIndex },
end: { row: pos.row, column: pos.column },
}, insertValue)
},
},
}
}))
},
}
editor.completers = [staticWordCompleter]
editor.commands.on('afterExec', function (e) {
if (['insertstring', 'Return'].includes(e.command.name) || /^[\w.($]$/.test(e.args)) {
editor.execCommand('startAutocomplete')
}
})
editor.renderer.setScrollMargin(7, 7)
editor.renderer.setPadding(10)
},
getContext (editor, session, pos) {
const line = session.getLine(pos.row)
const lastSpaceIndex = line.lastIndexOf(' ') >= 0 ? line.lastIndexOf(' ') : 0
const textBeforeCursor = line.slice(lastSpaceIndex, pos.column)
const context = textBeforeCursor.split('.').slice(0, -1).join('.').trim()
return context
},
getSuggestionsForContext (context) {
const suggestions = this.autoCompleteSuggestions
return suggestions[context] || []
},
},
}

View File

@@ -0,0 +1,163 @@
<template>
<div
class="position-relative"
>
<c-ace-editor
v-model="editorValue"
auto-complete
init-expressions
:auto-complete-suggestions="autoCompleteSuggestions"
v-bind="{ ...$attrs, ...$props }"
v-on="$listeners"
/>
</div>
</template>
<script>
import CAceEditor from './CAceEditor.vue'
export default {
components: {
CAceEditor,
},
props: {
value: {
type: String,
default: '',
},
lang: {
type: String,
default: 'text',
},
height: {
type: String,
default: '80px',
},
showLineNumbers: {
type: Boolean,
default: false,
},
fontSize: {
type: String,
default: '14px',
},
border: {
type: Boolean,
default: true,
},
showPopout: {
type: Boolean,
default: false,
},
readOnly: {
type: Boolean,
default: false,
},
highlightActiveLine: {
type: Boolean,
default: false,
},
showPrintMargin: {
type: Boolean,
default: false,
},
suggestionParams: {
type: Array,
default: () => [],
},
fontFamily: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
},
computed: {
editorValue: {
get () {
return this.value
},
set (value = '') {
this.$emit('update:value', value)
},
},
autoCompleteSuggestions () {
return this.getRecordBasedSuggestions(this.suggestionParams)
},
},
methods: {
getRecordBasedSuggestions (params = []) {
const result = {}
function addSuggestion (key, caption, value) {
if (!result[key]) result[key] = []
result[key].push({ caption: caption, value: value })
}
function processProperties (prefix, properties, interpolate) {
(properties || []).forEach((prop) => {
if (typeof prop === 'string') {
const value = prefix + '.' + prop + (interpolate ? '}' : '')
addSuggestion(prefix, prop, value)
} else {
const nestedPrefix = prefix + '.' + prop.value + '.'
addSuggestion(prefix, prop.value, nestedPrefix)
if (prop.properties) {
(prop.properties || []).forEach((nestedProp) => {
const nestedValue = nestedPrefix + nestedProp + (interpolate ? '}' : '')
addSuggestion(prefix + '.' + prop.value, nestedProp, nestedValue)
})
}
}
})
}
(params || []).forEach((p) => {
if (typeof p === 'string') {
addSuggestion('', '', p)
} else {
const { interpolate = false, properties = [], value, root = true } = p
const prefix = interpolate ? '${' : ''
const suffix = interpolate && !properties.length ? '}' : ''
const prefixAsValue = prefix + value + suffix + (properties.length > 0 ? '.' : '')
if (root) {
addSuggestion('', '', prefixAsValue)
}
if (interpolate) {
addSuggestion('$', prefixAsValue.slice(1), prefixAsValue)
addSuggestion('${', prefixAsValue.slice(2), prefixAsValue)
}
if (properties.length) {
processProperties(prefix + value, properties, interpolate)
}
}
})
return result
},
},
}
</script>

View File

@@ -8,6 +8,7 @@ export { default as CRichTextInput } from './CRichTextInput/index.vue'
export { default as CInputCheckbox } from './CInputCheckbox.vue'
export { default as CInputColorPicker } from './CInputColorPicker.vue'
export { default as CAceEditor } from './CAceEditor.vue'
export { default as CInputExpression } from './CInputExpression.vue'
export {
CButtonSubmit,
} from './button'

View File

@@ -2209,7 +2209,7 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"
acorn-jsx@^5.2.0, acorn-jsx@^5.3.2:
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
@@ -4708,13 +4708,19 @@ eslint-plugin-standard@^4.0.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz#0c3bf3a67e853f8bbbc580fb4945fbf16f41b7c5"
integrity sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ==
eslint-plugin-vue@6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-6.1.2.tgz#4b05c28c83c0ec912669b64dbd998bb8bf692ef6"
integrity sha512-M75oAB+2a/LNkLKRbeEaS07EjzjIUaV7/hYoHAfRFeeF8ZMmCbahUn8nQLsLP85mkar24+zDU3QW2iT1JRsACw==
eslint-plugin-vue@^9.21.1:
version "9.27.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz#c22dae704a03d9ecefa81364ff89f60ce0481f94"
integrity sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==
dependencies:
semver "^5.6.0"
vue-eslint-parser "^7.0.0"
"@eslint-community/eslint-utils" "^4.4.0"
globals "^13.24.0"
natural-compare "^1.4.0"
nth-check "^2.1.1"
postcss-selector-parser "^6.0.15"
semver "^7.6.0"
vue-eslint-parser "^9.4.3"
xml-name-validator "^4.0.0"
eslint-scope@^4.0.3:
version "4.0.3"
@@ -4724,7 +4730,7 @@ eslint-scope@^4.0.3:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-scope@^5.0.0, eslint-scope@^5.1.1:
eslint-scope@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -4806,15 +4812,6 @@ esm@^3.2.25:
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
espree@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
dependencies:
acorn "^7.1.1"
acorn-jsx "^5.2.0"
eslint-visitor-keys "^1.1.0"
espree@^9.3.1, espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
@@ -5595,7 +5592,7 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^13.19.0:
globals@^13.19.0, globals@^13.24.0:
version "13.24.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
@@ -7916,6 +7913,13 @@ nth-check@^2.0.1:
dependencies:
boolbase "^1.0.0"
nth-check@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
dependencies:
boolbase "^1.0.0"
num2fraction@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@@ -9113,6 +9117,14 @@ postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.15:
version "6.1.2"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de"
integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9:
version "6.0.9"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f"
@@ -10246,6 +10258,11 @@ semver@^7.5.4:
dependencies:
lru-cache "^6.0.0"
semver@^7.6.0:
version "7.6.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
send@0.17.2:
version "0.17.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820"
@@ -11714,19 +11731,6 @@ vue-echarts@6.7.3:
resize-detector "^0.3.0"
vue-demi "^0.13.11"
vue-eslint-parser@^7.0.0:
version "7.11.0"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.11.0.tgz#214b5dea961007fcffb2ee65b8912307628d0daf"
integrity sha512-qh3VhDLeh773wjgNTl7ss0VejY9bMMa0GoDG2fQVyDzRFdiU3L7fw74tWZDHNQXdZqxO3EveQroa9ct39D2nqg==
dependencies:
debug "^4.1.1"
eslint-scope "^5.1.1"
eslint-visitor-keys "^1.1.0"
espree "^6.2.1"
esquery "^1.4.0"
lodash "^4.17.21"
semver "^6.3.0"
vue-eslint-parser@^9.3.2:
version "9.3.2"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz#6f9638e55703f1c77875a19026347548d93fd499"
@@ -11740,6 +11744,19 @@ vue-eslint-parser@^9.3.2:
lodash "^4.17.21"
semver "^7.3.6"
vue-eslint-parser@^9.4.3:
version "9.4.3"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz#9b04b22c71401f1e8bca9be7c3e3416a4bde76a8"
integrity sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==
dependencies:
debug "^4.3.4"
eslint-scope "^7.1.1"
eslint-visitor-keys "^3.3.0"
espree "^9.3.1"
esquery "^1.4.0"
lodash "^4.17.21"
semver "^7.3.6"
vue-functional-data-merge@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz#08a7797583b7f35680587f8a1d51d729aa1dc657"
@@ -12167,6 +12184,11 @@ xdg-basedir@^3.0.0:
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
xml-name-validator@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
xtend@^4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"