eventespresso / barista

Javascript modules & tools for Event Espresso development
GNU General Public License v3.0
5 stars 1 forks source link

Registration Form Builder + Conditional Logic #757

Open tn3rb opened 3 years ago

tn3rb commented 3 years ago

Background

Our current registration form schema utililzes a system carried over from EE3 consisting of questions and question groups of which the latter are associated with events.

This system has many shortcomings that we have encountered over the years including:

Task

As part of our modernization process involving the conversion of our presentation layer to React + Apollo & GraphQL, as well as facilitating the ability to provide our users with some long awaited features, we will be refactoring the registration form system. This will involve:

Data Schema

The following tables show the proposed GQL data schema with the corresponding server side model schema where new fields are marked with an *.

Form Input | Question

Questions are the "Form Inputs", and we may want to refer to them as such from now on.

GQL Field DB Field (*=new) Type ???
dbId QST_ID int
id UUID* string save UUID to DB?
adminLabel admin_label string
adminOnly admin_only bool
default? system int immutable question: 1 (personal) or 2 (address)
displayText display_text string html label text
helpClass help_class* string
helpText help_text* string
htmlClass html_class* string
max max int
order order int
placeholder placeholder* string
required required bool
requiredText required_text string
status deleted string convert from bool to string
type type string
wpUser wp_user int

Form Section | Question Group

Question Groups are essentially "Form Sections", and we may want to refer to them as such from now on.

GQL Field DB Field (*=new) Type ???
dbId QSG_ID int
id UUID* string save UUID to DB?
default? system int immutable group: 1 (personal) or 2 (address)
description description string
htmlClass html_class* string
identifier identifier string deprecate?
name name string
order order int
parent parent* int ID or UUID of another form section
showDesc show_group_desc bool
showName show_group_name bool
status deleted string convert from bool to "archived"
wpUser wp_user int who created it

Form Relation | Event Question Group

This is currently the intersection table between Events and Question Groups, but we should refer to it from now on as "Form Relations" because this table will be getting heavily modified in order to expand what entities a question or question group can be related to

GQL Field DB Field (*=new) Type ???
dbId EQG_ID int
id UUID* string save UUID to DB?
appliesTo applies_to* string purchaser, primary, additional, all, ???
objId OBJ_ID* int model object ID: EVT_ID, DTT_ID, TKT_ID
objType OBJ_type* enum model object type: "Event", "Datetime", "Ticket", "Venue", etc
qsgId QSG_ID int link Form Section to Event, Datetime, Ticket, etc
qstId QST_ID* int link Form Input to Event, Datetime, Ticket, etc
EVT_ID int legacy field - deprecate?
primary string legacy field - deprecate?
additional string legacy field - deprecate?
example data
dbId id appliesTo objId objType qsgId qstId ???
275 A1B2C3 all 12 Event 1 assigns Qst Group 1 to all registrants
276 D4E5F6 primary 12 Event 2 assigns Qst Group 2 to primary registrant
365 G7H8I9 additional 47 Datetime 23 assigns Question 23 to additional registrants

Action (new)

A new table added to facilitate conditional logic. Actions specify what behaviour should occur when the associated conditional rules are satisfied. example: for the following Action Rule: DISPLAY Form Input 456 IF Form Input 123 Answer === "potato" the "action" might be "DisplayFormQuestion", the target would be "456", and the IF conditions would be handled by rules.

GQL Field DB Field Type ???
dbId ACT_ID int
id UUID string save UUID to DB?
action action string do what? (action handler FQCN?)
target target string to what? (action handler param?)
data data JSON extra data to be passed to action handler
example data
dbId id action target data ???
36 J2K4L7 displayFormInput 456 displays form input 456
37 M3N5O7 sendEmail eventAdmin eventSoldOut sends event sold out notice to event admin

Action Object (new)

A new table added to facilitate conditional logic. It is the intersection table for connecting actions to other entities

GQL Field DB Field Type ???
dbId AOB_ID int
id UUID string save UUID to DB?
actId ACT_ID int foreign key to Action table
objId OBJ_ID int model object ID: EVT_ID, DTT_ID, TKT_ID, QST_ID
object object enum model object type: "Event", "Datetime", "Ticket", "Question", etc
example data
dbId id actId objId object ???
14 P7Q8R9 36 123 Question assign action 36 to question 123
27 S1T4U6 37 12 Event assign action 37 to event 12

Action Rule (new)

A new table added to facilitate conditional logic. It is the intersection table for connecting actions to rules

GQL Field DB Field Type ???
dbId ARL_ID int
id UUID string save UUID to DB?
actId ACT_ID int foreign key to Action table
group group int subset this rule belongs to ex: (A or B) && (C or D) has two groups
operator operator enum 'AND', 'OR', NULL
order order int rule sequence
ruleId RUL_ID int foreign key to Rule table
example data : RULE 123 && RULE 124 && RULE 125
dbId id actId operator group ruleId order
33 V6W2X7 36 AND 123
34 Y7Z5Z0 36 124
35 Y7Z5Z0 36 125
example data : RULE 123 || RULE 1324|| RULE 125
dbId id actId operator group ruleId order
33 V6W2X7 36 OR 123
34 Y7Z5Z0 36 124
35 Y7Z5Z0 36 125
example data : RULE 123 && ( RULE 124 || RULE 125 )
dbId id actId operator group ruleId order
33 V6W2X7 36 AND 123
34 Y7Z5Z0 36 OR 1 124
35 Y7Z5Z0 36 1 125
example data : ( RULE 123 || RULE 124 ) && ( RULE 125 || RULE 126 )
dbId id actId operator group ruleId order
33 V6W2X7 36 AND 1
33 V6W2X7 36 OR 1 123 2
34 Y7Z5Z0 36 1 124 3
35 Y7Z5Z0 36 OR 2 125 4
35 Y7Z5Z0 36 2 126 5

If the above seems odd to you, it's because of the way the PHP model system operates. It may make more sense if you replace the following words with these phrases:

Rows with no ruleId mean that the following row is a subgroup, which is the only way I could think of to handle a set of rules like: "If ALL of the following are true" : ( A or B ) && ( C or D ) which is depicted in the last table of example data above.

Action Trigger (future use?)

A new table for dynamically controlling WHEN an action occurs, via crons, dates, or hooks.

GQL Field DB Field Type ???
dbId ATR_ID int
id UUID string save UUID to DB?
actId ACT_ID int foreign key to Action table
active active bool or maybe "status"?
description description string explanation of trigger
name name string admin label
trigger trigger enum cron, date, hook
value value string see "Action Trigger Values" below

Action Trigger Values

cron: daily|weekly|monthly date: ISO 8601 timestamp hook: name of hook, ex: AHEE__EE_Event__set_status__after_update

example data
dbId id actId name description trigger value active
45 H2D8K4 37 "Sold Out Event Notification" "Sends email notification to event admins when an event sells out" hook "AHEEEE_Event__set_statusto_sold_out" true

Rule (new)

This is a new table added to facilitate conditional logic. This model will be used for multiple features beyond conditional logic in the registration form, so some of its functionality might not make complete sense for the time being.

GQL Field DB Field Type ???
dbId RUL_ID int
id UUID string save UUID to DB?
comparison comparison enum (see "Comparison Enum Options" below)
source source string model/object name or FQCN for strategy class
target target string model field or class method
type type enum query, model, strategy
value value mixed

Comparison Enum Options

example data
dbId id type source target comparison value
33 L8B4V5 query "Datetime" "DTT_EVT_start" BEFORE "NOW"
37 W8F3K6 query "Registration" "STS_ID" = "RAP"
42 T4J5M1 model "WP_User" "has_cap" PARAM "event_administrator"
47 U9Q1W2 query "Answer" "QST_ID" = 123
47 U9Q1W2 query "Answer" "ANS_value" = "potato"
47 U9Q1W2 model "Ticket" "percentSold" >= 25

