Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 72 additions & 5 deletions src/Integrations/GravityForms/GravityFormsAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ public static function register(): void

HelpAbility::registerAbility('gds/forms-list', [
'label' => 'List Forms',
'description' => 'List Gravity Forms. Delegates to GF REST API v2.',
'description' => 'List Gravity Forms. Defaults to active, non-trashed forms — pass `include_inactive` and/or `include_trashed` to widen the listing when the user is hunting for a form they "remember was here" (e.g. a disabled or trashed contact form).',
'category' => 'gds-content',
'input_schema' => self::getRestInputSchema('/gf/v2/forms'),
'input_schema' => [
'type' => 'object',
'properties' => [
'include_inactive' => ['type' => 'boolean', 'description' => 'Include inactive forms (status off). Default false.'],
'include_trashed' => ['type' => 'boolean', 'description' => 'Include trashed forms. Default false.'],
],
'additionalProperties' => true,
],
'output_schema' => ['type' => 'array', 'items' => ['type' => 'object', 'additionalProperties' => true]],
'permission_callback' => '__return_true',
'execute_callback' => [$instance, 'listForms'],
Expand Down Expand Up @@ -199,8 +206,20 @@ public function listForms(mixed $input = []): array|WP_Error
return new WP_Error('gf_not_available', 'Gravity Forms is not active.');
}

// GFAPI::get_forms(active, trash) — default returns only active, non-trashed.
$forms = \GFAPI::get_forms(true, false);
$input = (array) ($input ?? []);
// GFAPI::get_forms($active, $trash):
// $active = null → ignore active filter (returns active + inactive)
// $active = true → active only
// $trash = true → trashed only
// $trash = false → not trashed
// $trash = null → ignore trash filter (both)
$includeInactive = ! empty($input['include_inactive']);
$includeTrashed = ! empty($input['include_trashed']);

$active = $includeInactive ? null : true;
$trash = $includeTrashed ? null : false;

$forms = \GFAPI::get_forms($active, $trash);

$result = [];
foreach ($forms as $form) {
Expand Down Expand Up @@ -383,7 +402,55 @@ public function updateForm(mixed $input = []): array|WP_Error
// ($current was read before the merge, so capture is free).
$saved = json_decode(json_encode(\GFAPI::get_form($id)), true) ?: [];

return $this->reversible($saved, 'restore-form', ['id' => $id, 'form' => $current], "Revert form \"{$saved['title']}\" to its previous state");
// Make consecutive undo entries for the same form distinguishable —
// two updates on a single form would otherwise both read "Revert form
// 'X' to its previous state", leaving the user no way to tell which
// snapshot is which when undoing from the chat UI.
$deltaSummary = self::summarizeUpdateDelta($current, $saved);
$label = "Revert form \"{$saved['title']}\" to its previous state";
if ($deltaSummary !== '') {
$label .= " ({$deltaSummary})";
}

return $this->reversible($saved, 'restore-form', ['id' => $id, 'form' => $current], $label);
}

/**
* Short human-readable summary of what changed in a forms-update, used to
* disambiguate audit-log labels when the user updates the same form
* multiple times. Picks the most informative dimension(s) — field count
* changes lead; otherwise list which top-level keys differ.
*/
private static function summarizeUpdateDelta(array $before, array $after): string
{
$bits = [];

$bf = count(is_array($before['fields'] ?? null) ? $before['fields'] : []);
$af = count(is_array($after['fields'] ?? null) ? $after['fields'] : []);
if ($bf !== $af) {
$bits[] = "fields: {$bf} → {$af}";
}

$watched = ['title', 'description', 'button', 'confirmations', 'notifications'];
$changed = [];
foreach ($watched as $key) {
$b = $before[$key] ?? null;
$a = $after[$key] ?? null;
if ($b !== $a && json_encode($b) !== json_encode($a)) {
$changed[] = $key;
}
}
if ($changed) {
$bits[] = 'changed: '.implode(', ', $changed);
}

// Same field count + same watched keys: at least timestamp it so two
// identical-shape edits still produce different labels.
if (! $bits) {
$bits[] = 'saved '.gmdate('H:i:s').' UTC';
}

return implode(', ', $bits);
}

/**
Expand Down
17 changes: 17 additions & 0 deletions src/Undo/RestoreSnapshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,21 @@ private static function restoreForm(array $data): array|WP_Error
if (! $id || ! $form) {
return new WP_Error('restore_failed', 'Missing form snapshot.');
}
// GFAPI::update_form returns false on validation/save failure without
// raising a WP_Error — checking only is_wp_error() lets a silent
// failure flow through as "Undone", which is what surfaced the bug
// "undo said done but the field didn't change". Treat false as an
// explicit failure so we don't lie to the user.
$result = \GFAPI::update_form($form, $id);
if (is_wp_error($result)) {
return $result;
}
if ($result === false) {
return new WP_Error(
'restore_failed',
"Failed to restore form {$id}: GFAPI::update_form returned false (validation rejected or save failed).",
);
}

return ['restored' => 'form', 'id' => $id];
}
Expand Down Expand Up @@ -389,6 +400,12 @@ private static function restoreFeed(array $data): array|WP_Error
if (is_wp_error($result)) {
return $result;
}
if ($result === false) {
return new WP_Error(
'restore_failed',
"Failed to restore feed {$feedId}: GFAPI::update_feed returned false.",
);
}

// update_feed only restores meta; the form binding / active state /
// order are separate properties that the edit could also have changed.
Expand Down
Loading