Plugin Directory

Changeset 3433300


Ignore:
Timestamp:
01/06/2026 07:35:03 AM (2 months ago)
Author:
griffinforms
Message:

Release 2.1.3.0

Location:
griffinforms-form-builder/trunk
Files:
16 added
19 edited

Legend:

Unmodified
Added
Removed
  • griffinforms-form-builder/trunk/admin/ajax/form.php

    r3377151 r3433300  
    105105    }
    106106
     107    public function getFormFields()
     108    {
     109        check_ajax_referer('get_form_fields', 'nonce');
     110        $this->checkPostData('form_id', 0);
     111
     112        $form_id = absint(wp_unslash($_POST['form_id']));
     113        if (!$form_id || !$this->sql->itemExists('form', $form_id)) {
     114            return $this->returnError(__('Form not found.', 'griffinforms-form-builder'));
     115        }
     116
     117        $fields = $this->sql->fieldsInForm($form_id);
     118        if (!is_array($fields)) {
     119            return $this->returnError(__('Unable to fetch form fields.', 'griffinforms-form-builder'));
     120        }
     121
     122        $output = [];
     123        foreach ($fields as $field) {
     124            if (empty($field->id)) {
     125                continue;
     126            }
     127            $output[] = [
     128                'id'          => (int) $field->id,
     129                'name'        => isset($field->name) ? (string) $field->name : '',
     130                'heading'     => isset($field->heading) ? (string) $field->heading : '',
     131                'field_type'  => isset($field->field_type) ? (string) $field->field_type : '',
     132                'description' => isset($field->description) ? (string) $field->description : '',
     133            ];
     134        }
     135
     136        return $this->returnSuccess(['fields' => $output]);
     137    }
     138
    107139    /**
    108140     * Handles AJAX request for fetching the number of rules for a given form.
     
    132164        add_action('wp_ajax_checkEmailField', array($this, 'checkEmailField'));
    133165        add_action('wp_ajax_griffinformsGetRulesCount', [$this, 'getRulesCount']);
     166        add_action('wp_ajax_griffinforms_get_form_fields', [$this, 'getFormFields']);
    134167
    135168    }
  • griffinforms-form-builder/trunk/admin/css/griffinforms.css

    r3424123 r3433300  
    402402}
    403403
     404.gf-entryalert-message-preview {
     405    background: #f8f9fa;
     406    border: 1px dashed #cfd4da;
     407    border-radius: 6px;
     408    padding: 6px 8px;
     409    min-height: 38px;
     410}
     411
     412.gf-entryalert-message-name {
     413    font-weight: 600;
     414}
     415
     416.gf-merge-token-panel {
     417    border: 1px solid #dcdcde;
     418    border-radius: 6px;
     419    padding: 12px;
     420    background: #fff;
     421}
     422
    404423.griffinforms-toast {
    405424    position: fixed;
  • griffinforms-form-builder/trunk/admin/html/formcontrols/form/entryalerts.php

    r3299683 r3433300  
    3434    protected function entryAlertsEmails()
    3535    {
    36         $data = array();
    37         $data['max'] = 10;
    38         $data['input_type'] = 'email';
    39         $data['placeholder'] = $this->lang->getText('admin_email_placeholder');
    40         $field = new \GriffinForms\Admin\Html\FormControls\MultiInput('entry_alerts_emails', $this->value, $data);
    41         $field->html();
     36        $rows = $this->normalizeAlertEmailRows();
     37        $default_label = $this->lang->getText('default_message_label');
     38
     39        echo '<div id="griffinforms-entryalertsemails-container" data-next-row-id="' . esc_attr($rows['next_row_id']) . '" data-max-emails="5">';
     40        foreach ($rows['rows'] as $row) {
     41            $this->renderAlertEmailRow($row['row_id'], $row['email'], $row['message_id'], $row['message_name'], $row['message_heading'], $default_label);
     42        }
     43        echo '</div>';
     44
     45        echo '<div class="row"><div class="col-md-7 text-center mt-3">';
     46        echo '<button type="button" id="griffinforms-entryalertsemails-add" class="gf-entryalert-add-btn btn btn-sm btn-dark">';
     47        $this->mIcon('add');
     48        echo '</button></div></div>';
     49    }
     50
     51    protected function normalizeAlertEmailRows(): array
     52    {
     53        $rows = [];
     54        $row_id = 0;
     55
     56        if (is_array($this->value) && !empty($this->value)) {
     57            $is_assoc = array_keys($this->value) !== range(0, count($this->value) - 1);
     58            if ($is_assoc) {
     59                foreach ($this->value as $email => $message_id) {
     60                    $row_id++;
     61                    $rows[] = $this->buildAlertEmailRow($row_id, $email, $message_id);
     62                }
     63            } else {
     64                foreach ($this->value as $email) {
     65                    $row_id++;
     66                    $rows[] = $this->buildAlertEmailRow($row_id, $email, 0);
     67                }
     68            }
     69        }
     70
     71        if (empty($rows)) {
     72            $row_id++;
     73            $rows[] = $this->buildAlertEmailRow($row_id, '', 0);
     74        }
     75
     76        return [
     77            'rows' => $rows,
     78            'next_row_id' => $row_id + 1,
     79        ];
     80    }
     81
     82    protected function buildAlertEmailRow(int $row_id, $email, $message_id): array
     83    {
     84        $email = is_string($email) ? $email : '';
     85        $message_id = absint($message_id);
     86        $message_name = '';
     87        $message_heading = '';
     88
     89        if ($message_id > 0) {
     90            $message = $this->sql->itemValues($message_id, 'message', ['name', 'heading']);
     91            if ($message) {
     92                $message_name = !empty($message->name) ? $message->name : 'Message ID ' . $message_id;
     93                $message_heading = $message->heading ?? '';
     94            }
     95        }
     96
     97        return [
     98            'row_id' => $row_id,
     99            'email' => $email,
     100            'message_id' => $message_id,
     101            'message_name' => $message_name,
     102            'message_heading' => $message_heading,
     103        ];
     104    }
     105
     106    protected function truncateText(string $text, int $maxLength): string
     107    {
     108        if ($maxLength <= 0 || $text === '') {
     109            return $text;
     110        }
     111        if (mb_strlen($text) <= $maxLength) {
     112            return $text;
     113        }
     114        return mb_substr($text, 0, $maxLength - 3) . '...';
     115    }
     116
     117    protected function renderAlertEmailRow(int $row_id, string $email, int $message_id, string $message_name, string $message_heading, string $default_label): void
     118    {
     119        $select_label = $this->lang->getText('select_message_label');
     120        $change_label = $this->lang->getText('change_message_label');
     121        $remove_label = $this->lang->getText('remove_message_label');
     122        $placeholder = $this->lang->getText('admin_email_placeholder');
     123
     124        $button_label = $message_id > 0 ? $change_label : $select_label;
     125        $show_remove = $message_id > 0 ? '' : ' style="display: none;"';
     126        $message_name = $message_id > 0 ? $message_name : $default_label;
     127        $message_heading = $message_id > 0 ? $message_heading : '';
     128        $message_name = $this->truncateText($message_name, 40);
     129        $message_heading = $this->truncateText($message_heading, 48);
     130
     131        echo '<div class="gf-entryalert-row row align-items-center mb-2" data-alert-row-id="' . esc_attr($row_id) . '">';
     132        echo '<div class="col-md-3">';
     133        echo '<input type="email" class="form-control griffinforms-entryalertsemails-input" placeholder="' . esc_attr($placeholder) . '" value="' . esc_attr($email) . '">';
     134        echo '</div>';
     135        echo '<div class="col-md-3">';
     136        echo '<button type="button" class="button small btn-sm gf-entryalert-select-message me-2" data-alert-row-id="' . esc_attr($row_id) . '">' . esc_html($button_label) . '</button>';
     137        echo '<button type="button" class="button small btn-sm gf-entryalert-remove-message" data-alert-row-id="' . esc_attr($row_id) . '"' . $show_remove . '>' . esc_html($remove_label) . '</button>';
     138        echo '</div>';
     139        echo '<div class="col-md-4">';
     140        echo '<div class="gf-entryalert-message-preview">';
     141        echo '<span class="gf-entryalert-message-name">' . esc_html($message_name) . '</span>';
     142        if (!empty($message_heading)) {
     143            echo '<br><span class="text-muted small gf-entryalert-message-heading">' . esc_html($message_heading) . '</span>';
     144        } else {
     145            echo '<br><span class="text-muted small gf-entryalert-message-heading"></span>';
     146        }
     147        echo '</div>';
     148        echo '</div>';
     149        echo '<div class="col-md-1 text-end">';
     150        echo '<button type="button" class="gf-entryalert-remove-row btn btn-sm btn-dark">';
     151        $this->mIcon('delete');
     152        echo '</button>';
     153        echo '</div>';
     154        echo '<input type="hidden" class="gf-entryalert-message-id" value="' . esc_attr($message_id) . '">';
     155        echo '</div>';
     156    }
     157
     158    public function fieldJs()
     159    {
     160        $select_label = $this->lang->getText('select_message_label');
     161        $change_label = $this->lang->getText('change_message_label');
     162        $remove_label = $this->lang->getText('remove_message_label');
     163        $default_label = $this->lang->getText('default_message_label');
     164        $placeholder = $this->lang->getText('admin_email_placeholder');
     165
     166        $row_template = '<div class="gf-entryalert-row row align-items-center mb-2" data-alert-row-id="__ROW_ID__">' .
     167            '<div class="col-md-3">' .
     168            '<input type="email" class="form-control griffinforms-entryalertsemails-input" placeholder="' . esc_attr($placeholder) . '" value="">' .
     169            '</div>' .
     170            '<div class="col-md-3">' .
     171            '<button type="button" class="button small btn-sm gf-entryalert-select-message me-2" data-alert-row-id="__ROW_ID__">' . esc_html($select_label) . '</button>' .
     172            '<button type="button" class="button small btn-sm gf-entryalert-remove-message" data-alert-row-id="__ROW_ID__" style="display: none;">' . esc_html($remove_label) . '</button>' .
     173            '</div>' .
     174            '<div class="col-md-4">' .
     175            '<div class="gf-entryalert-message-preview">' .
     176            '<span class="gf-entryalert-message-name">' . esc_html($default_label) . '</span>' .
     177            '<br><span class="text-muted small gf-entryalert-message-heading"></span>' .
     178            '</div>' .
     179            '</div>' .
     180            '<div class="col-md-1 text-end">' .
     181            '<button type="button" class="gf-entryalert-remove-row btn btn-sm btn-dark">' . wp_kses_post($this->mIcon('delete', 'rounded', 'return')) . '</button>' .
     182            '</div>' .
     183            '<input type="hidden" class="gf-entryalert-message-id" value="0">' .
     184            '</div>';
     185
     186        $js = '
     187        jQuery(document).ready(function($) {
     188            var container = $("#griffinforms-entryalertsemails-container");
     189            var addBtn = $("#griffinforms-entryalertsemails-add");
     190            var rowTemplate = ' . json_encode($row_template) . ';
     191            var selectLabel = ' . json_encode($select_label) . ';
     192            var changeLabel = ' . json_encode($change_label) . ';
     193            var defaultLabel = ' . json_encode($default_label) . ';
     194            function truncateText(text, maxLength) {
     195                if (!text) { return ""; }
     196                return text.length > maxLength ? text.substring(0, maxLength - 3) + "..." : text;
     197            }
     198            var maxEmails = parseInt(container.attr("data-max-emails"), 10) || 5;
     199
     200            var selectModal = $("#griffinforms-selectmessage-modal");
     201            if (selectModal.length) {
     202                selectModal.appendTo("body");
     203            }
     204            var createModal = $("#griffinforms-createmessage-modal");
     205            if (createModal.length) {
     206                createModal.appendTo("body");
     207            }
     208
     209            function nextRowId() {
     210                var current = parseInt(container.attr("data-next-row-id"), 10) || 1;
     211                container.attr("data-next-row-id", current + 1);
     212                return current;
     213            }
     214
     215            function ensureAtLeastOneRow() {
     216                if (container.find(".gf-entryalert-row").length === 0) {
     217                    addRow();
     218                }
     219            }
     220
     221            function updateRemoveRowVisibility() {
     222                var rows = container.find(".gf-entryalert-row");
     223                if (rows.length <= 1) {
     224                    rows.find(".gf-entryalert-remove-row").hide();
     225                } else {
     226                    rows.find(".gf-entryalert-remove-row").show();
     227                }
     228
     229                if (rows.length >= maxEmails) {
     230                    addBtn.hide();
     231                } else {
     232                    addBtn.show();
     233                }
     234            }
     235
     236            function addRow() {
     237                var rowId = nextRowId();
     238                var html = rowTemplate.replace(/__ROW_ID__/g, rowId);
     239                container.append(html);
     240                updateRemoveRowVisibility();
     241            }
     242
     243            function resetMessageSelection(row) {
     244                row.find(".gf-entryalert-message-id").val("0");
     245                row.find(".gf-entryalert-message-name").text(defaultLabel);
     246                row.find(".gf-entryalert-message-heading").text("");
     247                row.find(".gf-entryalert-select-message").text(selectLabel);
     248                row.find(".gf-entryalert-remove-message").hide();
     249            }
     250
     251            addBtn.on("click", function() {
     252                if (container.find(".gf-entryalert-row").length >= maxEmails) {
     253                    return;
     254                }
     255                addRow();
     256            });
     257
     258            container.on("click", ".gf-entryalert-remove-row", function() {
     259                $(this).closest(".gf-entryalert-row").remove();
     260                ensureAtLeastOneRow();
     261                updateRemoveRowVisibility();
     262            });
     263
     264            container.on("click", ".gf-entryalert-remove-message", function() {
     265                var row = $(this).closest(".gf-entryalert-row");
     266                resetMessageSelection(row);
     267            });
     268
     269            container.on("click", ".gf-entryalert-select-message", function() {
     270                var row = $(this).closest(".gf-entryalert-row");
     271                var rowId = row.attr("data-alert-row-id");
     272                var modal = $("#griffinforms-selectmessage-modal");
     273                modal.data("gf-trigger", "map_alert_email_message");
     274                modal.data("gf-alert-row-id", rowId);
     275                modal.modal("show");
     276            });
     277
     278            window.griffinformsSaveAlertEmailMapping = function(rowId, itemId, itemName, itemHeading) {
     279                var row = container.find(".gf-entryalert-row[data-alert-row-id=\'" + rowId + "\']");
     280                if (!row.length) { return; }
     281                row.find(".gf-entryalert-message-id").val(itemId);
     282                row.find(".gf-entryalert-message-name").text(truncateText(itemName || defaultLabel, 30));
     283                row.find(".gf-entryalert-message-heading").text(truncateText(itemHeading || "", 38));
     284                row.find(".gf-entryalert-select-message").text(changeLabel);
     285                row.find(".gf-entryalert-remove-message").show();
     286            };
     287
     288            updateRemoveRowVisibility();
     289        });';
     290
     291        wp_add_inline_script('griffinforms-global-js', $js);
    42292    }
    43293}
  • griffinforms-form-builder/trunk/admin/html/modals/selectitem.php

    r3310004 r3433300  
    172172                    if (
    173173                        $(caller).attr("id") === "<?php echo esc_attr($this->ids['open_btn']); ?>" ||
    174                         $(caller).data("gf-trigger") === "map_email_message"
     174                        $(caller).data("gf-trigger") === "map_email_message" ||
     175                        $(caller).data("gf-trigger") === "map_alert_email_message"
    175176                    ) {
    176177                        getItemsForModal(0, "list_empty", caller);
     
    287288            function handleEmailFieldMessageMapping(itemId, itemName, itemHeading) {
    288289                var selectionModal = $("#<?php echo esc_attr($this->id); ?>");
     290                if (selectionModal.data("gf-trigger") !== "map_email_message") {
     291                    return;
     292                }
    289293                var fieldId = selectionModal.data("gf-email-field-id");
    290294                var parentModalId = selectionModal.data("gf-parentmodalid");
     
    300304                    }
    301305                }
     306            }
     307
     308            // Handle mapping for admin alert email rows (no parent modal).
     309            function handleAlertEmailMessageMapping(itemId, itemName, itemHeading) {
     310                var selectionModal = $("#<?php echo esc_attr($this->id); ?>");
     311                if (selectionModal.data("gf-trigger") !== "map_alert_email_message") {
     312                    return;
     313                }
     314                var rowId = selectionModal.data("gf-alert-row-id");
     315                if (!rowId) {
     316                    return;
     317                }
     318
     319                if (typeof window.griffinformsSaveAlertEmailMapping === "function") {
     320                    window.griffinformsSaveAlertEmailMapping(rowId, itemId, itemName, itemHeading);
     321                }
     322
     323                selectionModal.modal("hide");
    302324            }
    303325
     
    311333
    312334                handleEmailFieldMessageMapping(itemId, itemName, itemHeading);
     335                handleAlertEmailMessageMapping(itemId, itemName, itemHeading);
    313336
    314337                itemIcon = $(this).find(".griffinforms-modalitemlist-icon").attr("src");
  • griffinforms-form-builder/trunk/admin/html/pages/single/format.php

    r3299683 r3433300  
    110110    public function getOption($prop, $wide = false)
    111111    {
    112         $label_class = 'col-md-3';
     112        $label_class = 'col-md-3 ps-3';
    113113        $input_class = 'col-md-9';
    114114
    115115        if ($wide) {
    116             $label_class = 'col-md-12';
    117             $input_class = 'col-md-12';
     116            $label_class = 'col-md-12 ps-3 mb-3';
     117            $input_class = 'col-md-12 ps-3';
    118118        }
    119119
     
    153153        }
    154154       
    155         echo '<label class="fw-bold ps-3">' . wp_kses_post($this->lang->getText($this->prop, 'label') . ' ' . $label_suffix) . '</label>';
     155        echo '<label class="fw-bold">' . wp_kses_post($this->lang->getText($this->prop, 'label') . ' ' . $label_suffix) . '</label>';
    156156    }
    157157   
  • griffinforms-form-builder/trunk/admin/html/pages/single/message.php

    r3299683 r3433300  
    1515    {
    1616        echo '<div class="container border-bottom griffinforms-item-options w-75 mt-3 pt-3">';
    17         $this->getOption('content');
     17        $this->mergeTokenTools();
     18        $this->getOption('content', true);
    1819        echo '</div>';
    1920    }
     
    3637        $this->richText();
    3738    }
     39
     40    protected function mergeTokenTools()
     41    {
     42        $forms = $this->sql->getFormContextOptions();
     43
     44        echo '<div class="gf-merge-token-panel mb-3">';
     45        echo '<div class="row mb-2">';
     46        echo '<div class="col-md-12 small text-muted">' . esc_html($this->lang->getText('merge_tokens_description')) . '</div>';
     47        echo '</div>';
     48
     49        echo '<div class="row g-2 align-items-end">';
     50        echo '<div class="col-md-3">';
     51        echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_form_context_label')) . '</label>';
     52        echo '<select class="form-select form-select-sm" id="griffinforms-merge-form-context">';
     53        echo '<option value="">' . esc_html($this->lang->getText('select_form')) . '</option>';
     54        foreach ($forms as $form) {
     55            $label = !empty($form->name) ? $form->name : ('Form ' . $form->id);
     56            if (!empty($form->heading) && $form->heading !== $label) {
     57                $label .= ' - ' . $form->heading;
     58            }
     59            echo '<option value="' . esc_attr($form->id) . '">' . esc_html($label) . '</option>';
     60        }
     61        echo '</select>';
     62        echo '</div>';
     63
     64        echo '<div class="col-md-2">';
     65        echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_item_type_label')) . '</label>';
     66        echo '<select class="form-select form-select-sm" id="griffinforms-merge-item-type">';
     67        echo '<option value="field">' . esc_html($this->lang->getText('merge_item_type_field')) . '</option>';
     68        echo '<option value="form">' . esc_html($this->lang->getText('merge_item_type_form')) . '</option>';
     69        echo '<option value="submission">' . esc_html($this->lang->getText('merge_item_type_submission')) . '</option>';
     70        echo '</select>';
     71        echo '</div>';
     72
     73        echo '<div class="col-md-3">';
     74        echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_field_label')) . '</label>';
     75        echo '<div class="d-flex align-items-center gap-2">';
     76        echo '<select class="form-select form-select-sm" id="griffinforms-merge-field-select">';
     77        echo '<option value="">' . esc_html($this->lang->getText('select_field')) . '</option>';
     78        echo '</select>';
     79        echo '<span class="spinner is-active" id="griffinforms-merge-field-spinner" style="visibility: hidden;"></span>';
     80        echo '</div>';
     81        echo '</div>';
     82
     83        echo '<div class="col-md-2">';
     84        echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_attr_label')) . '</label>';
     85        echo '<select class="form-select form-select-sm" id="griffinforms-merge-attr-select"></select>';
     86        echo '</div>';
     87
     88        echo '<div class="col-md-2">';
     89        echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_fallback_label')) . '</label>';
     90        echo '<input type="text" class="form-control form-control-sm" id="griffinforms-merge-fallback">';
     91        echo '</div>';
     92        echo '</div>';
     93
     94        echo '<div class="row g-2 align-items-end mt-2">';
     95        echo '<div class="col-md-10">';
     96        echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_token_preview_label')) . '</label>';
     97        echo '<input type="text" class="form-control form-control-sm" id="griffinforms-merge-token-preview" readonly>';
     98        echo '</div>';
     99
     100        echo '<div class="col-md-2">';
     101        echo '<div class="d-flex justify-content-end align-items-center gap-2">';
     102        echo '<span class="spinner is-active" id="griffinforms-merge-token-spinner" style="visibility: hidden;"></span>';
     103        echo '<button type="button" class="button button-primary btn-sm" id="griffinforms-merge-token-insert">' . esc_html($this->lang->getText('merge_insert_token_label')) . '</button>';
     104        echo '</div>';
     105        echo '</div>';
     106        echo '</div>';
     107        echo '</div>';
     108    }
    38109}
  • griffinforms-form-builder/trunk/admin/js/local/form.php

    r3299683 r3433300  
    124124                var sendTo, fieldValue;
    125125               
    126                 fieldValue = [];
     126                fieldValue = {};
    127127                               
    128128                sendTo = $("input[name=entry_alerts_to]:checked").val();
    129129                                   
    130130                if (sendTo === "specific_emails") {
    131                    
    132                     $(".griffinforms-entryalertsemails-input").each(function (index) {
    133                         $(this).removeClass("border-danger");
    134                        
    135                         if (isValidEmail($(this).val()) == false) {
     131                    var validCount = 0;
     132                    var maxEmails = 5;
     133
     134                    $(".gf-entryalert-row").each(function () {
     135                        var emailInput = $(this).find(".griffinforms-entryalertsemails-input");
     136                        var messageIdInput = $(this).find(".gf-entryalert-message-id");
     137                        var emailValue = $.trim(emailInput.val());
     138                        var messageId = parseInt(messageIdInput.val(), 10);
     139
     140                        emailInput.removeClass("border-danger");
     141
     142                        if (!emailValue) {
     143                            return;
     144                        }
     145                       
     146                        if (isValidEmail(emailValue) == false) {
    136147                            showItemError("' . esc_js($this->lang->getText('invalid_email')) . '", "griffinforms-entryalertsemails-container");
    137                             $(this).addClass("border-danger");
    138                         } else {
    139                             fieldValue.push($(this).val());
     148                            emailInput.addClass("border-danger");
     149                            return;
     150                        }
     151
     152                        if (!messageId || isNaN(messageId)) {
     153                            messageId = 0;
     154                        }
     155
     156                        if (validCount < maxEmails) {
     157                            fieldValue[emailValue] = messageId;
     158                            validCount++;
    140159                        }
    141160                    });
     161
     162                    if (validCount === 0) {
     163                        showItemError("' . esc_js($this->lang->getText('invalid_email')) . '", "griffinforms-entryalertsemails-container");
     164                        $(".griffinforms-entryalertsemails-input").each(function () {
     165                            if (!$.trim($(this).val())) {
     166                                $(this).addClass("border-danger");
     167                            }
     168                        });
     169                    }
    142170                }
    143171               
  • griffinforms-form-builder/trunk/admin/js/local/message.php

    r3299683 r3433300  
    55class Message extends \GriffinForms\Admin\Js\Local\Single
    66{
     7    protected function localJs()
     8    {
     9        $js = parent::localJs();
     10        $js .= $this->mergeTokenBuilderJs() . PHP_EOL;
     11        return $js;
     12    }
     13
    714    protected function getContent()
    815    {
     
    1320        }';
    1421    }
     22
     23    protected function mergeTokenBuilderJs()
     24    {
     25        $nonce = wp_create_nonce('get_form_fields');
     26
     27        $js = '
     28        function griffinformsMergeTokenInit() {
     29            var formSelect = $("#griffinforms-merge-form-context");
     30            var itemTypeSelect = $("#griffinforms-merge-item-type");
     31            var fieldSelect = $("#griffinforms-merge-field-select");
     32            var attrSelect = $("#griffinforms-merge-attr-select");
     33            var fallbackInput = $("#griffinforms-merge-fallback");
     34            var previewInput = $("#griffinforms-merge-token-preview");
     35            var insertBtn = $("#griffinforms-merge-token-insert");
     36            var fieldSpinner = $("#griffinforms-merge-field-spinner");
     37            var tokenSpinner = $("#griffinforms-merge-token-spinner");
     38
     39            if (!formSelect.length) {
     40                return;
     41            }
     42
     43            var formAttrs = [
     44                { value: "id", label: "' . esc_js($this->lang->getText('merge_attr_id_label')) . '" },
     45                { value: "name", label: "' . esc_js($this->lang->getText('merge_attr_name_label')) . '" },
     46                { value: "heading", label: "' . esc_js($this->lang->getText('merge_attr_heading_label')) . '" },
     47                { value: "description", label: "' . esc_js($this->lang->getText('merge_attr_description_label')) . '" }
     48            ];
     49
     50            var fieldAttrs = [
     51                { value: "id", label: "' . esc_js($this->lang->getText('merge_attr_id_label')) . '" },
     52                { value: "name", label: "' . esc_js($this->lang->getText('merge_attr_name_label')) . '" },
     53                { value: "label", label: "' . esc_js($this->lang->getText('merge_attr_label_label')) . '" },
     54                { value: "value", label: "' . esc_js($this->lang->getText('merge_attr_value_label')) . '" },
     55                { value: "answer_count", label: "' . esc_js($this->lang->getText('merge_attr_answer_count_label')) . '" },
     56                { value: "description", label: "' . esc_js($this->lang->getText('merge_attr_helptext_label')) . '" }
     57            ];
     58
     59            var submissionAttrs = [
     60                { value: "id", label: "' . esc_js($this->lang->getText('merge_attr_id_label')) . '" },
     61                { value: "position", label: "' . esc_js($this->lang->getText('merge_attr_position_label')) . '" },
     62                { value: "time", label: "' . esc_js($this->lang->getText('merge_attr_time_label')) . '" },
     63                { value: "data", label: "' . esc_js($this->lang->getText('merge_attr_data_label')) . '" },
     64                { value: "attachments_count", label: "' . esc_js($this->lang->getText('merge_attr_attachments_count_label')) . '" },
     65                { value: "attachments_size", label: "' . esc_js($this->lang->getText('merge_attr_attachments_size_label')) . '" },
     66                { value: "payment_total", label: "' . esc_js($this->lang->getText('merge_attr_payment_total_label')) . '" },
     67                { value: "payment_breakdown", label: "' . esc_js($this->lang->getText('merge_attr_payment_breakdown_label')) . '" },
     68                { value: "payment_status", label: "' . esc_js($this->lang->getText('merge_attr_payment_status_label')) . '" }
     69            ];
     70
     71            function populateAttrs(list, preferredValue) {
     72                attrSelect.empty();
     73                list.forEach(function(item) {
     74                    var option = $("<option>").val(item.value).text(item.label);
     75                    if (preferredValue && item.value === preferredValue) {
     76                        option.prop("selected", true);
     77                    }
     78                    attrSelect.append(option);
     79                });
     80            }
     81
     82            function loadFields(formId) {
     83                fieldSelect.empty();
     84                fieldSelect.append($("<option>").val("").text("' . esc_js($this->lang->getText('select_field')) . '"));
     85                if (!formId) {
     86                    return;
     87                }
     88
     89                fieldSpinner.css("visibility", "visible");
     90                tokenSpinner.css("visibility", "visible");
     91                insertBtn.prop("disabled", true);
     92
     93                $.post(ajaxurl, {
     94                    action: "griffinforms_get_form_fields",
     95                    form_id: formId,
     96                    nonce: "' . esc_js($nonce) . '"
     97                }, function(response) {
     98                    if (typeof response !== "object") {
     99                        try {
     100                            response = JSON.parse(response);
     101                        } catch (e) {
     102                            return;
     103                        }
     104                    }
     105                    var fields = response.fields || (response.data && response.data.fields) || [];
     106                    if (!response || !response.success || !Array.isArray(fields)) {
     107                        return;
     108                    }
     109                    fields.forEach(function(field) {
     110                        var label = field.heading || field.name || ("Field " + field.id);
     111                        fieldSelect.append($("<option>").val(field.id).text(label));
     112                    });
     113                }).always(function() {
     114                    fieldSpinner.css("visibility", "hidden");
     115                    tokenSpinner.css("visibility", "hidden");
     116                    updatePreview();
     117                });
     118            }
     119
     120            function toggleFieldSelect() {
     121                if (itemTypeSelect.val() === "form" || itemTypeSelect.val() === "submission") {
     122                    fieldSelect.prop("disabled", true);
     123                } else {
     124                    fieldSelect.prop("disabled", false);
     125                }
     126            }
     127
     128            function toggleAttrSelect() {
     129                if (itemTypeSelect.val() === "form" || itemTypeSelect.val() === "submission") {
     130                    attrSelect.prop("disabled", false);
     131                    return;
     132                }
     133                var fieldId = parseInt(fieldSelect.val(), 10);
     134                attrSelect.prop("disabled", !(fieldId > 0));
     135            }
     136
     137            function buildToken() {
     138                var itemType = itemTypeSelect.val();
     139                var attr = attrSelect.val();
     140                var fallback = $.trim(fallbackInput.val());
     141                var token = {
     142                    item_type: itemType,
     143                    attr: attr
     144                };
     145
     146                if (itemType === "field") {
     147                    var fieldId = parseInt(fieldSelect.val(), 10);
     148                    if (fieldId > 0) {
     149                        token.item_id = fieldId;
     150                    }
     151                }
     152
     153                if (fallback !== "") {
     154                    token.fallback = fallback;
     155                }
     156
     157                return "{{json:" + JSON.stringify(token) + "}}";
     158            }
     159
     160            function updatePreview() {
     161                previewInput.val(buildToken());
     162                toggleInsert();
     163                toggleAttrSelect();
     164            }
     165
     166            function toggleInsert() {
     167                var itemType = itemTypeSelect.val();
     168                if (itemType === "form" || itemType === "submission") {
     169                    insertBtn.prop("disabled", !attrSelect.val());
     170                    return;
     171                }
     172                var fieldId = parseInt(fieldSelect.val(), 10);
     173                insertBtn.prop("disabled", !(fieldId > 0 && attrSelect.val()));
     174            }
     175
     176            function insertToken() {
     177                var token = buildToken();
     178                var editorId = "griffinforms-content-richtext";
     179                if (window.wp && wp.editor && typeof wp.editor.insertContent === "function") {
     180                    wp.editor.insertContent(token);
     181                    return;
     182                }
     183                if (window.tinymce && tinymce.get(editorId)) {
     184                    tinymce.get(editorId).execCommand("mceInsertContent", false, token);
     185                    return;
     186                }
     187                var textarea = $("#" + editorId);
     188                if (textarea.length) {
     189                    var current = textarea.val();
     190                    textarea.val(current + token);
     191                }
     192            }
     193
     194            formSelect.on("change", function() {
     195                loadFields($(this).val());
     196                updatePreview();
     197            });
     198
     199            itemTypeSelect.on("change", function() {
     200                if ($(this).val() === "form") {
     201                    populateAttrs(formAttrs, "name");
     202                } else if ($(this).val() === "submission") {
     203                    populateAttrs(submissionAttrs, "id");
     204                } else {
     205                    populateAttrs(fieldAttrs, "value");
     206                }
     207                toggleFieldSelect();
     208                updatePreview();
     209            });
     210
     211            fieldSelect.on("change", updatePreview);
     212            attrSelect.on("change", updatePreview);
     213            fallbackInput.on("input", updatePreview);
     214            insertBtn.on("click", insertToken);
     215
     216            populateAttrs(fieldAttrs, "value");
     217            toggleFieldSelect();
     218            updatePreview();
     219        }
     220
     221        griffinformsMergeTokenInit();
     222        ';
     223
     224        return $js;
     225    }
    15226}
  • griffinforms-form-builder/trunk/admin/language/form.php

    r3299683 r3433300  
    408408    }
    409409
     410    protected function defaultMessageLabel()
     411    {
     412        return __('Default message', 'griffinforms-form-builder');
     413    }
     414
    410415    protected function selectMessage()
    411416    {
  • griffinforms-form-builder/trunk/admin/language/message.php

    r3299683 r3433300  
    5252    {
    5353        return __('Write the content of your message. This content will be used in emails, so make sure it is clear and concise. Always review the message for accuracy before saving.', 'griffinforms-form-builder');
     54    }
     55
     56    protected function mergeTokensDescription()
     57    {
     58        return __('Build a JSON merge token for this message. Tokens are resolved at send time using the submission context. Tip: Submission attributes are form-agnostic, while Field attributes require a selected form for lookup.', 'griffinforms-form-builder');
     59    }
     60
     61    protected function mergeFormContextLabel()
     62    {
     63        return __('Form Context (Preview)', 'griffinforms-form-builder');
     64    }
     65
     66    protected function mergeItemTypeLabel()
     67    {
     68        return __('Item Type', 'griffinforms-form-builder');
     69    }
     70
     71    protected function mergeItemTypeField()
     72    {
     73        return __('Field', 'griffinforms-form-builder');
     74    }
     75
     76    protected function mergeItemTypeForm()
     77    {
     78        return __('Form', 'griffinforms-form-builder');
     79    }
     80
     81    protected function mergeItemTypeSubmission()
     82    {
     83        return __('Submission', 'griffinforms-form-builder');
     84    }
     85
     86    protected function mergeFieldLabel()
     87    {
     88        return __('Field', 'griffinforms-form-builder');
     89    }
     90
     91    protected function mergeAttrLabel()
     92    {
     93        return __('Attribute', 'griffinforms-form-builder');
     94    }
     95
     96    protected function mergeFallbackLabel()
     97    {
     98        return __('Fallback', 'griffinforms-form-builder');
     99    }
     100
     101
     102    protected function mergeTokenPreviewLabel()
     103    {
     104        return __('Token Preview', 'griffinforms-form-builder');
     105    }
     106
     107    protected function mergeInsertTokenLabel()
     108    {
     109        return __('Insert Token', 'griffinforms-form-builder');
     110    }
     111
     112    protected function selectForm()
     113    {
     114        return __('Select Form', 'griffinforms-form-builder');
     115    }
     116
     117    protected function selectField()
     118    {
     119        return __('Select Field', 'griffinforms-form-builder');
     120    }
     121
     122    protected function mergeAttrIdLabel()
     123    {
     124        return __('ID', 'griffinforms-form-builder');
     125    }
     126
     127    protected function mergeAttrNameLabel()
     128    {
     129        return __('Name', 'griffinforms-form-builder');
     130    }
     131
     132    protected function mergeAttrHeadingLabel()
     133    {
     134        return __('Heading', 'griffinforms-form-builder');
     135    }
     136
     137    protected function mergeAttrDescriptionLabel()
     138    {
     139        return __('Description', 'griffinforms-form-builder');
     140    }
     141
     142    protected function mergeAttrHelptextLabel()
     143    {
     144        return __('Helptext', 'griffinforms-form-builder');
     145    }
     146
     147    protected function mergeAttrLabelLabel()
     148    {
     149        return __('Label', 'griffinforms-form-builder');
     150    }
     151
     152    protected function mergeAttrValueLabel()
     153    {
     154        return __('Value', 'griffinforms-form-builder');
     155    }
     156
     157    protected function mergeAttrAnswerCountLabel()
     158    {
     159        return __('Number of Answers', 'griffinforms-form-builder');
     160    }
     161
     162    protected function mergeAttrPositionLabel()
     163    {
     164        return __('Position', 'griffinforms-form-builder');
     165    }
     166
     167    protected function mergeAttrTimeLabel()
     168    {
     169        return __('Time', 'griffinforms-form-builder');
     170    }
     171
     172    protected function mergeAttrDataLabel()
     173    {
     174        return __('Submission Data', 'griffinforms-form-builder');
     175    }
     176
     177    protected function mergeAttrAttachmentsCountLabel()
     178    {
     179        return __('Attachments Count', 'griffinforms-form-builder');
     180    }
     181
     182    protected function mergeAttrAttachmentsSizeLabel()
     183    {
     184        return __('Attachments Size', 'griffinforms-form-builder');
     185    }
     186
     187    protected function mergeAttrPaymentTotalLabel()
     188    {
     189        return __('Payment Total', 'griffinforms-form-builder');
     190    }
     191
     192    protected function mergeAttrPaymentBreakdownLabel()
     193    {
     194        return __('Payment Breakdown', 'griffinforms-form-builder');
     195    }
     196
     197    protected function mergeAttrPaymentStatusLabel()
     198    {
     199        return __('Payment Status', 'griffinforms-form-builder');
    54200    }
    55201   
  • griffinforms-form-builder/trunk/admin/secure/form.php

    r3299683 r3433300  
    132132    protected function secureAlertEmails($emails)
    133133    {
    134         $secured_emails_array = array_map('sanitize_email', $emails);
    135         return $secured_emails_array;
     134        if (!is_array($emails)) {
     135            return [];
     136        }
     137
     138        $secured = [];
     139        $keys = array_keys($emails);
     140        $is_assoc = $keys !== range(0, count($emails) - 1);
     141
     142        if ($is_assoc) {
     143            foreach ($emails as $email => $message_id) {
     144                $clean_email = sanitize_email($email);
     145                if (empty($clean_email)) {
     146                    continue;
     147                }
     148                $secured[$clean_email] = absint($message_id);
     149            }
     150            return $secured;
     151        }
     152
     153        foreach ($emails as $email) {
     154            $clean_email = sanitize_email($email);
     155            if (!empty($clean_email)) {
     156                $secured[$clean_email] = 0;
     157            }
     158        }
     159
     160        return $secured;
    136161    }
    137162   
  • griffinforms-form-builder/trunk/admin/sql/message.php

    r3299683 r3433300  
    55class Message extends \GriffinForms\Admin\Sql\Format
    66{
     7    public function getFormContextOptions(): array
     8    {
     9        global $wpdb;
     10
     11        $forms_table = $this->config->getTable('form');
     12        if (empty($forms_table)) {
     13            return [];
     14        }
     15
     16        $rows = $wpdb->get_results(
     17            $wpdb->prepare(
     18                'SELECT id, name, heading FROM %i ORDER BY id DESC',
     19                $forms_table
     20            )
     21        );
     22
     23        return is_array($rows) ? $rows : [];
     24    }
    725}
  • griffinforms-form-builder/trunk/config.php

    r3425584 r3433300  
    55class Config
    66{
    7     public const VERSION = '2.1.2.0';
     7    public const VERSION = '2.1.3.0';
    88    public const DB_VER = '1.0';
    99    public const PHP_REQUIRED = '8.2';
  • griffinforms-form-builder/trunk/frontend/actions/postsubmission.php

    r3425584 r3433300  
    2424
    2525    /**
     26     * Normalize alert email data into a map of email => message_id.
     27     *
     28     * @param array $alert_emails
     29     * @return array<string,int>
     30     */
     31    protected function normalizeAlertEmailMap(array $alert_emails): array
     32    {
     33        if (empty($alert_emails)) {
     34            return [];
     35        }
     36
     37        $keys = array_keys($alert_emails);
     38        $is_assoc = $keys !== range(0, count($alert_emails) - 1);
     39        $normalized = [];
     40        $form_id = $this->getFormId();
     41
     42        if ($is_assoc) {
     43            foreach ($alert_emails as $email => $message_id) {
     44                $clean_email = sanitize_email($email);
     45                if (empty($clean_email)) {
     46                    if ($form_id > 0) {
     47                        $this->log->add(
     48                            'warning',
     49                            'form',
     50                            $form_id,
     51                            get_current_user_id(),
     52                            'Admin alert email ignored: invalid recipient address in mapping.',
     53                            $this->getMailLogCategory()
     54                        );
     55                    }
     56                    continue;
     57                }
     58                $normalized[$clean_email] = absint($message_id);
     59            }
     60            return $normalized;
     61        }
     62
     63        if ($form_id > 0) {
     64            $this->log->add(
     65                'info',
     66                'form',
     67                $form_id,
     68                get_current_user_id(),
     69                'Legacy alert_emails format detected; normalized to message mapping with default template.',
     70                $this->getMailLogCategory()
     71            );
     72        }
     73
     74        foreach ($alert_emails as $email) {
     75            $clean_email = sanitize_email($email);
     76            if (!empty($clean_email)) {
     77                $normalized[$clean_email] = 0;
     78            } elseif ($form_id > 0) {
     79                $this->log->add(
     80                    'warning',
     81                    'form',
     82                    $form_id,
     83                    get_current_user_id(),
     84                    'Admin alert email ignored: invalid recipient address in legacy list.',
     85                    $this->getMailLogCategory()
     86                );
     87            }
     88        }
     89
     90        return $normalized;
     91    }
     92
     93    /**
    2694     * Processes and sends admin notification emails based on configured alert emails.
    2795     */
     
    2997    {
    3098        $this->data['form'] = $this->form;
    31         $email_alert = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($this->data);
    3299        $submission_id = isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0;
     100        $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine();
     101        $merge_context = \GriffinForms\Includes\Pipelines\SubmissionContextBuilder::getInstance()->build($submission_id);
     102        $merge_context['form_object'] = $this->form;
     103        $form_id = $this->getFormId();
     104        if ($submission_id > 0 && (empty($merge_context) || empty($merge_context['submission']))) {
     105            $this->log->add(
     106                'warning',
     107                'submission',
     108                $submission_id,
     109                get_current_user_id(),
     110                'Merge context missing for admin alert; tokens may not resolve.',
     111                $this->getMailLogCategory()
     112            );
     113        }
    33114        $mailer = new \GriffinForms\Includes\Mailer\MailHandler();
    34115        $mailer->setLogContext('submission', $submission_id);
    35116        $mailer->setSubmissionId($submission_id);
    36117
    37         $mailer->setEmailSubject($email_alert->getSubject());
    38         $mailer->setEmailContent($email_alert->getContent());
    39 
    40         foreach ($this->alert_emails as $email) {
    41             $mailer->setEmailRecipient($email);           
     118        $alert_map = $this->normalizeAlertEmailMap($this->alert_emails);
     119        $default_template = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($this->data);
     120        $default_subject = $merge_engine->render($default_template->getSubject(), $merge_context);
     121        $default_content = $merge_engine->render($default_template->getContent(), $merge_context);
     122
     123        foreach ($alert_map as $email => $message_id) {
     124            $subject = '';
     125            $content = '';
     126            $has_custom_message = false;
     127            if ($message_id > 0) {
     128                $message = $this->sql->getItemById($message_id, 'message');
     129                if (is_array($message)) {
     130                    $subject = $merge_engine->render($message['name'] ?? '', $merge_context);
     131                    $content = $merge_engine->render($message['content'] ?? '', $merge_context);
     132                    $has_custom_message = true;
     133                    if ($submission_id > 0 && trim($content) === '') {
     134                        $this->log->add(
     135                            'warning',
     136                            'submission',
     137                            $submission_id,
     138                            get_current_user_id(),
     139                            sprintf('Admin alert message %d has empty content; using default template.', $message_id),
     140                            $this->getMailLogCategory()
     141                        );
     142                    }
     143                } elseif ($submission_id > 0) {
     144                    $this->log->add(
     145                        'warning',
     146                        'submission',
     147                        $submission_id,
     148                        get_current_user_id(),
     149                        sprintf('Admin alert message %d no longer exists; using default template.', $message_id),
     150                        $this->getMailLogCategory()
     151                    );
     152                }
     153            }
     154
     155            if ($subject === '' && $content === '') {
     156                $subject = $default_subject;
     157                $content = $default_content;
     158            } elseif (trim($content) === '') {
     159                $content = $default_content;
     160                if ($subject === '') {
     161                    $subject = $default_subject;
     162                }
     163            } elseif ($has_custom_message && $subject === '') {
     164                $subject = $default_subject;
     165            }
     166
     167            $mailer->setEmailSubject($subject);
     168            $mailer->setEmailContent($content);
     169            $mailer->setEmailRecipient($email);
    42170            $mailer->sendMail();
    43171        }
     
    127255    public function processUserAutoresponderMappings($mappings)
    128256    {
     257        $submission_id = isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0;
     258        $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine();
     259        $merge_context = \GriffinForms\Includes\Pipelines\SubmissionContextBuilder::getInstance()->build($submission_id);
     260        $merge_context['form_object'] = $this->form;
     261        $form_id = $this->getFormId();
     262        if ($submission_id > 0 && (empty($merge_context) || empty($merge_context['submission']))) {
     263            $this->log->add(
     264                'warning',
     265                'submission',
     266                $submission_id,
     267                get_current_user_id(),
     268                'Merge context missing for autoresponder; tokens may not resolve.',
     269                $this->getMailLogCategory()
     270            );
     271        }
     272
    129273        foreach ($mappings as $mapping) {
    130274            $email_field_id = $mapping['email_field_id'];
     
    160304                }
    161305                if (!is_email($email_value)) {
     306                    if ($submission_id > 0) {
     307                        $this->log->add(
     308                            'warning',
     309                            'submission',
     310                            $submission_id,
     311                            get_current_user_id(),
     312                            sprintf(
     313                                'Autoresponder email ignored: mapped field ID %d contains an invalid email.',
     314                                $email_field_id
     315                            ),
     316                            $this->getMailLogCategory()
     317                        );
     318                    }
    162319                    $this->log->add(
    163320                        'error',
     
    177334
    178335                if (!$message) {
     336                    if ($submission_id > 0) {
     337                        $this->log->add(
     338                            'warning',
     339                            'submission',
     340                            $submission_id,
     341                            get_current_user_id(),
     342                            sprintf('Autoresponder message %d no longer exists; email not sent.', $message_id),
     343                            $this->getMailLogCategory()
     344                        );
     345                    }
    179346                    $this->log->add(
    180347                        'error',
     
    188355                }
    189356
    190                 $submission_id = isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0;
    191357                $mailer = new \GriffinForms\Includes\Mailer\MailHandler();
    192358                $mailer->setLogContext('submission', $submission_id);
    193359                $mailer->setSubmissionId($submission_id);
    194                 $mailer->setEmailSubject($message['name'] ?? '');
    195                 $mailer->setEmailContent($message['content'] ?? '');
     360                $subject = $merge_engine->render($message['name'] ?? '', $merge_context);
     361                $content = $merge_engine->render($message['content'] ?? '', $merge_context);
     362                if ($submission_id > 0 && trim($content) === '') {
     363                    $this->log->add(
     364                        'warning',
     365                        'submission',
     366                        $submission_id,
     367                        get_current_user_id(),
     368                        sprintf('Autoresponder message %d has empty content; email not sent.', $message_id),
     369                        $this->getMailLogCategory()
     370                    );
     371                    continue;
     372                }
     373                $mailer->setEmailSubject($subject);
     374                $mailer->setEmailContent($content);
    196375                $mailer->setEmailRecipient($email_value);
    197376
     
    513692
    514693        $email_alert = $this->isEmailAlert();
    515         $alert_emails = $email_alert ? array_map('sanitize_email', $this->alert_emails) : [];
     694        $alert_emails = $email_alert ? $this->normalizeAlertEmailMap($this->alert_emails) : [];
    516695
    517696        $has_autoresponder = $this->hasUserAutoresponderMappings();
     
    598777        return $this->mail_log_category;
    599778    }
     779
     780    private function getFormId(): int
     781    {
     782        if ($this->form && method_exists($this->form, 'getProp')) {
     783            $form_id = (int) ($this->form->getProp('id') ?? 0);
     784            if ($form_id > 0) {
     785                return $form_id;
     786            }
     787        }
     788
     789        return isset($this->data['form']) ? (int) $this->data['form'] : 0;
     790    }
    600791}
  • griffinforms-form-builder/trunk/griffinforms.php

    r3425584 r3433300  
    44 * Plugin URI:        https://griffinforms.com/
    55 * Description:       A powerful and flexible form builder for WordPress. Create multi-page forms with drag-and-drop ease, custom validations, and full submission management.
    6  * Version:           2.1.2.0
     6 * Version:           2.1.3.0
    77 * Requires at least: 6.6
    88 * Requires PHP:      8.2
  • griffinforms-form-builder/trunk/includes/pipelines/handlers/mailqueuehandler.php

    r3421663 r3433300  
    7474    private function queueAdminAlerts(array $recipients, int $submission_id, int $form_id): void
    7575    {
     76        if (empty($recipients)) {
     77            return;
     78        }
     79
     80        $keys = array_keys($recipients);
     81        $is_assoc = $keys !== range(0, count($recipients) - 1);
     82
     83        if ($is_assoc) {
     84            foreach ($recipients as $email => $message_id) {
     85                $email = sanitize_email($email);
     86                if (empty($email)) {
     87                    continue;
     88                }
     89                $this->repository->enqueue(
     90                    'mail_admin_alert',
     91                    'mailer',
     92                    $submission_id,
     93                    $form_id,
     94                    [
     95                        'recipient' => $email,
     96                        'message_id' => (int) $message_id,
     97                    ],
     98                    10
     99                );
     100            }
     101            return;
     102        }
     103
    76104        foreach ($recipients as $recipient) {
    77105            $email = sanitize_email($recipient);
  • griffinforms-form-builder/trunk/includes/pipelines/jobworker.php

    r3424123 r3433300  
    1515    /** @var \GriffinForms\Log */
    1616    protected $log;
     17
     18    /** @var string|null */
     19    protected $mail_log_category;
    1720
    1821    public function __construct()
     
    6770    {
    6871        $recipient = sanitize_email($payload['recipient'] ?? '');
     72        $message_id = isset($payload['message_id']) ? (int) $payload['message_id'] : 0;
     73        $form_id = isset($job['form_id']) ? (int) $job['form_id'] : 0;
     74        $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0;
    6975        if (empty($recipient)) {
    70             throw new \RuntimeException('Recipient missing for admin alert');
     76            $this->log->add(
     77                'warning',
     78                'submission',
     79                $submission_id,
     80                0,
     81                'Admin alert email ignored: invalid recipient address.',
     82                $this->getMailLogCategory()
     83            );
     84            return;
    7185        }
    7286
     
    7488        $form = $this->loadForm((int) ($job['form_id'] ?? 0));
    7589        $context['form_object'] = $form;
     90        if ($submission_id > 0 && (empty($context) || empty($context['submission']))) {
     91            $this->log->add(
     92                'warning',
     93                'submission',
     94                $submission_id,
     95                0,
     96                'Merge context missing for admin alert; tokens may not resolve.',
     97                $this->getMailLogCategory()
     98            );
     99        }
    76100
    77101        $template_data = [
     
    80104        ];
    81105
    82         $template = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($template_data);
     106        $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine();
     107        $subject = '';
     108        $content = '';
     109        $default_subject = '';
     110        $default_content = '';
     111
     112        if ($message_id > 0) {
     113            $message = $this->getMessageById($message_id);
     114            if ($message) {
     115                $subject = $merge_engine->render($message['name'] ?? '', $context);
     116                $content = $merge_engine->render($message['content'] ?? '', $context);
     117                if ($submission_id > 0 && trim($content) === '') {
     118                    $this->log->add(
     119                        'warning',
     120                        'submission',
     121                        $submission_id,
     122                        0,
     123                        sprintf('Admin alert message %d has empty content; using default template.', $message_id),
     124                        $this->getMailLogCategory()
     125                    );
     126                }
     127            } else {
     128                if ($submission_id > 0) {
     129                    $this->log->add(
     130                        'warning',
     131                        'submission',
     132                        $submission_id,
     133                        0,
     134                        sprintf('Admin alert message %d no longer exists; using default template.', $message_id),
     135                        $this->getMailLogCategory()
     136                    );
     137                }
     138            }
     139        }
     140
     141        if ($subject === '' || trim($content) === '') {
     142            $template = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($template_data);
     143            $default_subject = $merge_engine->render($template->getSubject(), $context);
     144            $default_content = $merge_engine->render($template->getContent(), $context);
     145        }
     146
     147        if ($subject === '') {
     148            $subject = $default_subject;
     149        }
     150
     151        if (trim($content) === '') {
     152            $content = $default_content;
     153        }
    83154
    84155        $mailer = $this->createMailer((int) ($job['submission_id'] ?? 0));
    85         $mailer->setEmailSubject($template->getSubject());
    86         $mailer->setEmailContent($template->getContent());
     156        $mailer->setEmailSubject($subject);
     157        $mailer->setEmailContent($content);
    87158        $mailer->setEmailRecipient($recipient);
    88159        $mailer->sendMail();
     
    93164        $recipient = sanitize_email($payload['recipient'] ?? '');
    94165        $message_id = isset($payload['message_id']) ? (int) $payload['message_id'] : 0;
     166        $form_id = isset($job['form_id']) ? (int) $job['form_id'] : 0;
     167        $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0;
    95168        if (empty($recipient) || $message_id <= 0) {
    96             throw new \RuntimeException('Invalid autoresponder payload');
     169            $this->log->add(
     170                'warning',
     171                'submission',
     172                $submission_id,
     173                0,
     174                'Autoresponder email ignored: invalid recipient or missing message mapping.',
     175                $this->getMailLogCategory()
     176            );
     177            return;
    97178        }
    98179
    99180        $message = $this->getMessageById($message_id);
    100181        if (!$message) {
    101             throw new \RuntimeException(sprintf('Message %d not found', $message_id));
     182            $this->log->add(
     183                'warning',
     184                'submission',
     185                $submission_id,
     186                0,
     187                sprintf('Autoresponder message %d no longer exists; email not sent.', $message_id),
     188                $this->getMailLogCategory()
     189            );
     190            return;
     191        }
     192
     193        $context = SubmissionContextBuilder::getInstance()->build((int) $job['submission_id']);
     194        $form = $this->loadForm((int) ($job['form_id'] ?? 0));
     195        $context['form_object'] = $form;
     196        if ($submission_id > 0 && (empty($context) || empty($context['submission']))) {
     197            $this->log->add(
     198                'warning',
     199                'submission',
     200                $submission_id,
     201                0,
     202                'Merge context missing for autoresponder; tokens may not resolve.',
     203                $this->getMailLogCategory()
     204            );
     205        }
     206        $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine();
     207        $subject = $merge_engine->render($message['name'] ?? '', $context);
     208        $content = $merge_engine->render($message['content'] ?? '', $context);
     209        if (trim($content) === '') {
     210            $this->log->add(
     211                'warning',
     212                'submission',
     213                $submission_id,
     214                0,
     215                sprintf('Autoresponder message %d has empty content; email not sent.', $message_id),
     216                $this->getMailLogCategory()
     217            );
     218            return;
    102219        }
    103220
    104221        $mailer = $this->createMailer((int) ($job['submission_id'] ?? 0));
    105         $mailer->setEmailSubject($message['name'] ?? '');
    106         $mailer->setEmailContent($message['content'] ?? '');
     222        $mailer->setEmailSubject($subject);
     223        $mailer->setEmailContent($content);
    107224        $mailer->setEmailRecipient($recipient);
    108225        $mailer->sendMail();
     
    169286        );
    170287    }
     288
     289    private function getMailLogCategory(): string
     290    {
     291        if (!empty($this->mail_log_category)) {
     292            return $this->mail_log_category;
     293        }
     294
     295        $registry = \GriffinForms\Includes\Integrations\Categories\Mailer::getInstance();
     296        $provider = $registry ? $registry->getActiveProviderId() : 'server_mail';
     297        if (empty($provider)) {
     298            $provider = 'server_mail';
     299        }
     300
     301        $this->mail_log_category = sprintf('Mail Integration (%s)', $provider);
     302        return $this->mail_log_category;
     303    }
    171304}
  • griffinforms-form-builder/trunk/installdata.php

    r3421663 r3433300  
    2222        $this->addDefaultLogSettings();
    2323        $this->addTemplatesToDb();
     24        $this->addEmailTemplatesToDb();
    2425        $this->addDefaultFormThemes();
    2526    }
     
    124125    }
    125126
     127    public function addEmailTemplatesToDb($force_reimport = false)
     128    {
     129        global $wpdb;
     130
     131        $table = $this->config->getTable('message');
     132        $templates_dir = $this->config->getRootPath() . 'admin/email_templates/';
     133
     134        if (!is_dir($templates_dir)) {
     135            return;
     136        }
     137
     138        $template_files = glob($templates_dir . '*.json');
     139        if (empty($template_files)) {
     140            return;
     141        }
     142
     143        $inserted = 0;
     144        $updated = 0;
     145        $skipped = 0;
     146
     147        foreach ($template_files as $file) {
     148            $template_data = json_decode(file_get_contents($file), true);
     149            if (!is_array($template_data)) {
     150                $this->log->add('error', 'message', 0, 0, 'Email template import failed: Invalid JSON in ' . basename($file), 'message');
     151                $skipped++;
     152                continue;
     153            }
     154
     155            $name = sanitize_text_field($template_data['name'] ?? '');
     156            if ($name === '') {
     157                $this->log->add('error', 'message', 0, 0, 'Email template import skipped: Missing name in ' . basename($file), 'message');
     158                $skipped++;
     159                continue;
     160            }
     161
     162            $heading = sanitize_text_field($template_data['heading'] ?? '');
     163            $description = sanitize_text_field($template_data['description'] ?? '');
     164            $content = $template_data['content'] ?? '';
     165            $content = is_string($content) ? wp_kses_post($content) : '';
     166
     167            $existing_template = $wpdb->get_row(
     168                $wpdb->prepare("SELECT id FROM %i WHERE name = %s", $table, $name)
     169            );
     170
     171            $data = array(
     172                'name'        => $name,
     173                'heading'     => $heading,
     174                'description' => $description,
     175                'content'     => $content,
     176                'last_editor' => 0,
     177                'last_edited' => current_time('mysql', true),
     178            );
     179
     180            if ($existing_template) {
     181                if ($force_reimport) {
     182                    $wpdb->update($table, $data, array('id' => $existing_template->id));
     183                    $updated++;
     184                } else {
     185                    $skipped++;
     186                }
     187                continue;
     188            }
     189
     190            $data['author'] = 0;
     191            $data['created'] = current_time('mysql', true);
     192
     193            $wpdb->insert($table, $data);
     194            $inserted++;
     195        }
     196
     197        $this->log->add('info', 'message', 0, 0, "Email template installation summary: Inserted $inserted, Updated $updated, Skipped $skipped", 'message');
     198    }
     199
    126200    /**
    127201     * Import default FormTheme templates from admin/themes directory on activation.
  • griffinforms-form-builder/trunk/readme.txt

    r3425584 r3433300  
    370370== Changelog ==
    371371
     372= 2.1.3.0 – 2026-01-06 =
     373* Feature: Admin alert emails can map each recipient to a message template with a default fallback.
     374* Feature: Message editor now includes a merge-token builder and JSON placeholder insertion.
     375* Feature: Merge tokens resolve at send time for admin alerts and autoresponders.
     376* Improvement: Added plain-text autoresponder and admin alert starter templates.
     377* Improvement: Mail-related logging now captures mapping and merge edge cases at the submission level.
     378
    372379= 2.1.2.0 – 2025-12-22 =
    373380* Fix: File upload queued list no longer shifts due to theme list padding.
     
    651658== Upgrade Notice ==
    652659
     660= 2.1.3.0 =
     661Admin alerts can now use per-recipient message templates with mail-merge placeholders, and the message editor includes a token builder. Includes new starter templates and improved mail logging. Recommended update.
     662
    653663= 2.1.2.0 =
    654664Polish release focused on layout and clarity: file upload lists now ignore theme padding, the builder summary stays out of the way during drag‑and‑drop, attachments headers align, and long settings history values are easier to read. Logging labels for mail and payment are clearer and more consistent. Recommended update.
Note: See TracChangeset for help on using the changeset viewer.