3
0

Update de-duplication UI on module and fields configuration pages

This commit is contained in:
Kelani Tolulope 2022-12-08 15:16:09 +01:00 committed by Vivek Patel
parent 74f372794b
commit 5f4d02ac84
6 changed files with 402 additions and 21 deletions

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

View File

@ -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
}
},
},
}

View File

@ -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: {

View File

@ -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')) {

View File

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

View File

@ -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 &#x23;{{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