-
Notifications
You must be signed in to change notification settings - Fork 595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pro 6799 design default values invisible fields #4816
Changes from all commits
f8b197b
288f460
6625d0f
ab34c76
bead68d
893a48e
f726703
a8a9231
e7316ed
11b774e
67f072c
906335d
c030360
2393e96
d9a426f
b7a4a90
477d453
7dc5db5
3497f07
513257f
3096067
cdaa05e
98173a4
ad6f949
5244c98
04eba94
84a532f
ebeba68
b52050d
88938e7
aa8c535
92c0123
2d00a1c
c41f8e4
a27d68d
966ad3c
f77638b
2ef3280
8b14eed
5de55e2
1508b0a
63eef2a
f1f047e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,6 @@ module.exports = { | |
alias: 'schema' | ||
}, | ||
init(self) { | ||
|
||
self.fieldTypes = {}; | ||
self.fieldsById = {}; | ||
self.arrayManagers = {}; | ||
|
@@ -489,7 +488,15 @@ module.exports = { | |
const destinationKey = _.get(destination, key); | ||
|
||
if (key === '$or') { | ||
const results = await Promise.all(val.map(clause => self.evaluateCondition(req, field, clause, destination, conditionalFields))); | ||
const results = await Promise.all( | ||
val.map(clause => self.evaluateCondition( | ||
req, | ||
field, | ||
clause, | ||
destination, | ||
conditionalFields) | ||
) | ||
); | ||
const testResults = _.isPlainObject(results?.[0]) | ||
? results.some(({ value }) => value) | ||
: results.some((value) => value); | ||
|
@@ -585,20 +592,18 @@ module.exports = { | |
{ | ||
fetchRelationships = true, | ||
ancestors = [], | ||
isParentVisible = true | ||
rootConvert = true | ||
} = {} | ||
) { | ||
const options = { | ||
fetchRelationships, | ||
ancestors, | ||
isParentVisible | ||
ancestors | ||
}; | ||
if (Array.isArray(req)) { | ||
throw new Error('convert invoked without a req, do you have one in your context?'); | ||
} | ||
|
||
const errors = []; | ||
|
||
const convertErrors = []; | ||
for (const field of schema) { | ||
if (field.readOnly) { | ||
continue; | ||
|
@@ -611,92 +616,207 @@ module.exports = { | |
} | ||
|
||
const { convert } = self.fieldTypes[field.type]; | ||
if (!convert) { | ||
continue; | ||
} | ||
|
||
if (convert) { | ||
try { | ||
const isAllParentsVisible = isParentVisible === false | ||
? false | ||
: await self.isVisible(req, schema, destination, field.name); | ||
const isRequired = await self.isFieldRequired(req, field, destination); | ||
await convert( | ||
req, | ||
{ | ||
...field, | ||
required: isRequired | ||
}, | ||
data, | ||
destination, | ||
{ | ||
...options, | ||
isParentVisible: isAllParentsVisible | ||
} | ||
); | ||
} catch (error) { | ||
if (Array.isArray(error)) { | ||
const invalid = self.apos.error('invalid', { | ||
errors: error | ||
}); | ||
invalid.path = field.name; | ||
errors.push(invalid); | ||
} else { | ||
error.path = field.name; | ||
errors.push(error); | ||
try { | ||
const isRequired = await self.isFieldRequired(req, field, destination); | ||
await convert( | ||
req, | ||
{ | ||
...field, | ||
required: isRequired | ||
}, | ||
data, | ||
destination, | ||
{ | ||
...options, | ||
rootConvert: false | ||
} | ||
} | ||
); | ||
} catch (err) { | ||
const error = Array.isArray(err) | ||
? self.apos.error('invalid', { errors: err }) | ||
: err; | ||
|
||
error.path = field.name; | ||
error.schemaPath = field.aposPath; | ||
boutell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
convertErrors.push(error); | ||
} | ||
} | ||
|
||
const errorsList = []; | ||
|
||
for (const error of errors) { | ||
if (error.path) { | ||
// `self.isVisible` will only throw for required fields that have | ||
// an external condition containing an unknown module or method: | ||
const isVisible = isParentVisible === false | ||
? false | ||
: await self.isVisible(req, schema, destination, error.path); | ||
|
||
if (!isVisible) { | ||
// It is not reasonable to enforce required, | ||
// min, max or anything else for fields | ||
// hidden via "if" as the user cannot correct it | ||
// and it will not be used. If the user changes | ||
// the conditional field later then they won't | ||
// be able to save until the erroneous field | ||
// is corrected | ||
const name = error.path; | ||
const field = schema.find(field => field.name === name); | ||
if (field) { | ||
// To protect against security issues, an invalid value | ||
// for a field that is not visible should be quietly discarded. | ||
// We only worry about this if the value is not valid, as otherwise | ||
// it's a kindness to save the work so the user can toggle back to it | ||
destination[field.name] = klona((field.def !== undefined) | ||
? field.def | ||
: self.fieldTypes[field.type]?.def); | ||
continue; | ||
} | ||
if (!rootConvert) { | ||
if (convertErrors.length) { | ||
throw convertErrors; | ||
} | ||
|
||
return; | ||
} | ||
|
||
const nonVisibleFields = await self.getNonVisibleFields({ | ||
req, | ||
schema, | ||
destination | ||
}); | ||
|
||
const validErrors = await self.handleConvertErrors({ | ||
req, | ||
schema, | ||
convertErrors, | ||
destination, | ||
nonVisibleFields | ||
}); | ||
|
||
for (const error of validErrors) { | ||
self.apos.util.error(error.stack); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved log of errors out of |
||
|
||
if (validErrors.length) { | ||
throw validErrors; | ||
} | ||
}, | ||
|
||
async getNonVisibleFields({ | ||
req, schema, destination, nonVisibleFields = new Set(), fieldPath = '' | ||
}) { | ||
for (const field of schema) { | ||
const curPath = fieldPath ? `${fieldPath}.${field.name}` : field.name; | ||
const isVisible = await self.isVisible(req, schema, destination, field.name); | ||
if (!isVisible) { | ||
nonVisibleFields.add(curPath); | ||
continue; | ||
} | ||
if (!field.schema) { | ||
continue; | ||
} | ||
|
||
// Relationship does not support conditional fields right now | ||
if ([ 'array' /*, 'relationship' */].includes(field.type) && field.schema) { | ||
for (const arrayItem of destination[field.name] || []) { | ||
await self.getNonVisibleFields({ | ||
req, | ||
schema: field.schema, | ||
destination: arrayItem, | ||
nonVisibleFields, | ||
fieldPath: `${curPath}.${arrayItem._id}` | ||
}); | ||
} | ||
if (isParentVisible === false) { | ||
} else if (field.type === 'object') { | ||
await self.getNonVisibleFields({ | ||
req, | ||
schema: field.schema, | ||
destination: destination[field.name], | ||
nonVisibleFields, | ||
fieldPath: curPath | ||
}); | ||
} | ||
} | ||
|
||
return nonVisibleFields; | ||
}, | ||
|
||
async handleConvertErrors({ | ||
req, | ||
schema, | ||
convertErrors, | ||
nonVisibleFields, | ||
destination, | ||
destinationPath = '', | ||
hiddenAncestors = false | ||
}) { | ||
const validErrors = []; | ||
for (const error of convertErrors) { | ||
const [ destId, destPath ] = error.path.includes('.') | ||
? error.path.split('.') | ||
: [ null, error.path ]; | ||
|
||
const curDestination = destId | ||
? destination.find(({ _id }) => _id === destId) | ||
: destination; | ||
|
||
const errorPath = destinationPath | ||
? `${destinationPath}.${error.path}` | ||
: error.path; | ||
|
||
// Case were this error field hasn't been treated | ||
// Should check if path starts with, because parent can be invisible | ||
const nonVisibleField = hiddenAncestors || nonVisibleFields.has(errorPath); | ||
|
||
// We set default values only on final error fields | ||
if (nonVisibleField && !error.data?.errors) { | ||
const curSchema = self.getFieldLevelSchema(schema, error.schemaPath); | ||
self.setDefaultToInvisibleField(curDestination, curSchema, error.path); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this method is really useful. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the field is not visible and never gets touched in the UI, doesn't the frontend submit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know, maybe it does. setDefaultToInvisibleField(destination, schema, fieldPath) {
// Field path might contain the ID of the object in which it is contained
// We just want the field name here
const [ _id, fieldName ] = fieldPath.includes('.')
? fieldPath.split('.')
: [ null, fieldPath ];
// It is not reasonable to enforce required,
// min, max or anything else for fields
// hidden via "if" as the user cannot correct it
// and it will not be used. If the user changes
// the conditional field later then they won't
// be able to save until the erroneous field
// is corrected
const field = schema.find(field => field.name === fieldName);
if (field) {
// To protect against security issues, an invalid value
// for a field that is not visible should be quietly discarded.
// We only worry about this if the value is not valid, as otherwise
// it's a kindness to save the work so the user can toggle back to it
destination[field.name] = klona((field.def !== undefined)
? field.def
: self.fieldTypes[field.type]?.def);
}
} So I'm just wondering how useful is it, is it worth keeping it etc. Also how to properly test it. |
||
continue; | ||
} | ||
|
||
if (error.data?.errors) { | ||
const subErrors = await self.handleConvertErrors({ | ||
req, | ||
schema, | ||
convertErrors: error.data.errors, | ||
nonVisibleFields, | ||
destination: curDestination[destPath], | ||
destinationPath: errorPath, | ||
hiddenAncestors: nonVisibleField | ||
}); | ||
|
||
// If invalid error has no sub error, this one can be removed | ||
if (!subErrors.length) { | ||
continue; | ||
} | ||
} | ||
|
||
if (!Array.isArray(error) && typeof error !== 'string') { | ||
self.apos.util.error(error + '\n\n' + error.stack); | ||
error.data.errors = subErrors; | ||
} | ||
errorsList.push(error); | ||
validErrors.push(error); | ||
} | ||
|
||
return validErrors; | ||
}, | ||
|
||
setDefaultToInvisibleField(destination, schema, fieldPath) { | ||
// Field path might contain the ID of the object in which it is contained | ||
// We just want the field name here | ||
const [ _id, fieldName ] = fieldPath.includes('.') | ||
? fieldPath.split('.') | ||
: [ null, fieldPath ]; | ||
// It is not reasonable to enforce required, | ||
// min, max or anything else for fields | ||
// hidden via "if" as the user cannot correct it | ||
// and it will not be used. If the user changes | ||
// the conditional field later then they won't | ||
// be able to save until the erroneous field | ||
// is corrected | ||
const field = schema.find(field => field.name === fieldName); | ||
if (field) { | ||
// To protect against security issues, an invalid value | ||
// for a field that is not visible should be quietly discarded. | ||
// We only worry about this if the value is not valid, as otherwise | ||
// it's a kindness to save the work so the user can toggle back to it | ||
destination[field.name] = klona((field.def !== undefined) | ||
? field.def | ||
: self.fieldTypes[field.type]?.def); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As described above, not sure where this method is useful except for strings have a |
||
} | ||
}, | ||
|
||
if (errorsList.length) { | ||
throw errorsList; | ||
getFieldLevelSchema(schema, fieldPath) { | ||
if (!fieldPath || fieldPath === '/') { | ||
return schema; | ||
} | ||
let curSchema = schema; | ||
const parts = fieldPath.split('/'); | ||
parts.pop(); | ||
for (const part of parts) { | ||
const curField = curSchema.find(({ name }) => name === part); | ||
curSchema = curField.schema; | ||
} | ||
|
||
return curSchema; | ||
}, | ||
|
||
// Determine whether the given field is visible | ||
// based on `if` conditions of all fields | ||
|
||
async isVisible(req, schema, object, name) { | ||
async isVisible(req, schema, destination, name) { | ||
const conditionalFields = {}; | ||
const errors = {}; | ||
|
||
|
@@ -705,7 +825,13 @@ module.exports = { | |
for (const field of schema) { | ||
if (field.if) { | ||
try { | ||
const result = await self.evaluateCondition(req, field, field.if, object, conditionalFields); | ||
const result = await self.evaluateCondition( | ||
req, | ||
field, | ||
field.if, | ||
destination, | ||
conditionalFields | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just linting |
||
const previous = conditionalFields[field.name]; | ||
if (previous !== result) { | ||
change = true; | ||
|
@@ -1332,19 +1458,23 @@ module.exports = { | |
// reasonable values for certain properties, such as the `idsStorage` property | ||
// of a `relationship` field, or the `label` property of anything. | ||
|
||
validate(schema, options) { | ||
validate(schema, options, parent = null) { | ||
schema.forEach(field => { | ||
// Infinite recursion prevention | ||
const key = `${options.type}:${options.subtype}.${field.name}`; | ||
if (!self.validatedSchemas[key]) { | ||
self.validatedSchemas[key] = true; | ||
self.validateField(field, options); | ||
self.validateField(field, options, parent); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
}); | ||
}, | ||
|
||
// Validates a single schema field. See `validate`. | ||
validateField(field, options, parent = null) { | ||
field.aposPath = parent | ||
? `${parent.aposPath}/${field.name}` | ||
: field.name; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Build |
||
|
||
const fieldType = self.fieldTypes[field.type]; | ||
if (!fieldType) { | ||
fail('Unknown schema field type.'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just linting