Re-working the error handling for the SQL relationship modal, as well as adding some better defaults for the majority of the options to make the UI a bit easier to use.
This commit is contained in:
parent
50b3c06dbd
commit
3a51933801
2 changed files with 208 additions and 141 deletions
|
@ -10,17 +10,17 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import { RelationshipErrorChecker } from "./relationshipErrors"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let save
|
export let save
|
||||||
export let datasource
|
export let datasource
|
||||||
export let plusTables = []
|
export let plusTables = []
|
||||||
export let fromRelationship = {}
|
export let fromRelationship = {}
|
||||||
export let toRelationship = {}
|
export let toRelationship = {}
|
||||||
|
export let selectedFromTable
|
||||||
export let close
|
export let close
|
||||||
|
|
||||||
const colNotSet = "Please specify a column name"
|
|
||||||
const relationshipAlreadyExists =
|
|
||||||
"A relationship between these tables already exists."
|
|
||||||
const relationshipTypes = [
|
const relationshipTypes = [
|
||||||
{
|
{
|
||||||
label: "One to Many",
|
label: "One to Many",
|
||||||
|
@ -42,8 +42,11 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
let tableOptions
|
let tableOptions
|
||||||
|
let errorChecker = new RelationshipErrorChecker(
|
||||||
|
invalidThroughTable,
|
||||||
|
relationshipExists
|
||||||
|
)
|
||||||
let errors = {}
|
let errors = {}
|
||||||
let hasClickedSave = !!fromRelationship.relationshipType
|
|
||||||
let fromPrimary,
|
let fromPrimary,
|
||||||
fromForeign,
|
fromForeign,
|
||||||
fromTable,
|
fromTable,
|
||||||
|
@ -51,12 +54,19 @@
|
||||||
throughTable,
|
throughTable,
|
||||||
fromColumn,
|
fromColumn,
|
||||||
toColumn
|
toColumn
|
||||||
let fromId, toId, throughId, throughToKey, throughFromKey
|
let fromId = selectedFromTable?._id,
|
||||||
|
toId,
|
||||||
|
throughId,
|
||||||
|
throughToKey,
|
||||||
|
throughFromKey
|
||||||
let isManyToMany, isManyToOne, relationshipType
|
let isManyToMany, isManyToOne, relationshipType
|
||||||
|
let hasValidated = false
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!fromPrimary) {
|
if (!fromPrimary) {
|
||||||
fromPrimary = fromRelationship.foreignKey
|
fromPrimary = fromRelationship.foreignKey
|
||||||
|
}
|
||||||
|
if (!fromForeign) {
|
||||||
fromForeign = toRelationship.foreignKey
|
fromForeign = toRelationship.foreignKey
|
||||||
}
|
}
|
||||||
if (!fromColumn && !errors.fromColumn) {
|
if (!fromColumn && !errors.fromColumn) {
|
||||||
|
@ -77,7 +87,8 @@
|
||||||
throughToKey = fromRelationship.throughTo
|
throughToKey = fromRelationship.throughTo
|
||||||
}
|
}
|
||||||
if (!relationshipType) {
|
if (!relationshipType) {
|
||||||
relationshipType = fromRelationship.relationshipType
|
relationshipType =
|
||||||
|
fromRelationship.relationshipType || RelationshipTypes.MANY_TO_ONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +96,7 @@
|
||||||
label: table.name,
|
label: table.name,
|
||||||
value: table._id,
|
value: table._id,
|
||||||
}))
|
}))
|
||||||
$: valid = getErrorCount(errors) === 0 || !hasClickedSave
|
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
||||||
|
|
||||||
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
|
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||||
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
|
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
|
||||||
|
@ -95,10 +106,20 @@
|
||||||
|
|
||||||
$: toRelationship.relationshipType = fromRelationship?.relationshipType
|
$: toRelationship.relationshipType = fromRelationship?.relationshipType
|
||||||
|
|
||||||
const getErrorCount = errors =>
|
function getErrorCount(errors) {
|
||||||
Object.entries(errors)
|
return Object.entries(errors)
|
||||||
.filter(entry => !!entry[1])
|
.filter(entry => !!entry[1])
|
||||||
.map(entry => entry[0]).length
|
.map(entry => entry[0]).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function allRequiredAttributesSet() {
|
||||||
|
const base = fromTable && toTable && fromColumn && toColumn
|
||||||
|
if (isManyToOne) {
|
||||||
|
return base && fromPrimary && fromForeign
|
||||||
|
} else {
|
||||||
|
return base && throughTable && throughFromKey && throughToKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function invalidThroughTable() {
|
function invalidThroughTable() {
|
||||||
// need to know the foreign key columns to check error
|
// need to know the foreign key columns to check error
|
||||||
|
@ -118,93 +139,48 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate() {
|
function validate() {
|
||||||
const isMany = relationshipType === RelationshipTypes.MANY_TO_MANY
|
if (!allRequiredAttributesSet() && !hasValidated) {
|
||||||
const tableNotSet = "Please specify a table"
|
return
|
||||||
const foreignKeyNotSet = "Please pick a foreign key"
|
}
|
||||||
|
hasValidated = true
|
||||||
|
errorChecker.setType(relationshipType)
|
||||||
const errObj = {}
|
const errObj = {}
|
||||||
if (!relationshipType) {
|
errObj.relationshipType = errorChecker.relationshipTypeSet(relationshipType)
|
||||||
errObj.relationshipType = "Please specify a relationship type"
|
errObj.fromTable = errorChecker.tableSet(fromTable)
|
||||||
}
|
errObj.toTable = errorChecker.tableSet(toTable)
|
||||||
if (!fromTable) {
|
errObj.throughTable = errorChecker.throughTableSet(throughTable)
|
||||||
errObj.fromTable = tableNotSet
|
errObj.throughFromKey = errorChecker.manyForeignKeySet(throughFromKey)
|
||||||
}
|
errObj.throughToKey = errorChecker.manyForeignKeySet(throughToKey)
|
||||||
if (!toTable) {
|
errObj.throughTable = errorChecker.throughIsNullable()
|
||||||
errObj.toTable = tableNotSet
|
errObj.fromForeign = errorChecker.foreignKeySet(fromForeign)
|
||||||
}
|
errObj.fromPrimary = errorChecker.foreignKeySet(fromPrimary)
|
||||||
if (isMany && !throughTable) {
|
errObj.fromTable = errorChecker.doesRelationshipExists()
|
||||||
errObj.throughTable = tableNotSet
|
errObj.toTable = errorChecker.doesRelationshipExists()
|
||||||
}
|
|
||||||
if (isMany && !throughFromKey) {
|
|
||||||
errObj.throughFromKey = foreignKeyNotSet
|
|
||||||
}
|
|
||||||
if (isMany && !throughToKey) {
|
|
||||||
errObj.throughToKey = foreignKeyNotSet
|
|
||||||
}
|
|
||||||
if (invalidThroughTable()) {
|
|
||||||
errObj.throughTable =
|
|
||||||
"Ensure non-key columns are nullable or auto-generated"
|
|
||||||
}
|
|
||||||
if (!isMany && !fromForeign) {
|
|
||||||
errObj.fromForeign = foreignKeyNotSet
|
|
||||||
}
|
|
||||||
if (!fromColumn) {
|
|
||||||
errObj.fromColumn = colNotSet
|
|
||||||
}
|
|
||||||
if (!toColumn) {
|
|
||||||
errObj.toColumn = colNotSet
|
|
||||||
}
|
|
||||||
if (!isMany && !fromPrimary) {
|
|
||||||
errObj.fromPrimary = "Please pick the primary key"
|
|
||||||
}
|
|
||||||
if (isMany && relationshipExists()) {
|
|
||||||
errObj.fromTable = relationshipAlreadyExists
|
|
||||||
errObj.toTable = relationshipAlreadyExists
|
|
||||||
}
|
|
||||||
|
|
||||||
// currently don't support relationships back onto the table itself, needs to relate out
|
// currently don't support relationships back onto the table itself, needs to relate out
|
||||||
const tableError = "From/to/through tables must be different"
|
errObj.fromTable = errorChecker.differentTables(fromId, toId, throughId)
|
||||||
if (fromTable && (fromTable === toTable || fromTable === throughTable)) {
|
errObj.toTable = errorChecker.differentTables(toId, fromId, throughId)
|
||||||
errObj.fromTable = tableError
|
errObj.throughTable = errorChecker.differentTables(throughId, fromId, toId)
|
||||||
}
|
errObj.fromColumn = errorChecker.columnBeingUsed(
|
||||||
if (toTable && (toTable === fromTable || toTable === throughTable)) {
|
toTable,
|
||||||
errObj.toTable = tableError
|
fromColumn,
|
||||||
}
|
originalFromColumnName
|
||||||
if (
|
)
|
||||||
throughTable &&
|
errObj.toColumn = errorChecker.columnBeingUsed(
|
||||||
(throughTable === fromTable || throughTable === toTable)
|
fromTable,
|
||||||
) {
|
toColumn,
|
||||||
errObj.throughTable = tableError
|
originalToColumnName
|
||||||
}
|
)
|
||||||
const colError = "Column name cannot be an existing column"
|
errObj.fromForeign = errorChecker.typeMismatch(
|
||||||
if (isColumnNameBeingUsed(toTable, fromColumn, originalFromColumnName)) {
|
fromTable,
|
||||||
errObj.fromColumn = colError
|
toTable,
|
||||||
}
|
fromPrimary,
|
||||||
if (isColumnNameBeingUsed(fromTable, toColumn, originalToColumnName)) {
|
fromForeign
|
||||||
errObj.toColumn = colError
|
)
|
||||||
}
|
console.log(errObj)
|
||||||
|
|
||||||
let fromType, toType
|
|
||||||
if (fromPrimary && fromForeign) {
|
|
||||||
fromType = fromTable?.schema[fromPrimary]?.type
|
|
||||||
toType = toTable?.schema[fromForeign]?.type
|
|
||||||
}
|
|
||||||
if (fromType && toType && fromType !== toType) {
|
|
||||||
errObj.fromForeign =
|
|
||||||
"Column type of the foreign key must match the primary key"
|
|
||||||
}
|
|
||||||
|
|
||||||
errors = errObj
|
errors = errObj
|
||||||
return getErrorCount(errors) === 0
|
return getErrorCount(errors) === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function isColumnNameBeingUsed(table, columnName, originalName) {
|
|
||||||
if (!table || !columnName || columnName === originalName) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const keys = Object.keys(table.schema).map(key => key.toLowerCase())
|
|
||||||
return keys.indexOf(columnName.toLowerCase()) !== -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRelationships() {
|
function buildRelationships() {
|
||||||
const id = Helpers.uuid()
|
const id = Helpers.uuid()
|
||||||
//Map temporary variables
|
//Map temporary variables
|
||||||
|
@ -320,7 +296,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveRelationship() {
|
async function saveRelationship() {
|
||||||
hasClickedSave = true
|
|
||||||
if (!validate()) {
|
if (!validate()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -342,6 +317,20 @@
|
||||||
await tables.fetch()
|
await tables.fetch()
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changed(fn) {
|
||||||
|
if (fn && typeof fn === "function") {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (selectedFromTable) {
|
||||||
|
fromColumn = selectedFromTable.name
|
||||||
|
fromPrimary = selectedFromTable?.primary[0] || null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -355,34 +344,32 @@
|
||||||
options={relationshipTypes}
|
options={relationshipTypes}
|
||||||
bind:value={relationshipType}
|
bind:value={relationshipType}
|
||||||
bind:error={errors.relationshipType}
|
bind:error={errors.relationshipType}
|
||||||
on:change={() => (errors.relationshipType = null)}
|
on:change={changed}
|
||||||
/>
|
/>
|
||||||
<div class="headings">
|
<div class="headings">
|
||||||
<Detail>Tables</Detail>
|
<Detail>Tables</Detail>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
{#if !selectedFromTable}
|
||||||
label="Select from table"
|
<Select
|
||||||
options={tableOptions}
|
label="Select from table"
|
||||||
bind:value={fromId}
|
options={tableOptions}
|
||||||
bind:error={errors.fromTable}
|
bind:value={fromId}
|
||||||
on:change={e => {
|
bind:error={errors.fromTable}
|
||||||
fromColumn = tableOptions.find(opt => opt.value === e.detail)?.label || ""
|
on:change={e =>
|
||||||
if (errors.fromTable === relationshipAlreadyExists) {
|
changed(() => {
|
||||||
errors.toColumn = null
|
const table = plusTables.find(tbl => tbl._id === e.detail)
|
||||||
}
|
fromColumn = table?.name || ""
|
||||||
errors.fromTable = null
|
fromPrimary = table?.primary?.[0]
|
||||||
errors.fromColumn = null
|
})}
|
||||||
errors.toTable = null
|
/>
|
||||||
errors.throughTable = null
|
{/if}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{#if isManyToOne && fromTable}
|
{#if isManyToOne && fromTable}
|
||||||
<Select
|
<Select
|
||||||
label={`Primary Key (${fromTable.name})`}
|
label={`Primary Key (${fromTable.name})`}
|
||||||
options={Object.keys(fromTable.schema)}
|
options={Object.keys(fromTable.schema)}
|
||||||
bind:value={fromPrimary}
|
bind:value={fromPrimary}
|
||||||
bind:error={errors.fromPrimary}
|
bind:error={errors.fromPrimary}
|
||||||
on:change={() => (errors.fromPrimary = null)}
|
on:change={changed}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Select
|
<Select
|
||||||
|
@ -390,16 +377,12 @@
|
||||||
options={tableOptions}
|
options={tableOptions}
|
||||||
bind:value={toId}
|
bind:value={toId}
|
||||||
bind:error={errors.toTable}
|
bind:error={errors.toTable}
|
||||||
on:change={e => {
|
on:change={e =>
|
||||||
toColumn = tableOptions.find(opt => opt.value === e.detail)?.label || ""
|
changed(() => {
|
||||||
if (errors.toTable === relationshipAlreadyExists) {
|
const table = plusTables.find(tbl => tbl._id === e.detail)
|
||||||
errors.fromColumn = null
|
toColumn = table.name || ""
|
||||||
}
|
fromForeign = null
|
||||||
errors.toTable = null
|
})}
|
||||||
errors.toColumn = null
|
|
||||||
errors.fromTable = null
|
|
||||||
errors.throughTable = null
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{#if isManyToMany}
|
{#if isManyToMany}
|
||||||
<Select
|
<Select
|
||||||
|
@ -407,11 +390,11 @@
|
||||||
options={tableOptions}
|
options={tableOptions}
|
||||||
bind:value={throughId}
|
bind:value={throughId}
|
||||||
bind:error={errors.throughTable}
|
bind:error={errors.throughTable}
|
||||||
on:change={() => {
|
on:change={() =>
|
||||||
errors.fromTable = null
|
changed(() => {
|
||||||
errors.toTable = null
|
throughToKey = null
|
||||||
errors.throughTable = null
|
throughFromKey = null
|
||||||
}}
|
})}
|
||||||
/>
|
/>
|
||||||
{#if fromTable && toTable && throughTable}
|
{#if fromTable && toTable && throughTable}
|
||||||
<Select
|
<Select
|
||||||
|
@ -419,24 +402,24 @@
|
||||||
options={Object.keys(throughTable?.schema)}
|
options={Object.keys(throughTable?.schema)}
|
||||||
bind:value={throughToKey}
|
bind:value={throughToKey}
|
||||||
bind:error={errors.throughToKey}
|
bind:error={errors.throughToKey}
|
||||||
on:change={e => {
|
on:change={e =>
|
||||||
if (throughFromKey === e.detail) {
|
changed(() => {
|
||||||
throughFromKey = null
|
if (throughFromKey === e.detail) {
|
||||||
}
|
throughFromKey = null
|
||||||
errors.throughToKey = null
|
}
|
||||||
}}
|
})}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label={`Foreign Key (${toTable?.name})`}
|
label={`Foreign Key (${toTable?.name})`}
|
||||||
options={Object.keys(throughTable?.schema)}
|
options={Object.keys(throughTable?.schema)}
|
||||||
bind:value={throughFromKey}
|
bind:value={throughFromKey}
|
||||||
bind:error={errors.throughFromKey}
|
bind:error={errors.throughFromKey}
|
||||||
on:change={e => {
|
on:change={e =>
|
||||||
if (throughToKey === e.detail) {
|
changed(() => {
|
||||||
throughToKey = null
|
if (throughToKey === e.detail) {
|
||||||
}
|
throughToKey = null
|
||||||
errors.throughFromKey = null
|
}
|
||||||
}}
|
})}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if isManyToOne && toTable}
|
{:else if isManyToOne && toTable}
|
||||||
|
@ -445,7 +428,7 @@
|
||||||
options={Object.keys(toTable?.schema)}
|
options={Object.keys(toTable?.schema)}
|
||||||
bind:value={fromForeign}
|
bind:value={fromForeign}
|
||||||
bind:error={errors.fromForeign}
|
bind:error={errors.fromForeign}
|
||||||
on:change={() => (errors.fromForeign = null)}
|
on:change={changed}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="headings">
|
<div class="headings">
|
||||||
|
@ -459,15 +442,13 @@
|
||||||
label="From table column"
|
label="From table column"
|
||||||
bind:value={fromColumn}
|
bind:value={fromColumn}
|
||||||
bind:error={errors.fromColumn}
|
bind:error={errors.fromColumn}
|
||||||
on:change={e => {
|
on:change={changed}
|
||||||
errors.fromColumn = e.detail?.length > 0 ? null : colNotSet
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="To table column"
|
label="To table column"
|
||||||
bind:value={toColumn}
|
bind:value={toColumn}
|
||||||
bind:error={errors.toColumn}
|
bind:error={errors.toColumn}
|
||||||
on:change={e => (errors.toColumn = e.detail?.length > 0 ? null : colNotSet)}
|
on:change={changed}
|
||||||
/>
|
/>
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
{#if originalFromColumnName != null}
|
{#if originalFromColumnName != null}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { RelationshipTypes } from "constants/backend"
|
||||||
|
|
||||||
|
export const typeMismatch =
|
||||||
|
"Column type of the foreign key must match the primary key"
|
||||||
|
export const columnCantExist = "Column name cannot be an existing column"
|
||||||
|
export const mustBeDifferentTables = "From/to/through tables must be different"
|
||||||
|
export const primaryKeyNotSet = "Please pick the primary key"
|
||||||
|
export const throughNotNullable =
|
||||||
|
"Ensure non-key columns are nullable or auto-generated"
|
||||||
|
export const noRelationshipType = "Please specify a relationship type"
|
||||||
|
export const tableNotSet = "Please specify a table"
|
||||||
|
export const foreignKeyNotSet = "Please pick a foreign key"
|
||||||
|
export const relationshipAlreadyExists =
|
||||||
|
"A relationship between these tables already exists"
|
||||||
|
|
||||||
|
function isColumnNameBeingUsed(table, columnName, originalName) {
|
||||||
|
if (!table || !columnName || columnName === originalName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const keys = Object.keys(table.schema).map(key => key.toLowerCase())
|
||||||
|
return keys.indexOf(columnName.toLowerCase()) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RelationshipErrorChecker {
|
||||||
|
constructor(invalidThroughTableFn, relationshipExistsFn) {
|
||||||
|
this.invalidThroughTable = invalidThroughTableFn
|
||||||
|
this.relationshipExists = relationshipExistsFn
|
||||||
|
}
|
||||||
|
|
||||||
|
setType(type) {
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
isMany() {
|
||||||
|
return this.type === RelationshipTypes.MANY_TO_MANY
|
||||||
|
}
|
||||||
|
|
||||||
|
relationshipTypeSet(type) {
|
||||||
|
return !type ? noRelationshipType : null
|
||||||
|
}
|
||||||
|
|
||||||
|
tableSet(table) {
|
||||||
|
return !table ? tableNotSet : null
|
||||||
|
}
|
||||||
|
|
||||||
|
throughTableSet(table) {
|
||||||
|
return this.isMany() && !table ? tableNotSet : null
|
||||||
|
}
|
||||||
|
|
||||||
|
manyForeignKeySet(key) {
|
||||||
|
return this.isMany() && !key ? foreignKeyNotSet : null
|
||||||
|
}
|
||||||
|
|
||||||
|
foreignKeySet(key) {
|
||||||
|
return !this.isMany() && !key ? foreignKeyNotSet : null
|
||||||
|
}
|
||||||
|
|
||||||
|
throughIsNullable() {
|
||||||
|
return this.invalidThroughTable() ? throughNotNullable : null
|
||||||
|
}
|
||||||
|
|
||||||
|
doesRelationshipExists() {
|
||||||
|
return this.isMany() && this.relationshipExists()
|
||||||
|
? relationshipAlreadyExists
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
differentTables(table1, table2, table3) {
|
||||||
|
// currently don't support relationships back onto the table itself, needs to relate out
|
||||||
|
const error = table1 && (table1 === table2 || (table3 && table1 === table3))
|
||||||
|
return error ? mustBeDifferentTables : null
|
||||||
|
}
|
||||||
|
|
||||||
|
columnBeingUsed(table, column, ogName) {
|
||||||
|
return isColumnNameBeingUsed(table, column, ogName) ? columnCantExist : null
|
||||||
|
}
|
||||||
|
|
||||||
|
typeMismatch(fromTable, toTable, primary, foreign) {
|
||||||
|
let fromType, toType
|
||||||
|
if (primary && foreign) {
|
||||||
|
fromType = fromTable?.schema[primary]?.type
|
||||||
|
toType = toTable?.schema[foreign]?.type
|
||||||
|
}
|
||||||
|
return fromType && toType && fromType !== toType ? typeMismatch : null
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue