Skip to content
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

Merged
merged 43 commits into from
Jan 9, 2025

Conversation

ValJed
Copy link
Contributor

@ValJed ValJed commented Nov 26, 2024

PRO-6528

Summary

  • Adds control over when fields are ready and can be validated, by handling a new fieldReady property for each field.
    Must work with async fields like dynamic choices, being able to set a field not ready during fetching and ready when operation is finished.
  • Reworks schema level convert method. Check errors only when on root convert method to have access to the finale destination object to be able to check non visible fields, as well as non visible parents. No more order problem..
  • Adds aposPath to fields during validation process.
  • Adds tests.

What are the specific steps to test this change?

Cypress tests 🟢

What kind of change does this PR introduce?

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Build-related changes
  • Other

Make sure the PR fulfills these requirements:

  • It includes a) the existing issue ID being resolved, b) a convincing reason for adding this feature, or c) a clear description of the bug it resolves
  • The changelog is updated
  • Related documentation has been updated
  • Related tests have been updated

If adding a new feature without an already open issue, it's best to open a feature request issue first and wait for approval before working on it.

Other information:

@ValJed ValJed self-assigned this Nov 26, 2024
@ValJed ValJed marked this pull request as draft November 26, 2024 13:31
Copy link
Member

@boutell boutell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like where you're going with this.

modules/@apostrophecms/schema/index.js Outdated Show resolved Hide resolved
modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js Outdated Show resolved Hide resolved
@ValJed ValJed force-pushed the pro-6799-design-default-values-invisible-fields branch 2 times, most recently from 6de153c to 9bbcd21 Compare November 26, 2024 17:22
const errorsList = [];
const isCurrentVisible = isParentVisible === false
? false
: await self.isVisible(req, schema, data, field.name);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notable difference here (previous versions might be buggy). We pass data instead of destination because destination is not fully built at this point.
I think isVisible should compare data as they have been set by the user and not the potential default values.
Tests are green with it but you might have some reasons to not allow this.
Let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data can be unvalidated stuff of unexpected types etc., it could be anything. Trusting it is problematic. You might get away with that in isVisible because you're not saving the values, but then isVisible must be written carefully to not assume the values are at all well-formed, for instance it must not assume string fields are actually strings yet, etc. because nothing has been validated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data might contains obsolete values.
You can also have preventFieldsOnly set to true meaning destination will only contains partial fields.
We should think about these and if it has any impact on your implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It now uses destination when eveything as been converted.

@ValJed ValJed marked this pull request as ready for review November 27, 2024 13:17
@ValJed ValJed requested review from boutell and haroun November 27, 2024 13:17
handler() {
this.updateNextAndEmit();
}
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need a deep watch here, we can simply call the method when v-model updates fieldState, see above change.

@@ -52,6 +52,7 @@
:server-error="fields[field.name].serverError"
:doc-id="docId"
:generation="generation"
@update:model-value="updateNextAndEmit"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, no more need for a deep watcher.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not on the compare meta version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep the behavior consistent, fieldState was watched before, I just replaced the watch by this. compareMetaState wasn't.

} else {
this.next.data[field.name] = this.modelValue.data[field.name];
}
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only indentation in this block

Copy link
Member

@boutell boutell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible that changes are not necessary but see my note here about trusting data, isVisible must be written very defensively if it is going to see unvalidated user input.

const errorsList = [];
const isCurrentVisible = isParentVisible === false
? false
: await self.isVisible(req, schema, data, field.name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data can be unvalidated stuff of unexpected types etc., it could be anything. Trusting it is problematic. You might get away with that in isVisible because you're not saving the values, but then isVisible must be written carefully to not assume the values are at all well-formed, for instance it must not assume string fields are actually strings yet, etc. because nothing has been validated.

@@ -52,6 +52,7 @@
:server-error="fields[field.name].serverError"
:doc-id="docId"
:generation="generation"
@update:model-value="updateNextAndEmit"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not on the compare meta version?

const errorsList = [];
const isCurrentVisible = isParentVisible === false
? false
: await self.isVisible(req, schema, data, field.name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data might contains obsolete values.
You can also have preventFieldsOnly set to true meaning destination will only contains partial fields.
We should think about these and if it has any impact on your implementation.

@ValJed ValJed force-pushed the pro-6799-design-default-values-invisible-fields branch 5 times, most recently from 52e6290 to 2341d53 Compare December 10, 2024 09:02
@ValJed ValJed requested review from boutell and haroun December 10, 2024 09:20
@ValJed
Copy link
Contributor Author

ValJed commented Dec 10, 2024

@boutell @haroun Code could be cleaned but I would like your opinion on the general direction.

}

if (typeof error !== 'string') {
self.apos.util.error(error.path + '\n' + error.stack);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me to show what field failed, error path being the name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to log this detail later when logging the overall error. But I wouldn't die on that hill.

Will the error type ever be a string? I believe that went away in A3/A4...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's smart to check.

Copy link
Contributor Author

@ValJed ValJed Dec 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk, this was existing code, I removed it and moved the logs out of handleErrors function.
Also created a ticket to improve logs in convert.

Copy link
Member

@boutell boutell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the general strategy. Still think it makes more sense to set aposPath on each field just once in schema.validate and its related functions.

CHANGELOG.md Outdated
@@ -7,6 +7,8 @@
* Focus properly Widget Editor modals when opened. Keep the previous active focus on the modal when closing the widget editor.
* a11y improvements for context menus.
* Fixes broken widget preview URL when the image is overridden (module improve) and external build module is registered.
* In th schema convert method, we wait all sub convert to run, to have access to the final destination object in order to check if
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

} = {}
) {
const options = {
fetchRelationships,
ancestors,
isParentVisible
ancestorSchemas,
ancestorPath
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to compute the aposPath of a field once, in the validate method of schema and its related functions, rather than pushing this boulder up the hill on each convert call 🪨

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes good point. We agree that we are talking about the schema path and not the actual destination one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, it's now computed during validate.

}

if (typeof error !== 'string') {
self.apos.util.error(error.path + '\n' + error.stack);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to log this detail later when logging the overall error. But I wouldn't die on that hill.

Will the error type ever be a string? I believe that went away in A3/A4...

}

if (typeof error !== 'string') {
self.apos.util.error(error.path + '\n' + error.stack);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's smart to check.

@ValJed ValJed force-pushed the pro-6799-design-default-values-invisible-fields branch from a4969e5 to 2601619 Compare December 30, 2024 13:02
destination,
conditionalFields)
)
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just linting

