Update de-duplication UI on module and fields configuration pages
This commit is contained in:
parent
74f372794b
commit
5f4d02ac84
215
client/web/compose/src/components/Admin/Module/UniqueValues.vue
Normal file
215
client/web/compose/src/components/Admin/Module/UniqueValues.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(rule, index) in rules"
|
||||
:key="index"
|
||||
>
|
||||
<hr v-if="index">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 v-html="$t('uniqueValueConstraint', { index: index + 1, interpolation: { escapeValue: false } })" />
|
||||
|
||||
<div class="px-4">
|
||||
<c-input-confirm
|
||||
@confirmed="rules.splice(index, 1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<b-form-checkbox
|
||||
v-model="rule.strict"
|
||||
switch
|
||||
class="mt-3"
|
||||
>
|
||||
{{ $t("preventRecordsSave") }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<b-table-simple
|
||||
v-if="rule.constraints.length > 0"
|
||||
borderless
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ $t("field") }}
|
||||
</th>
|
||||
<th>
|
||||
{{ $t("type") }}
|
||||
</th>
|
||||
<th style="width: 250px">
|
||||
{{ $t("valueModifiers") }}
|
||||
</th>
|
||||
<th style="width: 250px">
|
||||
{{ $t("multiValues") }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="rule.constraints">
|
||||
<tr
|
||||
v-for="(constraint, consIndex) in rule.constraints"
|
||||
:key="`constraint-${consIndex}`"
|
||||
>
|
||||
<td>{{ getOptionLabel(getField(constraint.attribute)) }}</td>
|
||||
<td>{{ getField(constraint.attribute).kind }}</td>
|
||||
<td>
|
||||
<b-form-select
|
||||
v-model="constraint.modifier"
|
||||
:options="modifierOptions"
|
||||
size="sm"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<b-form-select
|
||||
v-model="constraint.multiValue"
|
||||
:options="multiValueOptions"
|
||||
:disabled="!getField(constraint.attribute).isMulti"
|
||||
size="sm"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right p-0 px-4 align-middle">
|
||||
<c-input-confirm
|
||||
button-class="text-right"
|
||||
@confirmed="rule.constraints.splice(consIndex, 1)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</b-table-simple>
|
||||
|
||||
<b-form-group>
|
||||
<b-input-group>
|
||||
<vue-select
|
||||
v-model="rule.currentField"
|
||||
:placeholder="$t('searchFields')"
|
||||
:get-option-label="getOptionLabel"
|
||||
:options="filterFieldOptions(rule)"
|
||||
class="bg-white"
|
||||
/>
|
||||
|
||||
<b-input-group-append>
|
||||
<b-button
|
||||
variant="primary"
|
||||
class="px-4"
|
||||
@click="updateRuleConstraint(rule)"
|
||||
>
|
||||
{{ $t("add") }}
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<b-button
|
||||
size="lg"
|
||||
variant="link"
|
||||
class="d-flex align-items-center text-decoration-none p-0 mt-3"
|
||||
@click="addNewConstraint"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'plus']"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ $t("addNewConstraint") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compose } from '@cortezaproject/corteza-js'
|
||||
import { VueSelect } from 'vue-select'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'module',
|
||||
keyPrefix: 'edit.config.uniqueValues',
|
||||
},
|
||||
|
||||
components: {
|
||||
VueSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
module: {
|
||||
type: compose.Module,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
rules: {
|
||||
get () {
|
||||
this.module.config.recordDeDup.rules.forEach(rule => {
|
||||
if (rule.constraints === null) {
|
||||
rule.constraints = []
|
||||
}
|
||||
})
|
||||
|
||||
return this.module.config.recordDeDup.rules
|
||||
},
|
||||
set (value) {
|
||||
this.module.config.recordDeDup.rules = value
|
||||
},
|
||||
},
|
||||
|
||||
modifierOptions () {
|
||||
return [
|
||||
{ value: 'ignore-case', text: this.$t('ignoreCase') },
|
||||
{ value: 'fuzzy-match', text: this.$t('fuzzyMatch') },
|
||||
{ value: 'sounds-like', text: this.$t('soundsLike') },
|
||||
{ value: 'case-sensitive', text: this.$t('caseSensitive') },
|
||||
]
|
||||
},
|
||||
|
||||
multiValueOptions () {
|
||||
return [
|
||||
{ value: 'one-of', text: this.$t('oneOf') },
|
||||
{ value: 'equal', text: this.$t('equal') },
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addNewConstraint () {
|
||||
this.rules.push({
|
||||
name: '',
|
||||
strict: true,
|
||||
constraints: [],
|
||||
})
|
||||
},
|
||||
|
||||
updateRuleConstraint (rule) {
|
||||
rule.constraints.push({
|
||||
attribute: rule.currentField.name,
|
||||
modifier: 'case-sensitive',
|
||||
multiValue: 'equal',
|
||||
type: rule.currentField.kind,
|
||||
isMulti: rule.currentField.isMulti,
|
||||
})
|
||||
|
||||
rule.currentField = undefined
|
||||
},
|
||||
|
||||
filterFieldOptions (rule) {
|
||||
const selectedFields = rule.constraints.map(({ attribute }) => attribute)
|
||||
return this.module.fields.filter(({ name }) => !selectedFields.includes(name))
|
||||
},
|
||||
|
||||
getField (attribute) {
|
||||
const field = this.module.fields.find(
|
||||
({ name }) => name === attribute,
|
||||
)
|
||||
|
||||
return field || {}
|
||||
},
|
||||
|
||||
getOptionLabel ({ kind, label, name }) {
|
||||
return label || name || kind
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -56,7 +56,7 @@
|
||||
<b-button
|
||||
variant="link"
|
||||
class="p-0 ml-1 mr-auto"
|
||||
@click="add()"
|
||||
@click="field.expressions.validators.push({ test: '', error: '' })"
|
||||
>
|
||||
{{ $t('sanitizers.add') }}
|
||||
</b-button>
|
||||
@ -116,6 +116,60 @@
|
||||
{{ $t('validators.description') }}
|
||||
</b-form-text>
|
||||
</b-form-group>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<h5>{{ $t('constraints.label') }}</h5>
|
||||
<b-form-checkbox
|
||||
v-model="fieldConstraint.exists"
|
||||
class="mt-3"
|
||||
@change="toggleFieldConstraint"
|
||||
>
|
||||
{{ $t('constraints.description') }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('constraints.valueModifiers')"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="constraint.modifier"
|
||||
:options="modifierOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('constraints.multiValues')"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="constraint.multiValue"
|
||||
:options="multiValueOptions"
|
||||
:disabled="!field.isMulti"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div
|
||||
v-if="fieldConstraint.total"
|
||||
class="mt-3"
|
||||
>
|
||||
<i>
|
||||
{{ $t('constraints.totalFieldConstraintCount', { total: fieldConstraint.total }) }}
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -149,6 +203,13 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
loaded: false,
|
||||
fieldConstraint: {
|
||||
ruleIndex: null,
|
||||
total: 0,
|
||||
exists: false,
|
||||
index: null,
|
||||
},
|
||||
rule: {},
|
||||
}
|
||||
},
|
||||
|
||||
@ -158,9 +219,43 @@ export default {
|
||||
const [year, month] = VERSION.split('.')
|
||||
return `https://docs.cortezaproject.org/corteza-docs/${year}.${month}/integrator-guide/compose-configuration/index.html`
|
||||
},
|
||||
|
||||
modifierOptions () {
|
||||
return [
|
||||
{ value: 'ignore-case', text: this.$t('constraints.ignoreCase') },
|
||||
{ value: 'fuzzy-match', text: this.$t('constraints.fuzzyMatch') },
|
||||
{ value: 'sounds-like', text: this.$t('constraints.soundsLike') },
|
||||
{ value: 'case-sensitive', text: this.$t('constraints.caseSensitive') },
|
||||
]
|
||||
},
|
||||
|
||||
multiValueOptions () {
|
||||
return [
|
||||
{ value: 'one-of', text: this.$t('constraints.oneOf') },
|
||||
{ value: 'equal', text: this.$t('constraints.equal') },
|
||||
]
|
||||
},
|
||||
|
||||
constraint: {
|
||||
get () {
|
||||
if (this.module.config.recordDeDup.rules[this.fieldConstraint.ruleIndex]) {
|
||||
return this.module.config.recordDeDup.rules[this.fieldConstraint.ruleIndex].constraints[this.fieldConstraint.index]
|
||||
}
|
||||
|
||||
return {}
|
||||
},
|
||||
|
||||
set (value) {
|
||||
if (this.module.config.recordDeDup.rules[this.fieldConstraint.ruleIndex]) {
|
||||
this.module.config.recordDeDup.rules[this.fieldConstraint.ruleIndex].constraints[this.fieldConstraint.index] = value
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.checkForFieldConstraint()
|
||||
|
||||
if (!this.field.expressions.sanitizers) {
|
||||
this.$set(this.field.expressions, 'sanitizers', [])
|
||||
}
|
||||
@ -189,9 +284,45 @@ export default {
|
||||
return !(value.validatorID && value.validatorID !== NoID)
|
||||
},
|
||||
|
||||
add () {
|
||||
this.field.expressions.validators
|
||||
.push({ test: '', error: '' })
|
||||
checkForFieldConstraint () {
|
||||
this.module.config.recordDeDup.rules.forEach((rule, x) => {
|
||||
const { constraints } = rule
|
||||
|
||||
constraints.forEach((constraint, i) => {
|
||||
if (constraint.attribute === this.field.name) {
|
||||
if (constraints.length === 1) {
|
||||
this.fieldConstraint.exists = true
|
||||
this.fieldConstraint.index = i
|
||||
this.fieldConstraint.ruleIndex = x
|
||||
}
|
||||
|
||||
this.fieldConstraint.total += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
toggleFieldConstraint (value) {
|
||||
if (!value) {
|
||||
this.module.config.recordDeDup.rules.splice(this.fieldConstraint.ruleIndex, 1)
|
||||
|
||||
this.fieldConstraint.ruleIndex = null
|
||||
this.fieldConstraint.index = null
|
||||
} else if (this.fieldConstraint.ruleIndex == null) {
|
||||
this.module.config.recordDeDup.rules.push({
|
||||
name: '',
|
||||
strict: true,
|
||||
constraints: [{
|
||||
attribute: this.field.name,
|
||||
modifier: 'case-sensitive',
|
||||
multiValue: 'equal',
|
||||
type: this.field.kind,
|
||||
}],
|
||||
})
|
||||
|
||||
this.fieldConstraint.ruleIndex = this.module.config.recordDeDup.rules.length - 1
|
||||
this.fieldConstraint.index = this.module.config.recordDeDup.rules[this.fieldConstraint.ruleIndex].constraints.length - 1
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -340,10 +340,9 @@
|
||||
</b-tab>
|
||||
|
||||
<b-tab
|
||||
v-if="module.config.recordDeDup.enabled"
|
||||
:title="$t('edit.config.validation.title')"
|
||||
:title="$t('edit.config.uniqueValues.title')"
|
||||
>
|
||||
<validation
|
||||
<unique-values
|
||||
:module="module"
|
||||
/>
|
||||
</b-tab>
|
||||
@ -458,7 +457,7 @@ import DalSettings from 'corteza-webapp-compose/src/components/Admin/Module/DalS
|
||||
import RecordRevisionsSettings from 'corteza-webapp-compose/src/components/Admin/Module/RecordRevisionsSettings'
|
||||
import DataPrivacySettings from 'corteza-webapp-compose/src/components/Admin/Module/DataPrivacySettings'
|
||||
import ModuleTranslator from 'corteza-webapp-compose/src/components/Admin/Module/ModuleTranslator'
|
||||
import Validation from 'corteza-webapp-compose/src/components/Admin/Module/Validation'
|
||||
import UniqueValues from 'corteza-webapp-compose/src/components/Admin/Module/UniqueValues'
|
||||
import RelatedPages from 'corteza-webapp-compose/src/components/Admin/Module/RelatedPages'
|
||||
import { compose, NoID } from '@cortezaproject/corteza-js'
|
||||
import EditorToolbar from 'corteza-webapp-compose/src/components/Admin/EditorToolbar'
|
||||
@ -485,8 +484,8 @@ export default {
|
||||
ModuleTranslator,
|
||||
RelatedPages,
|
||||
EditorToolbar,
|
||||
Validation,
|
||||
Export,
|
||||
UniqueValues,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
||||
@ -58,8 +58,6 @@ interface Config {
|
||||
};
|
||||
|
||||
recordDeDup: {
|
||||
enabled: boolean;
|
||||
strict: boolean;
|
||||
rules: RecordDeDupRule[];
|
||||
};
|
||||
}
|
||||
@ -71,10 +69,17 @@ interface ConfigDiscoveryAccess {
|
||||
};
|
||||
}
|
||||
|
||||
interface Constraint {
|
||||
attribute: string;
|
||||
modifier: string;
|
||||
multiValue: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface RecordDeDupRule {
|
||||
name: string;
|
||||
name?: string;
|
||||
strict: boolean;
|
||||
attributes: string[];
|
||||
constraints: Constraint[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,10 +168,12 @@ export class Module {
|
||||
},
|
||||
|
||||
recordDeDup: {
|
||||
// Always true for now since empty array of rules is the same as disabling it
|
||||
enabled: true,
|
||||
strict: false,
|
||||
rules: [],
|
||||
rules: [
|
||||
{
|
||||
strict: true,
|
||||
constraints: []
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@ -224,9 +231,6 @@ export class Module {
|
||||
this.config = merge({}, this.config, m.config)
|
||||
|
||||
// Remove when we improve duplicate detection, for now its always enabled
|
||||
if (this.config.recordDeDup) {
|
||||
this.config.recordDeDup.enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
if (IsOf(m, 'labels')) {
|
||||
|
||||
@ -222,3 +222,16 @@ valueExpr:
|
||||
label:
|
||||
required: Required
|
||||
multi: Multi value
|
||||
constraints:
|
||||
label: Unique value constraint
|
||||
description: Use unique value constraints for this field
|
||||
ignoreCase: Ignore case
|
||||
fuzzyMatch: Fuzzy match
|
||||
soundsLike: Sounds like
|
||||
caseSensitive: Case sensitive
|
||||
oneOf: One of
|
||||
none: None
|
||||
equal: Equal
|
||||
valueModifiers: Value modifiers
|
||||
multiValues: Multi-field values
|
||||
totalFieldConstraintCount: The field is used in {{total}} other unique value constraint. See "unique values" tab on module editor.
|
||||
|
||||
@ -165,6 +165,26 @@ edit:
|
||||
label: Soft duplicate value validation
|
||||
description: Record will be saved and user will be presented with a warning when a duplicate value is detected in the selected fields in any existing record of this module
|
||||
|
||||
uniqueValues:
|
||||
title: Unique values
|
||||
preventRecordsSave: Prevent record saving if duplicate values are found
|
||||
warningLabel: Warning or error message toast when constraint matches
|
||||
valueModifiers: Value modifiers
|
||||
multiValues: Multi-field values
|
||||
add: Add
|
||||
searchFields: Search fields
|
||||
ignoreCase: Ignore case
|
||||
fuzzyMatch: Fuzzy match
|
||||
soundsLike: Sounds like
|
||||
caseSensitive: Case sensitive
|
||||
oneOf: One of
|
||||
equal: Equal
|
||||
warningMessage: Warning or error message toast when constraint matches
|
||||
field: Field
|
||||
type: Type
|
||||
none: None
|
||||
addNewConstraint: Add new constraint
|
||||
uniqueValueConstraint: Unique value constraint #{{index}}
|
||||
|
||||
federated: Federated
|
||||
forModule:
|
||||
@ -210,4 +230,3 @@ searchPlaceholder: Type here to search all modules in this namespace
|
||||
title: List of Modules
|
||||
tooltip:
|
||||
permissions: Module permissions
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user