Each record could be viewed as representing a single condition from the WHERE clause of an SQL query. example:

perform some action if registration status equals approved

would utilize the following Rule fields

// perform some action if
{source} {target} {comparison} {value}
// {registration} {status} {equals} {approved}

This is the initial issue for planning things out and is still a work in progress. Additional focused issues will be created for directing PRs.

manzoorwanijk commented 3 years ago

Everything looks perfect, though I didn't fully understand how the schema defined for Rule will be integrated with our form.

tn3rb commented 3 years ago

I didn't fully understand how the schema defined for Rule will be integrated with our form.

It was still a work in progress and has been updated since you last saw it. There is now a table for recording what action should be taken when a set of rules passes; for example, displaying a form input when the answer for another input matches some value. Then there are a couple of intersection tables between Actions and other models called "Action Object" as well as between Actions and Rules called "Action Rule".

tn3rb commented 3 years ago

Usage

The Data Schema above does nothing on its own of course and only becomes functional within a framework that can utilize it. Ideally I would like to see things combined in a functional or OOP manner (depending on language) following established design patterns.

For server side code I plan to utilize the "Specification" pattern as well as Composites (or variations thereof) so that rule objects can be created from db records and then passed to a mediator along with the other data being tested.

So off the top of my head, maybe something like:

class ActionHandler
{
    private $actions_model;
    private $rules_manager;

    public function executeActionsFor(Candidate $candidate)
    {
        $actions = $this->getActionsFor($candidate);
        foreach($actions as $action) {
            $rules = $this->rules_manager->getRulesForAction($action);
            if ($candidate->satisfies($rules)) {
                $action->execute();
            }
        }
    }

    public function getActionsFor($candidate)
    {
        return $this->actions_model->get(
            [
                'OBJ_ID' => $candidate->ID(),
                'object' => $candidate->type(),
                // real query would be more complex
            ]
        );
    }
}

/**
 * mediator between model objects and specifications handler
 */
class Candidate
{
    private $model_object;
    private $specifications_handler;

    public function __construct(EE_Base_Class $model_object)
    {
        $this->model_object = $model_object;
    }

    public function satisfies($rules) : bool
    {
        return $this->specifications_handler->satisfies(
            $this->model_object,
            $rules
        );
    }
}

For the client side code, things will obviously look quite different. We will be starting with the conditional form logic where most usage will be for displaying or hiding inputs based on the current user input value for another question. Ideally we should build some sort of specifications manager in JS that will operate similarly in that the persisted rule data can be dynamically transformed into typed objects that can then be handled accordingly in order to determine whether a particular action should be executed.

It may be tempting to wire up the rule data directly to its usage in the form system, but that will eventually result in duplication down the road as more of our presentation layer gets converted to React.

Specification Pattern in JS/TS

Registration Form Generator

The new Registration Form Generator will be integrated within the EDTR instead of the existing legacy admin page. It will either be added below the tickets list OR... <tangent> we could add a new UI element that would basically be an additional vertical sidebar navigation on the left-hand side of the main content area. It would be 50-60px wide and normally only display icons but could expand and also show text on hover / focus. The icons would represent the different sections of the Event Editor, such as

The Registration Form Generator will allow event admins to create registration forms that are customized for each event as opposed to simply assigning question groups to either the primary or additional registrants. The existing Registration Form admin page will also need to be replaced with essentially the same UI we add to the EDTR but will be only for editing default questions and form sections that will be duplicated and added to new events upon creation.

The Registration Form Generator will need to consist of the following features:

Form Section Edit Modal

This modal appears when a user clicks on the edit action button for a form section. It will need to contain controls for all the attributes that get saved to the Question Group table. For improved UI/UX these should be separated into different groups whose display is controlled by tabs (???). For example:

Settings Tab

control type ???
name string  
showName bool toggle control
description text area  
showDesc bool toggle control

why toggle controls? because admins may want to label the form sections and add notes that they do not want displayed to the public in the actual registration form

Style Tab

control type ???
css class string
custom css text area

Rules Tab

control type ???
action select show or hide this field if
ALL or ANY select ALL (or ANY) of the following are true
target select list of current form elements
comparison select > < = etc
value text
delete rule icon button trash can icon after each rule
add rule button

Form Element Edit Modal

This modal appears when a user clicks on the edit action button for a form element. It will need to contain controls for all the attributes that get saved to the Question table. For improved UI/UX these should be separated into different groups whose display is controlled by tabs (???). For example:

Settings Tab

control type ???
admin label string
display text string html label text
placeholder string
help text string
adminOnly bool toggle control

Style Tab

control type ???
label css class string
input css class string
help text css class string
custom css text area

we can add additional controls here for various options that correspond to css classes for example, having a control to select field width with options like small, medium large or 25% 50% 100% which would result in specific css classes getting appended to the htmlClass value saved to the db

Validations Tab

changes depending on question type control type ???
required bool toggle control
requiredText string only displays if required is set to "on"
max int for number or text inputs
min int for number or text inputs

Rules Tab

control type ???
action select show or hide this field if
ALL or ANY select ALL (or ANY) of the following are true
target select list of current form elements
comparison select > < = etc
value text
delete rule icon button trash can icon after each rule
add rule button

plz note that we can also add extra fields to the Question or Question Group models in order to hold additional data if need be so the above configuration is jsut a starting point.

next steps ???

manzoorwanijk commented 3 years ago

I agree with following the "Specification" pattern. It will make everything much easier.

manzoorwanijk commented 3 years ago

Here are the options that I came across while researching about this:

  1. xState - Formal Forms with State Machines This solution is a robust one for any large scale application state management, but it doesn't fit here to be used for conditional forms generator. Conditional form generator is basically a collection for form fields embedded into stacked modals. xState may be an overkill for it. Another argument is that it basically handles the state, which we already do with Apollo, so adding another state/caching layer would not be a good idea.
  2. React Final Form (RFF) RFF provides us everything we need for managing the form states and it's already integrated into our code base and thus should be easier to deal with. Also, we already have the conditional logic support in our form package, it should be more than enough to create such forms.

The only catch however is that how the generated form data is expected on the front-end. I think we should may be create some mocks to easily draw a map of all the things that need to work together.

P.S. I also tried to explore many of the WP forms solutions out there but almost all of those codebases are private.

Conclusion: We already have everything we need to generate such forms:

tn3rb commented 3 years ago

ok great, thnx for researching that.

I'm a little bit dismayed you didn't have any objections to the following:

the action menu contains options such as "edit", "copy", "delete", and "reorder".

  • edit will open a modal window with inputs for controlling all aspects of that element and will be the meat and potatoes of the application (more on this below)

  • copy would create a duplicate immediately after the copied element in the form

  • delete should obviously drop all tables and databases on the server, as well as delete all files including those belonging to the operating system. If possible, all data on all servers sharing the same host field of the current server's IP address should also be wiped clean. You can never take data privacy too serious. A confirmation modal should be triggered prior to continuing with deletion.

manzoorwanijk commented 3 years ago

Well, there is a reason I mentioned creating mocks. Things will become clearer once we have a visual idea about the UI elements.

tn3rb commented 3 years ago

the general development roadmap might look like this:

tn3rb commented 3 years ago

here's a rough mockup to give you an idea of what we will be building in the admin

Registration_Form_Builder

the final product will undoubtedly end up being a bit different as I'm sure there will need to be changes as we discover various changes in requirements

tn3rb commented 3 years ago

let's name this puppy the EDTR Reg Form Builder

sethshoultes commented 3 years ago

So far, so good.