modules/@apostrophecms/schema/index.js Show resolved Hide resolved

for (const error of validErrors) {
self.apos.util.error(error.stack);
}
Copy link
Contributor Author

@ValJed ValJed Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved log of errors out of handleErrors method.
Created a ticket to improve globally message printed from convert errors.
Also, log stack only because it contains the error name and message also.

// 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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this method is really useful.
I wrote a test for it and realized I saw it work only for a string that has a pattern.
In the case of in integer having a min or a max for example, the convert updates the value itself, it does not need this method.
Maybe there are some cases I didn't test.

Copy link
Member

Choose a reason for hiding this comment

The 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 def anyway? It should.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, maybe it does.
My question was more about in what cases this is useful except for invisible string fields having a pattern that is not respected in the existing data.
I'm asking you because this function simply wraps existing code from you.
Also because you evoke min and max in the comments but I realized the convert already fixed min and max doe invisible fields.

      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.

// 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);
Copy link
Contributor Author

@ValJed ValJed Dec 30, 2024

Choose a reason for hiding this comment

The 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 pattern property.

field.if,
destination,
conditionalFields
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just linting

}
});
},

// Validates a single schema field. See `validate`.
validateField(field, options, parent = null) {
field.aposPath = parent
? `${parent.aposPath}/${field.name}`
: field.name;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build aposPath by using parent aposPath

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relationship field uses validateu an not validateField like does array and object fields.
Maybe the three should use the validate method since we can now pass the parent?

...(destination[field.name]
?.find?.(doc => doc._id === item._id)
?._fields || {})
...destItem?._fields || {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just make code more readable.

@@ -1218,7 +1219,7 @@ module.exports = (self) => {
self.validate(_field.schema, {
type: 'relationship',
subtype: _field.withType
});
}, _field);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing the parent to validate. Should we modify object and array to use the same validate method instead of validateField. (the validate method keeps tracks of already validated fields). I think it would make sense to use the same method for relationship, object and array.

@ValJed ValJed requested a review from boutell December 30, 2024 17:28
@ValJed ValJed force-pushed the pro-6799-design-default-values-invisible-fields branch from e3458a3 to 1508b0a Compare December 30, 2024 17:33
CHANGELOG.md Outdated
@@ -5,6 +5,12 @@
### Fixes

* Fixes a bug where images in Media manager are not selectable (click on an image does nothing) in both default and relationship mode.
* In the schema convert method, we wait all sub convert to run, to have access to the final destination object. In order to check if
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about: "eliminated superfluous error messages. The convert method now waits for all recursive invocations to complete before attempting to determine if fields are visible."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

CHANGELOG.md Outdated

### Adds

* Possibility to set a field not ready when performing async operations, when a field isn't ready, the validation and emit won't occur.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fields can now be marked as "not ready" when performing async operations. When a field isn't ready, the validation and emit won't occur.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

modules/@apostrophecms/schema/index.js Show resolved Hide resolved
// 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);
Copy link
Member

Choose a reason for hiding this comment

The 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 def anyway? It should.

@ValJed ValJed requested a review from boutell January 7, 2025 13:36
@ValJed ValJed requested review from haroun and boutell and removed request for haroun January 7, 2025 14:13
@ValJed ValJed merged commit 2dc106c into main Jan 9, 2025
9 checks passed
@ValJed ValJed deleted the pro-6799-design-default-values-invisible-fields branch January 9, 2025 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants