-
Notifications
You must be signed in to change notification settings - Fork 116
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
NEW Validation of fields when inline saving #1150
NEW Validation of fields when inline saving #1150
Conversation
577ca62
to
3f8cb66
Compare
9ea8753
to
bb23f17
Compare
bb23f17
to
4837d2b
Compare
22aab0d
to
9bb7edb
Compare
048c625
to
a4ffd80
Compare
e387931
to
4419ecb
Compare
I think I need a second opinion on this before I okay it - it feels like the flow is too convoluted and will cause trouble for us down the line, which I've mentioned in more detail in comments above. |
In terms of refactoring to make this less convoluted, as mentioned earlier the bottle neck is really the use of the GraphQL HOC on the publish button. So in order to refactor I can think of a couple of options: a) have way to use apollo/graphql so you can do an "inline" query rather than as a HOC (I don't know how to do this) - then we could use the mutation from Element.js |
That sounds preferable to me. |
Publishing has nothing to do with formschema. This would just be method on a controller that calls I did a POC a while back of using regular controllers in elemental so I'd largely just copy that |
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.
To the extent that all the other actions on the block (archive, duplicate, unpublish, etc) are still going through GraphQL, I would question the wisdom of moving publication off GraphQL.
I do think the way we write GraphQL request is dumb and unduly complicated. I would be in favour of coming up with a better standard for creating those GraphQL requests.
I think the terraformers have done some customisation to an important project in that space. Might be worth validating with them if that's going to bit them in the ass.
import * as toastsActions from 'state/toasts/ToastsActions'; | ||
import { addFormChanged, removeFormChanged } from 'state/unsavedForms/UnsavedFormsActions'; | ||
|
||
export const ElementContext = createContext(null); |
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.
This probably should be in its own file.
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.
Will be addressed in follow up card #1164
@@ -248,6 +423,19 @@ class Element extends Component { | |||
this.getVersionedStateClassName() | |||
); | |||
|
|||
// eslint-disable-next-line react/jsx-no-constructed-context-values | |||
const providerValue = { |
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.
I think this is abusing the useContext
hook. Context normally passes a simple value down the virtual dom. This is passing down values and a bunch of handlers to update things.
Seems like the component is already heavily using redux anyway, so layering context on top of it seems to be adding complexity.
If we're not going to put this in redux, then we should minimally be extremely clear about what each entry will is doing by putting the context in a dedicated file with clear documentation of what each key is doing.
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.
Will be addressed in follow up card #1164
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.
We're already rewriting most of the component ... seems like this would have been a good opportunity to refactor has a functional component.
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.
Considering how long we've been working on this I think that should be explicitly out of scope.
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.
Will be addressed in follow up card #1164
} | ||
} | ||
|
||
componentDidUpdate() { |
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.
Triggering HTTP requests by updating the state and picking up the state update in componentDidUpdate
seems incredibly weird and questionable.
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.
It's the only way I could work out how to do this.
Essentially we need to ensure that several dependent occurred in a sequence sending a publish request, e.g. when publishing
- The form has rendered before sending a request so that validation can occur - it won't have rendered if the element has been toggled open
- If the form is dirty, save the form first to trigger validation. If it's valid then publish.
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.
Have we tried using Promises?
The way I would approach this is:
- Define a
waitForFormRender
method. The method would return a Promise.
- If the form as already rendered, then
Promise.resolve
is return. - If the form as not resolved a new promise is returned that will be resolved when
handleFormInit
is called.
- Update
handlePublishButtonClick
to callwaitForFormRender
and trigger whatever request is needed when the promise resolves.
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.
Will be addressed in follow up card #1164
|
||
// Render the form off the screen so that it's not visible | ||
// Note that using display:none will mean the form is not rendered at all and cannot be submitted | ||
&--rendered-not-visible { |
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.
Why are we rendering the form off the screen?
Won't this cause accessibility problems?
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.
There's an aria-hidden
(or whatever the actual thing is) to avoid this causing problems for screen-reader users.
It needs to be rendered so the form can submit with the correct values. Otherwise, the formschema form submission has no values to submit.
It's an unfortunate quirk of using formschema in an element that can be collapsed.
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.
That's good context. This seems like a very important point that should be document in the component using this class rather than the style sheet.
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.
Have put a note about this in Content.js which is where this css class is added
@@ -132,6 +46,19 @@ const PublishAction = (MenuComponent) => (props) => { | |||
toggle: props.toggle, | |||
}; | |||
|
|||
useEffect(() => { | |||
if (formHasRendered && doPublishElement) { |
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.
We are triggering an action based on a state update we are getting from somewhere else. This seems very bad.
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.
Will be addressed in follow up card #1164
refetchElementalArea() { | ||
// This will trigger a graphql readOneElementalArea request that will cause this | ||
// element to re-render including any updated title and versioned badge | ||
window.ss.apolloClient.queryManager.reFetchObservableQueries(); |
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.
Surely there's a better way to refresh a single query than force every other query to re-run?
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.
Will be addressed in follow up card #1164
☝️
👍 |
Based on Max's reponse it seems like the plan is basically
Like has Guy has already replied, I'm not a fan of "refactor this to become a functional component" alongside an existing complex PR because that increase in scope makes things FAR harder to peer review. However to achieve 1) it's a requirement because apollo now uses hooks - https://www.apollographql.com/docs/react/data/mutations The existing apollo hoc method is deprecated https://www.apollographql.com/docs/react/api/react/hoc/ Given that this PR has been open and worked on for an extremely long time, can I propose that we just merge this as is and then open a new card to refactor this to a functional component and implement 1-3? |
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.
Given that this PR has been open and worked on for an extremely long time, can I propose that we just merge this as is and then open a new card to refactor this to a functional component and implement 1-3?
I would be keen to see the timeout, weird state management logic and the ElementContext solve in this card.
Given that GraphQL bits require coming up with new and improve way of doing GraphQL queries, I'm fine with putting that in a different card.
|
||
// Render the form off the screen so that it's not visible | ||
// Note that using display:none will mean the form is not rendered at all and cannot be submitted | ||
&--rendered-not-visible { |
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.
That's good context. This seems like a very important point that should be document in the component using this class rather than the style sheet.
} | ||
} | ||
|
||
componentDidUpdate() { |
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.
Have we tried using Promises?
The way I would approach this is:
- Define a
waitForFormRender
method. The method would return a Promise.
- If the form as already rendered, then
Promise.resolve
is return. - If the form as not resolved a new promise is returned that will be resolved when
handleFormInit
is called.
- Update
handlePublishButtonClick
to callwaitForFormRender
and trigger whatever request is needed when the promise resolves.
// time under certain conditions, specifically during a behat test when trying to publish a closed | ||
// block when presumably the apollo cache is empty (or something like that). This happens late and | ||
// there are no hooks/callbacks available after this happens the input onchange handlers are fired | ||
setTimeout(() => { |
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.
If this is waiting for a HTTP request to finish, it's entirely possible it will take more than .5 sec to complete.
If we know what action in redux form is triggering this behaviour, we could add our own logic in our reducer to handle it and trigger our own follow up action.
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.
Will be addressed in follow up card #1164
Most of the weird state management logic + ElementContext goes away if we resolve the GraphQL query. Hence why I suggested merging this as is and opening a new card. No point refactoring stuff as part of this peer review only to get rid of it almost straight away. This card has been in peer review for over a month :-/ Are people OK with my suggestion to just merge this as (I believe it works functionally just fine as is) and do any additional refactoring work in a new card? |
I'm really not sure what the value of merging something that introduces a lot of bad construct is if we are going to immediately have a follow up card. I'll just take all my grievances and convert them to ACs on the new card. You'll end up with something like:
|
I've created a follow up card for refactoring and put into refinement #1164 |
b76fda4
to
badbb6b
Compare
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.
I tested the following scenarios:
- Validation for not inline block ✔️
- Validation for errors thrown by the
validate
method on a block when manually saving a block ✔️ - Validation for errors thrown by the
validate
method on a block when saving the page⚠️ - Field level validation (putting a non-numeric value in a numeric field) ❌
Scenario 3 sort of works, but the validation message is attached to the page instead. I take it this is covered by #1155
Scenario 4 doesn't work either on block save or page save. It does work for non-inline-block. Are we splitting this up as well?
Yes 3 is will be covered by #1155 Re 4 - it's likely that the rendered Try using UrlField instead - you should see that validation is working correctly for that. Also for NumericField I do have this setup on my local, which does trigger for values for '1' on inline save: // app/src/MyNumbericField.php
use SilverStripe\Forms\NumericField;
class MyNumbericField extends NumericField
{
private static $extensions = [
MyNumericFieldExtension::class
];
} // app/src/MyNumericFieldExtension.php
use SilverStripe\Core\Extension;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\Validator;
/**
* @extends Extension<NumericField>
*/
class MyNumericFieldExtension extends Extension
{
public function updateValidationResult($result, Validator $validator)
{
if ($this->owner->value == 1) {
$validator->validationError($this->owner->getName(), 'This field cannot be 1');
}
}
} // app/src/MyBlock.php
// ...
class MyBlock extends BaseElement
{
private static $db = [
'MyInt' => 'Int',
];
// ...
public function getCMSFields()
{
// ...
$fields->removeByName('MyInt');
$fields->addFieldToTab('Root.Main', MyNumbericField::create('MyInt', 'My Int'));
}
} |
Still doesn't work for me validation-not-triggering-2024-04-09_17.59.22.webmHere's the sample block I was using <?php
use DNADesign\Elemental\Models\BaseElement;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\UrlField;
class ValidationBlock extends BaseElement
{
private static $db = [
'Integer' => 'Int',
'Url' => 'Varchar(255)'
];
private static $inline_editable = true;
private static $singular_name = 'validation block';
private static $plural_name = 'validation blocks';
private static $icon = 'font-icon-block';
private static $table_name = 'S_EB_ValidiationBlock';
public function getCMSFields()
{
$this->beforeUpdateCMSFields(function (FieldList $fields) {
$fields->addFieldToTab(
'Root.Main',
NumericField::create('Integer'),
UrlField::create('Url')
);
});
return parent::getCMSFields();
}
public function getType()
{
return _t(__CLASS__ . '.BlockType', 'Validation');
}
public function getSummary()
{
return '';
}
public function validate()
{
$result = parent::validate();
// This will add a field specific error to the ValidationResult
if ($this->Integer > 10) {
$result->addFieldError('Integer', 'Integer must be less than 11');
}
return $result;
}
}
class FormValidationBlock extends ValidationBlock
{
private static $inline_editable = false;
private static $singular_name = 'not inline validation block';
private static $plural_name = 'not inline validation blocks';
public function getType()
{
return _t(__CLASS__ . '.BlockType', 'Not inline Validation block');
}
} The problem might be with how I'm using I tried hacking my numeric field to always fail. The page save went through, but the other scenarios got blocked. validation-not-triggering-2024-04-09_18.09.14.webm |
Re numeric field - the inline validation was working correctly in your video. This PR is only for inline validation. It does not cover non-inline dataobject validation / page validation. Those concerns are being handled on #1155. Re the UrlField - that's really weird. I've copied your way of adding UrlField to my local using Perhaps try dumping this into your project, it works for me <?php
use DNADesign\Elemental\Models\BaseElement;
use SilverStripe\Forms\UrlField;
use SilverStripe\Forms\FieldList;
class MrBlock extends BaseElement
{
private static $db = [
'MyUrlOne' => 'Varchar',
'MyUrlTwo' => 'Varchar',
];
private static $table_name = 'MrBlock';
private static $singular_name = 'Mr Block';
private static $plural_name = 'Mr Blocks';
private static $description = 'This is mr block';
private static $icon = 'font-icon-block-content';
public function getType()
{
return 'Mr Block';
}
private static $inline_editable = true;
public function getCMSFields()
{
$this->beforeUpdateCMSFields(function (FieldList $fields) {
$fields->addFieldToTab(
'Root.Main',
UrlField::create('MyUrlTwo', 'My Url Two')
);
});
$fields = parent::getCMSFields();
$fields->removeByName('MyUrlOne');
$fields->addFieldToTab('Root.Main', UrlField::create('MyUrlOne', 'My Url One'));
return $fields;
}
} If that's still not working, please confirm you have the latest hash of this PR as well as this PR |
I'm an idiot.
However, if you try passing another form field to So my UrlField wasn't doing anything and the URL field was being scaffolded as a plain text field. I created an issue to strongly type FieldList in CMS 6 silverstripe/silverstripe-framework#11198 |
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.
65d6bb2
to
1193414
Compare
1193414
to
4789b34
Compare
Merging on Max's behalf |
We've decided to merge as-is with followup work to address my concerns
Issue #329
Requires silverstripe/silverstripe-admin#1685 to be functional
Requires silverstripe/silverstripe-frameworktest#169 for behat to pass
Use the following test block