At present, a plugin throws an exception in setParent() if container is incompatible, as following the example in RulesPlugin::setParent(). This can be triggered when plugins that can only be embedded in subclasses of, for example, RulesActionContainer are dragged outside that sub-container and the form is submitted. The trigger is in RulesContainerPluginUI::form_extract_values().

It would be good for the UI to catch the exception before moving the element vertically in the hierarchy such that the fatal error is not triggered.

UPDATE:
The issue now tries to extend the draggable table to prevent elements from nesting inside incompatible containers. Tests are in #21.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

zhangtaihao’s picture

Title: Catch exception in container UI when applying hierarchy » Catch exception in when changing element hierarchy

This would also happen when trying to delete an element.

zhangtaihao’s picture

Title: Catch exception in when changing element hierarchy » Catch exception when changing element hierarchy
Status: Active » Needs review
FileSize
1.9 KB

Corrected grammatical error in title.

Here's the patch.

mitchell’s picture

Status: Needs review » Needs work

Without the patch, if I try to nest an action in a conditional, I am forced to the site error page, and the form submission has no effect. With the patch, the form submission goes through, and I get an error within the Rules page, however the big difference is that the action gets removed. This doesn't seem like the behavior you would have intended.

As an alternative to this patch, would it be possible to prevent these element moves in the first place using the same mechanism Rules uses to stop you from nesting a loop in an action?

zhangtaihao’s picture

Hmm... it's not happening for me. Weird. EDIT: I've tried it with both the testing and standard profiles.

I presume you've applied the patch against Rules 7.x-2.1+7-dev?

zhangtaihao’s picture

Ideas on how to go about debugging that issue?

mitchell’s picture

I tried it again with a clean Rules git checkout, and the same thing happened. I don't know how to debug that. Is it necessary to use Drupal-dev over 7.14?

What do you think about my question in #3?

zhangtaihao’s picture

Ooh, right, sorry I got caught up in the problem.

I'll have a look at it when I get to work.

EDIT: Actually, I already have the answer. The tabledrag is built simply by specifying whether an element is leaf. There is really no robust way to dynamically configure which row can be nested under which other, specifically because Javascript won't care about the PHP class hierarchy.

However, there may be a way to get around the problem. I think the trick is for each type of embeddable plugin (UI) to declare its "classes" and possible parent "classes" in CSS form for tabledrag to understand. Then, theme_rules_elements() could add the extra relationships with drupal_add_tabledrag(). The existing plugins and default UI implementations could then reproduce the current effects, plus allowing for plugins like conditionals and branches to specify custom classes so as to restrict the "draggability" to particular types of sub-containers.

The patch above still needs fixing, though, just to ensure it works in the backend. EDIT: Or, the "delete" link could be removed for container plugins whose children can't be embedded in the new parent.

zhangtaihao’s picture

Title: Catch exception when changing element hierarchy » Keep embedded elements under compatible containers in container UI
FileSize
7.82 KB

To follow up on #3, I am working on a patch based on the idea behind #7. I've posted a WIP patch.

In essence, this has become a UX issue.

zhangtaihao’s picture

Before I post my patch, could you check if an element can somehow be dragged below the operations row?

This almost seems like a bug in core. Perhaps it has already been fixed.

zhangtaihao’s picture

Turns out to be much more complicated than I first thought. I had to write a behavior to override tabledrag.js and prevent drag if the target container is incompatible.

Also, this patch adds two methods to RulesPluginUIInterface.

I'm uploading this before I work on a less radical, but sort of shoddy idea.

EDIT: Please see #13 for a successful upload.

zhangtaihao’s picture

Sorry about the commented "console.log()" lines littered in the js. Here's a clean one.

EDIT: Please skip this comment.

zhangtaihao’s picture

Okay, last try. This time I've searched for "console.log()".

EDIT: This is insane. Why isn't the the js file included?

Again, please ignore this one.

zhangtaihao’s picture

Finally got it right.

zhangtaihao’s picture

As promised, here is the alternative approach. The differences here (compared to #13) include:

  • Each constraint is for any matched element to any matched container.
  • Compatibility check leverages plugin info 'embeddable' class name.
  • Compatibility between elements is computed beforehand. This is somewhat optimized by grouping the computation by class name (which should reduce most of the computing load for actions).
  • No change to RulesPluginUIInterface is necessary.

Basically, this approach might become expensive if there are lots of plugins with lots of different types added to a rule (i.e. minimal intersection and large number of checks). However, once we get into that scenario, this probably wouldn't be the most expensive operation.

zhangtaihao’s picture

Status: Needs work » Needs review
FileSize
11.59 KB

I forgot in #14 to dispose of row wrapper object onDrop() (in Javascript).

zhangtaihao’s picture

Fix to #15 to remove the 'delete' link if child elements are not compatible with the parent of the element to be deleted.

mitchell’s picture

Status: Needs review » Needs work

> could you check if an element can somehow be dragged below the operations row?
Yea, I noticed this for the first time when testing #2. Can't believe it hasn't come up before, but it's probably more so apparent to testers than users. Luckily if you save the settings like that, it doesn't stick. Still happens with #15. (I did a cursory search of core and nothing popped out as related. It's probably best you create the issue for this bug in 7.x.)

#15: Works. I like how a non-nested action will move all the way into a case rather than making a stop in the switch, and it doesn't wobble or becoming unstable as it passes over the switch. Very clean. Also, the tree lines still work.

There's an odd behavior moving something from top down. It will not drag to a spot below it until it reaches the bottom (below the ops), then it'll move up as expected.

Moving things between cases can be a little difficult. If an action is in a case above another case and I try to move it below, then it seems to move only some of the time. I had to move it all the way to the bottom and back up. Moving things from a case below to the one above will not have effect until it reaches one above the bottom, so for me it would jump above the bottom action even though I was trying to just move it from the top of the second case to the bottom of the first.

Also, at one point, I was able to move an action beneath a conditional when coming from the bottom, but it seemed to be nested further, beneath a non-existent parent.

@zhangtaihao: Could you please make a list of behaviors to be tested for measuring the overall needs of the patch to be committed? I understand the overall goals, but it would be easier for other testers and myself to sign-off on the patch's stability if we're checking off a list of expected behaviors.

zhangtaihao’s picture

I am compiling a testing suite now.

I am unable to reproduce the last issue you mentioned, i.e. moving an action beneath a conditional when coming from the bottom. Though it seems unlikely and rather silly for me to ask, are you sure it wasn't just the "if" statement above the action wrapping to the next line, making the action seem like it's directly under the conditional?

EDIT: Ooh, I've just found it. Will include in test suite.

zhangtaihao’s picture

Before I do anything else, I just noticed I was using class to optimize when I should be using plugin name.

Updated patch.

zhangtaihao’s picture

As an aside, one would almost think the actions were in a <tfoot> (but then, of course, #806982: Tables should take an optional footer variable and produce <tfoot> hasn't been resolved).

zhangtaihao’s picture

Status: Needs review » Needs work
FileSize
822 bytes

The following is a table of tests. Note that "Component" refers to the Rules component to test, imported from the uploaded file.

Each test is meant to be performed after refreshing.

Test Component Operation Expected result
1 dragtest nested Drag "Add a variable" and drop above "(Else) If". "Add a variable" lands above "(Else) If", embedded under "If".
2 dragtest nested Perform 1, then drag "Add a variable" and drop above "Conditional". "Add a variable" lands above "Conditional", without stopping between it and "If".
3 dragtest nested Drag "Add a variable" horizontally to the left edge of table. "Add a variable" lands on the same indentation level as "Conditional", without stopping at the level of "If".
4 dragtest nested Perform 1, then drag "Add a variable" horizontally to the left. "Add a variable" cannot be moved horizontally.
5 dragtest nested Perform 2, then drag "Add a variable" and drop below "If". "Add a variable" lands below "If", without stopping
between it and "Conditional".
6 dragtest nested Drag "If" and drop below "Add a variable". "If" lands below "Add a variable", without stopping between it and "(Else) If".
7 dragtest nested Drag "If" horizontally. "If" cannot be moved horizontally
8 dragtest nested Drag "Add a variable" and drop as embedded under "Loop". "Add a variable" lands beneath "Loop" embedded.
9 dragtest nested Perform 8, then drag "Add a variable" above "Loop" while maintaining the same indentation level as "(Else) If". "Add a variable" lands above "Loop" not directly embedded beneath "Conditional".
10 dragtest nested Drag "Loop" and drop above "Add a variable". "Loop" lands beneath "(Else) If" embedded.
11 dragtest nested Drag "Loop" beneath "(Else) If" embedded, and then while still dragging move the cursor to the bottom left of the "Loop" row. "Loop" stays embedded under "(Else) If".

Change log

  • Updated 9 to clarify the intention of preventing embedding in incompatible container from below.
  • Replaced "Else if" with "(Else) If" for the new labelling in Conditional Rules.
zhangtaihao’s picture

At present, most of the above tests are failing spectacularly (hence they are there, though some are quite funny).

Of course, if the table should include any more tests, let me know and I'll add it to the table at the end.

zhangtaihao’s picture

It seems like a row needs to be further indented while swapping to fit the necessary container.

Also, in the check for downward move, I've neglected to check for a tree group.

I'll fix up the patch in the morning and see how many tests the fix will affect.

zhangtaihao’s picture

Issue summary: View changes

Added actual proposal.

zhangtaihao’s picture

I just realized that I was overriding the wrong method in tableDrag.row for preventing swap. I have now correctly overridden swap() itself to swap only if a move is legal.

On another note, the tabledrag.js in Drupal works just fine. It's supposed to be swappable with non-draggable rows, in order to account for grouped rows. So, isValidSwap() is still overridden, but now with an additional check for the "rules-elements-add" class (i.e. operations row).

zhangtaihao’s picture

A tiny change to line 152 of rules.tabledrag.js:

-      var next = $(row).next('tr');
+      var next = $(row).next('tr.draggable');

So now a row can at least be dragged down to just above the operations row. This hopefully eases testing a bit.

Uploaded another WIP patch.

zhangtaihao’s picture

zhangtaihao’s picture

Status: Needs work » Needs review
FileSize
16.91 KB

I've rewritten the Javascript to use a cleaner approach. This patch (based on #24) only minimally hijacks the core tabledrag.js behavior.

Summary of overrides:

  • Treats tr.rules-elements-add as an invalid swap in tableDrag.row.isValidSwap().
  • Trims the indent interval from tableDrag.row.validIndentInterval() to further remove incompatible containers from interval boundaries. When the interval becomes invalid (i.e. interval has negative width), the original tableDrag.findDropTargetRow() will treat the swap as invalid and abort.
  • Indents a row to be embedded in the nearest compatible container when indenting a row with non-empty interval in tableDrag.row.indent().
  • Change detection in overrides for the following to optimize (expensive) compatibility lookups:
    • tableDrag.findDropTargetRow()
    • tableDrag.row.validIndentInterval()
    • tableDrag.row.indent()

All of the tests in #21 pass. This is the first patch that works reliably.

zhangtaihao’s picture

Ach! Comment error on line 57 of rules.tabledrag.js. Instead of saying:

   * Overrides tableDrag.row.findDropTargetRow() to track row change.

it should say:

   * Overrides tableDrag.findDropTargetRow() to track row change.

Will re-roll patch once the above has been tested.

zhangtaihao’s picture

Quick experiment for usability. This change highlights embeddable containers when the user drags a row (i.e. presses the holds the primary mouse button).

zhangtaihao’s picture

Note that for some reason I had to go through #1671344-11: Setting parent of plugin to incompatible container removes its current parent so that PHP doesn't choke on recursive dependency.

zhangtaihao’s picture

One more go at usability enhancement. This time, the active row does not have the green highlight, and the group being dragged (i.e. children) have only pale highlights to show that although those are compatible containers they are to be ignored since a row cannot be moved into its children.

mitchell’s picture

I am in awe of this, zhang! This green highlighter is terrific! The tests are pretty much all right. Amazing work!

Tested with #31, #1671344-11: Setting parent of plugin to incompatible container removes its current parent, #1672444-1: carify the UI-only use of the embeddable key:

  1. fixed
  2. good
  3. well, can't go directly left - but diagonal and left is good.
  4. works
  5. same as 4? good.
  6. I don't understand. no conditional?!
  7. good
  8. yup
  9. wow! that was cool. I just dragged an entire, previously-configured nest under a new loop. amazing!
  10. works. I was surprised.
  11. I don't think that's working as you expect, but it makes sense to me. I drag it inside, then take it out within a single motion, right?
zhangtaihao’s picture

Status: Needs work » Needs review

Thanks!

Some of my original tests don't make sense now that I've removed many of the original hacks.

5. Yes, it's the same as 4, just the other direction for completeness.
6. This is to ensure "If" isn't embedded in "(Else) If".
11. What originally happened was the loop outdented against the left (no indent). I suspect it had such a different treatment between swapping and indenting rows that the swap failed because it incorrectly detected an invalid swap, and yet the indent treated it as valid and removed the indent on the old row, orphaning "Add a variable".

So, what I've basically done is left Drupal.tableDrag to do what it was supposed to (e.g. prevent orphaning elements, move entire groups), and then added a compatibility matrix between rows.

zhangtaihao’s picture

This new patch disables the "delete" link, instead of removing it. The disabled link displays a tooltip that explains why the element cannot be deleted.

zhangtaihao’s picture

I have released Conditional Rules 7.x-1.0-beta1. Everything is working as expected.

Hopefully this makes it easier to test the above patch.

zhangtaihao’s picture

mitchell: Would you be able to RTBC #34?

zhangtaihao’s picture

Status: Needs review » Needs work

Okay, found a bug in #34: non-recursive container UI also disables the "delete" link if the child container element is not empty.

For example, a rule containing actions inside a rule set cannot be deleted with #34.

zhangtaihao’s picture

It seems that whether deleting an element involves deleting the children is a consequence of the default argument value of the RulesContainerPlugin::delete(). Specifically, the default container plugin elects to keep children if possible, whereas Rule::delete() elects to delete children by default.

Obviously, the default argument value is what we're dealing with, because rules_ui_delete_element() invokes delete() with no arguments. So, the various container plugin UI's will have to account for the default behaviors of their respective plugins' deletion process.

fago: do you agree with this approach? I will post another patch to demonstrate what I'm talking about.

zhangtaihao’s picture

Status: Needs work » Needs review
FileSize
20.37 KB

I've cleaned up #34 by moving all the fancy tabledrag stuff to its own method in RulesContainerPluginUI.

I've also tentatively changed the signature of RulesContainerPluginUI::operations() to add an optional argument for specifying $options for constructing operations links. Specifically, it checks 'delete_children' to determine whether to bypass disabling the delete link. Meanwhile, RulesRuleUI indicates 'delete_children' as TRUE.

zhangtaihao’s picture

Status: Needs review » Needs work

Okay this wouldn't work either. Components declared as actions rely on RulesPluginUIInterface::operations() to modify links. The extra argument basically changed the function signature such that rules_element_invoke_component_operations() no longer gets the right arguments.

zhangtaihao’s picture

Status: Needs work » Needs review
FileSize
19.97 KB

Slight compromise: skip RulesContainerPluginUI in RulesRuleUI::operations() by directly invoking RulesPluginUI::operations() since it is only supposed to show "edit" and "delete" anyway.

klausi’s picture

Quite a lot of JS code here to maintain ... is that really worth it? I mean is a form error upon submission not enough? Why can't we just catch the exception and point the user to the misconfiguration?

zhangtaihao’s picture

Well, I've tried that initially (see #2), and the difficulty comes in sizeable rules (15+ actions). When just catching exceptions, some parents are correctly set, while incompatible ones haven't, so the resulting error wouldn't actually help the user that much in the error just made because the re-ordering simply doesn't work consistently, and there are usually few clues as to what's been reset and what hasn't.

This is also inconsistent with the usual integrity check UX because with integrity errors the element is in the right place, i.e. it's as designed in Rules Core, but it wouldn't correctly evaluate, i.e. Rules Engine won't like it. When trying to place an element under an incompatible container, Rules Core will panic. Catching the exception only removes fatal errors, which only happens because it wasn't even supposed to have happened.

Calling setParent() on an incompatible container throws an evaluation exception. Not sure if that was the intended effect, but it suggests it's a more fundamental problem than an integrity error. Integrity errors tell the user that something was misconfigured, e.g. settings, variable names, etc. Evaluation exceptions thrown when placing an element under an incompatible container indicate broken states (contrary to original design). For example, with Conditional Rules, you wouldn't want to have an "If" under a "Switch".

Plus, much of the JS code exists only because the drag validity checks in tabledrag.js don't quite work the way I thought they would (e.g. it'd be nice if it had some basic targeting checks for parent and sibling classes). Not that I'm hacking, just extending it for Rules.

mitchell’s picture

#42: That's exactly the approach taken before I suggested this approach, so I'll try to defend this change:

The JS isn't actually all that's going on here; though, it's ~2/3 of the patch. Some of the developments in this issue, AFACT, are yet to be broken out into the [#12345: Add Predicates UI] (as a related or sub-issue of #1698296: [Meta] Merge Conditional Rules). zhangtaihao and I are still organizing these references, and I think without these, this change appears as only a radical, yet so far unexplained attempt to improve the tabledrag implementation, but it goes a lot deeper. Predicates is the underlying basis for this and it's going to be a considerable change. I could try to link to a few of zhangtaihao's comments, but I think it'll be easier to hold off and get these compiled into the Predicates and Predicates UI issues.

zhangtaihao’s picture

Okay, I now realize that, on the topic of deleting an element, I've been a massive idiot.

I've updated Conditional Rules to follow the example in Rule and delete all elements under a container whose parent cannot contain the children of the container to be deleted.

I've removed any alteration to the "delete" link and re-rolled the patch against 7.x-2.x.

EDIT: Patch (didn't get uploaded and) is in the next comment.

zhangtaihao’s picture

jpstrikesback’s picture

Status: Needs review » Needs work

Any known reason these tests aren't firing?

jpstrikesback’s picture

Status: Needs work » Needs review
OnkelTem’s picture

This is strange really

OnkelTem’s picture

Trying to reattach the patch from #46

mitchell’s picture

Assigned: Unassigned » fago
Priority: Normal » Major

So to summarize, this issue is separate from Conditional Rules and #1698296: [Meta] Merge Conditional Rules, but both sets of code have a lot of positive feedback. #46 is well tested and it makes the Rules UI more intelligent. When combined with Conditional Rules, the added features enable improve the UI for the more complex element trees.

Raising priority and assigning to fago for much needed feedback or, hopefully, a quick commit.
---

There's also a documentation issue in #1876716: How can I prevent subsequent actions from running?. I think once we resolve this issue, we should update the project page with the details about this added functionality, and we can probably close the Merge.. issue.

The tests issue is #1540266: Fix broken simpletests.

fago’s picture

Status: Needs review » Needs work

I just ran into this issue - sry for not commenting on it earlier :-(

As klausi above, I'm not in favour of replicating so much JS as I don't want to maintain a improved tabledrag version. Actually, I'm not so happy about the tabledrag any more, but that's it for d7 now - maybe we can do better for d8.

However, I see where this is problematic for you. But couldn't you just solve it from your module and replace the js files loaded by Rules with your own ones? If not, maybe we can work on making that easier overridable so you can alter in your own JS then.

>Calling setParent() on an incompatible container throws an evaluation exception

I agree, this is actually something that just should not happen. Maybe it would be better to throw a different exception, e.g. an InvalidArgumentException.

fago’s picture

Assigned: fago » Unassigned
zhangtaihao’s picture

> couldn't you just solve it from your module and replace the js files loaded by Rules with your own ones?

That's a good point. I'll try doing that instead.

> Maybe it would be better to throw a different exception

I think the issue is more how the UI should react to such exceptions, but perhaps that's a separate issue.

If you agree, we can change this issue to Conditional Rules and create the setParent() issue for separate follow-up.

fago’s picture

Sounds good!

fago’s picture

Issue summary: View changes

Added info since change in issue focus.

Samgarr’s picture

Status: Needs work » Needs review
OnkelTem’s picture

Issue summary: View changes
Status: Needs review » Needs work

This doesn't seem to work anymore. And this is very bad as now it's simply impossible to edit existing Rules Components. What a surprise...