Repository navigation
feat: show Eventyay description restore action as a block editor notice - #372
TheVaibhaw wants to merge 8 commits into
Conversation
Once an imported event's description is edited on the site (Edit Event modal, WP admin or dashboard), manual and scheduled syncs no longer overwrite it. Other imported fields keep syncing. Each import that writes the description stores a fingerprint of the saved content and excerpt; a mismatch means a local edit. Line-ending changes from form round-trips do not count as edits. The WP admin editor and the Edit Event modal show a note for such events, mention when Eventyay has a newer description, and offer "Restore from Eventyay", which applies the current Eventyay text and makes the event follow Eventyay again. Fixes fossasia#354
Slash the description at the post and meta write boundaries so a backslash in an Eventyay description survives import, tracking and "Restore from Eventyay". Ask for confirmation before the Edit Event modal's restore link leaves the page, since unsaved form changes would be lost.
Events imported before description tracking existed have no fingerprint. On their first sync, compare the post with the incoming Eventyay description: if it differs and the post was modified after its last import, the difference was made on this site, so keep it and mark it as a local edit. If the post was not modified since that import, the difference came from Eventyay and the description follows it as before.
Use description-specific evidence for events imported before tracking: imports write the same text to content and excerpt (or only content), while local edit paths change just one of them. Two different non-empty fields mean a local edit; title or status changes no longer lock the description. When marking such an edit, keep Eventyay's text as the baseline only if the unedited field still matches it; otherwise leave the baseline empty so the note reports that Eventyay has a newer description.
An empty excerpt next to content that differs from Eventyay can be a content-only import whose text changed on Eventyay or a local content edit. Keep it instead of overwriting, and let the note offer to restore Eventyay's newer description. Follow Eventyay only when the content still matches it.
WordPress rejects updates that leave the title, content and excerpt all empty. Allow that for the restore update only, so an empty Eventyay description can be restored on an event whose title was cleared.
Reviewer's GuideThe PR introduces post-meta fingerprinting and synchronization logic to preserve locally edited Eventyay descriptions across imports, provides a secure restore path, and surfaces that action through a block-editor notice, classic-editor fallback, and frontend edit modal with confirmation handling and comprehensive tests. Sequence diagram for restoring an Eventyay description from the block editorsequenceDiagram
actor Admin
participant Editor as BlockEditor
participant Notice as wp_notices
participant Restore as admin_post
participant Sync as Wpfaevent_Event_Description_Sync
participant WordPress
Editor->>Notice: createNotice
Admin->>Notice: Restore from Eventyay
Notice->>Editor: isEditedPostDirty
Editor-->>Admin: window.confirm
Admin->>Restore: GET restoreUrl
Restore->>Sync: handle_restore_request
Sync->>WordPress: check_admin_referer
Sync->>WordPress: current_user_can
Sync->>Sync: restore
Sync->>WordPress: wp_update_post
Sync->>Sync: remember_import
Restore-->>Editor: wp_safe_redirect
Flow diagram for preserving locally edited Eventyay descriptionsflowchart TD
A[Eventyay import] --> B[keep_local_description]
B --> C{has_local_edit}
C -->|No| D[wp_update_post]
C -->|Yes| E[Keep post_content and post_excerpt]
D --> F[remember_import]
E --> F
F --> G[Store REMOTE_META]
F --> H[Store APPLIED_META and HASH_META when applied]
H --> I[get_note]
E --> I
I --> J[Block editor notice or classic editor fallback]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Description tracking and import preservation includes/helpers/class-wpfaevent-event-description-sync.php, admin/class-wpfaevent-eventyay-importer.php, includes/eventyay-importer/class-wpfaevent-event-repository.php, tests/unit/EventDescriptionSyncTest.php |
The synchronization helper detects local edits, records remote and applied descriptions, and normalizes text for fingerprint comparisons. Both import paths preserve detected local content and excerpt edits while continuing to sync other fields. Unit tests cover imports, legacy tracking, and description restoration. |
Admin editor notice and restore includes/class-wpfaevent.php, admin/partials/meta-boxes/class-wpfaevent-admin-event-metabox.php, admin/js/wpfaevent-description-notice.js, tests/unit/EventDescriptionNoticeTest.php |
The block editor receives a dismissible notice and restore action when a local edit is detected. The classic editor uses a meta box. The restore request validates the nonce, event type, and edit permission. |
Frontend event modal notice public/class-wpfaevent-public.php, public/css/wpfaevent-public.css, public/js/wpfaevent-events.js, public/partials/events/event-card.php, public/partials/events/event-modal.php |
Editable event cards include the description-sync note and restore URL when a note exists. The edit modal displays the note and asks for confirmation before following the restore link. |
Priority:
Estimated code review effort: 4 (Complex) | ~45 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
actor EditorUser
participant AdminEditor
participant DescriptionNoticeScript
participant AdminPost
participant DescriptionSync
participant WordPressPost
EditorUser->>AdminEditor: Select restore action
AdminEditor->>DescriptionNoticeScript: Run confirmation
DescriptionNoticeScript->>AdminPost: Navigate to restore URL
AdminPost->>DescriptionSync: Handle restore request
DescriptionSync->>WordPressPost: Update content and excerpt
AdminPost-->>AdminEditor: Redirect after restore
Possibly related PRs
- fossasia/WPFAevent#371: Both PRs affect event description imports and the relationship between
post_contentandpost_excerpt. - fossasia/WPFAevent#364: Both PRs affect description editing in the frontend event modal.
Suggested reviewers: mateeb-haider, ubayed-bin-sufian
Merge Risk: 
Minimal · up to 1f714
-
The description notice and restore flows have no identified issue that needs to be fixed before merge.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Title check
-
Explanation
The title names the main change: moving the Eventyay description restore action into a block editor notice. The diff adds a block editor notice script, a classic-editor fallback meta box, and the restore handler. The title is specific and matches the PR objectives. It does not mention the dependency on PR
#355or the test changes, which is expected for a short title.
Full details: Linked Issues check
-
Explanation
Issue [
#370] requires a discoverable native editor UI, the existing status text, the restore action, nonce and capability checks, replacement and unsaved-change warnings, and editor compatibility. The implementation uses acore/noticeswarning with an action inadmin/js/wpfaevent-description-notice.js.enqueue_description_notice()supplies the note, restore URL, and confirmation messages.Wpfaevent_Event_Description_Sync::handle_restore_request()retains nonce andedit_postchecks. The classic meta box remains when the block editor is not active.EventDescriptionNoticeTestcovers placement conditions, permissions, nonce data, and the classic fallback.
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 51 functions across 12 files. (1 skipped: 1 unsupported.)
-
Full details: Out of Scope Changes check
-
Explanation
Issue [
#370] limits scope to restore-action placement and usability. The whole PR also changes description synchronization inWpfaevent_Event_Description_Sync, importer and repository behavior, and adds frontend event-card and edit-modal restore UI with CSS and tests. These changes implement description preservation and frontend behavior, not the editor-notice placement. The issue explicitly excludes reopening that description behavior and related consistency work. The PR description identifies these commits as the#355dependency, but they are still present in the merge-base-to-head changes.Resolution
Merge or rebase the
#355dependency before merging this PR so the#370change contains only the block-editor notice, classic-editor fallback, related localization, and focused tests. Move unrelated synchronization and frontend restore changes to their owning PR or link them to a separate active issue.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Warning
Some tools did not complete. Review the errors below.
🔧 ESLint
-
If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
ESLint install timed out. The project may have too many dependencies for the sandbox.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
There was a problem hiding this comment.
Hey - I've found 6 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="includes/eventyay-importer/class-wpfaevent-event-repository.php" line_range="79-81" />
<code_context>
$created = false;
+ // Keep a description that was edited on this site; other fields still sync.
+ $keep_local_description = $existing_id && Wpfaevent_Event_Description_Sync::keep_local_description( $existing_id, $description );
+ if ( $keep_local_description ) {
+ unset( $post_data['post_content'], $post_data['post_excerpt'] );
+ }
+
</code_context>
<issue_to_address>
**Concurrent local edits get overwritten**
When a local description save lands after `keep_local_description()` checks the fingerprint but before the importer writes the post, the importer uses its earlier check and writes the fetched description over the user's save; `remember_import()` then fingerprints the imported text as the baseline, so the user's edit is lost and future syncs treat the post as unedited.
Prevent the importer from overwriting a description saved after its local-edit check.
Also at `admin/class-wpfaevent-eventyay-importer.php:3353-3380`.
</issue_to_address>
### Comment 2
<location path="includes/helpers/class-wpfaevent-event-description-sync.php" line_range="153-167" />
<code_context>
+ */
+ public static function restore( $post_id ) {
+ $post_id = absint( $post_id );
+ $description = wp_kses_post( (string) get_post_meta( $post_id, self::REMOTE_META, true ) );
+
+ // WordPress rejects updates that leave title, content and excerpt all empty;
+ // an empty Eventyay description must still restore on an untitled event.
+ $allow_empty = static function ( $maybe_empty, $postarr ) use ( $post_id ) {
+ return isset( $postarr['ID'] ) && absint( $postarr['ID'] ) === $post_id ? false : $maybe_empty;
+ };
+ add_filter( 'wp_insert_post_empty_content', $allow_empty, 10, 2 );
+ $result = wp_update_post(
+ wp_slash(
+ array(
+ 'ID' => $post_id,
+ 'post_content' => $description,
+ 'post_excerpt' => $description,
+ )
+ ),
+ true
</code_context>
<issue_to_address>
**Replayed restore overwrites later edits**
When a restore URL is retried or revisited after the first restore and an editor has since changed the description, the second request reads the same stored Eventyay description and `restore()` writes it over the post’s current content and excerpt, so a local edit made after the first restore is lost.
Make restore a one-time or state-checked operation, such as using a POST request with a per-action token and refusing to apply it if the post changed after the original restore.
Also at `includes/helpers/class-wpfaevent-event-description-sync.php:232-248`.
</issue_to_address>
### Comment 3
<location path="includes/helpers/class-wpfaevent-event-description-sync.php" line_range="135" />
<code_context>
+ $description = wp_kses_post( (string) $description );
+
+ // Meta writes unslash their input; slash it so backslashes in the text survive.
+ update_post_meta( $post_id, self::REMOTE_META, wp_slash( $description ) );
+
+ if ( $applied ) {
</code_context>
<issue_to_address>
**Failed metadata write clears descriptions**
When a database error rejects the `REMOTE_META` write during an import while the event post is saved, `remember_import()` ignores a failed write to `REMOTE_META`, then `restore()` reads the missing or stale value and writes it to both the content and excerpt; the local description is replaced with empty or outdated text.
Check that the remote-description metadata was stored successfully and refuse to restore when no valid stored description is available.
</issue_to_address>
### Comment 4
<location path="includes/eventyay-importer/class-wpfaevent-event-repository.php" line_range="105" />
<code_context>
);
}
+ Wpfaevent_Event_Description_Sync::remember_import( $saved_id, $description, ! $keep_local_description );
+
$start_datetime = $this->eventyay_event_datetime( $event, 'start' );
</code_context>
<issue_to_address>
**Imported descriptions become locally locked**
When an import updates the post but the request fails before `remember_import()` stores the matching fingerprint, the next import sees a fingerprint mismatch and treats the imported text as a local edit, preserving stale text instead of applying Eventyay's description.
Keep the post update and its matching fingerprint consistent when the request fails between those steps.
Also at `admin/class-wpfaevent-eventyay-importer.php:3348-3380`, `includes/helpers/class-wpfaevent-event-description-sync.php:135-139`.
</issue_to_address>
### Comment 5
<location path="public/partials/events/event-card.php" line_range="143" />
<code_context>
data-all-day="<?php echo esc_attr( $event_all_day ? '1' : '0' ); ?>"
+ <?php if ( '' !== $description_note ) : ?>
+ data-description-note="<?php echo esc_attr( $description_note ); ?>"
+ data-description-restore-url="<?php echo esc_url( Wpfaevent_Event_Description_Sync::get_restore_url( $event_id ) ); ?>"
+ <?php endif; ?>
data-time="<?php echo esc_attr( $event_time_value ); ?>">
</code_context>
<issue_to_address>
**Cached restore link expires**
When a page containing the frontend restore link is cached past the WordPress nonce lifetime, the cached page keeps the expired nonce in `data-description-restore-url`; `handle_restore_request()` then fails at `check_admin_referer()` before restoring, so the frontend restore action is unavailable until the page cache is refreshed.
Avoid serving cached restore URLs, or fetch a fresh nonce-protected URL when the restore action is used.
</issue_to_address>
### Comment 6
<location path="admin/partials/meta-boxes/class-wpfaevent-admin-event-metabox.php" line_range="383" />
<code_context>
+ array(
+ 'note' => $note,
+ // wp_localize_script() decodes the "&" that wp_nonce_url() adds, so the script gets a usable URL.
+ 'restoreUrl' => Wpfaevent_Event_Description_Sync::get_restore_url( $current_post->ID ),
+ 'i18n' => array(
+ 'restore' => __( 'Restore from Eventyay', 'wpfaevent' ),
</code_context>
<issue_to_address>
**Block editor restore URL breaks**
When the block-editor notice's restore action is clicked, `get_restore_url()` returns a display-escaped nonce URL, and `wp_localize_script()` serializes that value into JavaScript without decoding HTML entities. The `&` in the URL starts a fragment instead of separating query parameters, so the restore request lacks its post ID and nonce and fails.
Pass an unescaped URL to the localized script, for example by building it with `add_query_arg()` and adding the nonce without the display-escaping step.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 6 findings to address first, and using “Restore from Eventyay” replaces the event’s locally edited content and excerpt with the remote description; reverting the code would not recover that local text. The overwrite is limited to one event and requires an explicit confirmation, but the lost description may not be reproducible.
Blocking findings: includes/eventyay-importer/class-wpfaevent-event-repository.php:81, includes/helpers/class-wpfaevent-event-description-sync.php:167, includes/helpers/class-wpfaevent-event-description-sync.php:135, includes/eventyay-importer/class-wpfaevent-event-repository.php:105, public/partials/events/event-card.php:143, and 1 more
| $keep_local_description = $existing_id && Wpfaevent_Event_Description_Sync::keep_local_description( $existing_id, $description ); | ||
| if ( $keep_local_description ) { | ||
| unset( $post_data['post_content'], $post_data['post_excerpt'] ); |
There was a problem hiding this comment.
🟠 High · Concurrent local edits get overwritten
When a local description save lands after keep_local_description() checks the fingerprint but before the importer writes the post, the importer uses its earlier check and writes the fetched description over the user's save; remember_import() then fingerprints the imported text as the baseline, so the user's edit is lost and future syncs treat the post as unedited.
Prevent the importer from overwriting a description saved after its local-edit check.
Also at admin/class-wpfaevent-eventyay-importer.php:3353-3380.
Prompt for AI agents
In `includes/eventyay-importer/class-wpfaevent-event-repository.php` at lines 79-81:
**Concurrent local edits get overwritten**
When a local description save lands after `keep_local_description()` checks the fingerprint but before the importer writes the post, the importer uses its earlier check and writes the fetched description over the user's save; `remember_import()` then fingerprints the imported text as the baseline, so the user's edit is lost and future syncs treat the post as unedited.
Prevent the importer from overwriting a description saved after its local-edit check.
Also at `admin/class-wpfaevent-eventyay-importer.php:3353-3380`.| $description = wp_kses_post( (string) get_post_meta( $post_id, self::REMOTE_META, true ) ); | ||
|
|
||
| // WordPress rejects updates that leave title, content and excerpt all empty; | ||
| // an empty Eventyay description must still restore on an untitled event. | ||
| $allow_empty = static function ( $maybe_empty, $postarr ) use ( $post_id ) { | ||
| return isset( $postarr['ID'] ) && absint( $postarr['ID'] ) === $post_id ? false : $maybe_empty; | ||
| }; | ||
| add_filter( 'wp_insert_post_empty_content', $allow_empty, 10, 2 ); | ||
| $result = wp_update_post( | ||
| wp_slash( | ||
| array( | ||
| 'ID' => $post_id, | ||
| 'post_content' => $description, | ||
| 'post_excerpt' => $description, | ||
| ) |
There was a problem hiding this comment.
🟡 Medium · Replayed restore overwrites later edits
When a restore URL is retried or revisited after the first restore and an editor has since changed the description, the second request reads the same stored Eventyay description and restore() writes it over the post’s current content and excerpt, so a local edit made after the first restore is lost.
Make restore a one-time or state-checked operation, such as using a POST request with a per-action token and refusing to apply it if the post changed after the original restore.
Also at includes/helpers/class-wpfaevent-event-description-sync.php:232-248.
Prompt for AI agents
In `includes/helpers/class-wpfaevent-event-description-sync.php` at lines 153-167:
**Replayed restore overwrites later edits**
When a restore URL is retried or revisited after the first restore and an editor has since changed the description, the second request reads the same stored Eventyay description and `restore()` writes it over the post’s current content and excerpt, so a local edit made after the first restore is lost.
Make restore a one-time or state-checked operation, such as using a POST request with a per-action token and refusing to apply it if the post changed after the original restore.
Also at `includes/helpers/class-wpfaevent-event-description-sync.php:232-248`.| $description = wp_kses_post( (string) $description ); | ||
|
|
||
| // Meta writes unslash their input; slash it so backslashes in the text survive. | ||
| update_post_meta( $post_id, self::REMOTE_META, wp_slash( $description ) ); |
There was a problem hiding this comment.
🟡 Medium · Failed metadata write clears descriptions
When a database error rejects the REMOTE_META write during an import while the event post is saved, remember_import() ignores a failed write to REMOTE_META, then restore() reads the missing or stale value and writes it to both the content and excerpt; the local description is replaced with empty or outdated text.
Check that the remote-description metadata was stored successfully and refuse to restore when no valid stored description is available.
Prompt for AI agents
In `includes/helpers/class-wpfaevent-event-description-sync.php` at line 135:
**Failed metadata write clears descriptions**
When a database error rejects the `REMOTE_META` write during an import while the event post is saved, `remember_import()` ignores a failed write to `REMOTE_META`, then `restore()` reads the missing or stale value and writes it to both the content and excerpt; the local description is replaced with empty or outdated text.
Check that the remote-description metadata was stored successfully and refuse to restore when no valid stored description is available.| ); | ||
| } | ||
|
|
||
| Wpfaevent_Event_Description_Sync::remember_import( $saved_id, $description, ! $keep_local_description ); |
There was a problem hiding this comment.
🟡 Medium · Imported descriptions become locally locked
When an import updates the post but the request fails before remember_import() stores the matching fingerprint, the next import sees a fingerprint mismatch and treats the imported text as a local edit, preserving stale text instead of applying Eventyay's description.
Keep the post update and its matching fingerprint consistent when the request fails between those steps.
Also at admin/class-wpfaevent-eventyay-importer.php:3348-3380, includes/helpers/class-wpfaevent-event-description-sync.php:135-139.
Prompt for AI agents
In `includes/eventyay-importer/class-wpfaevent-event-repository.php` at line 105:
**Imported descriptions become locally locked**
When an import updates the post but the request fails before `remember_import()` stores the matching fingerprint, the next import sees a fingerprint mismatch and treats the imported text as a local edit, preserving stale text instead of applying Eventyay's description.
Keep the post update and its matching fingerprint consistent when the request fails between those steps.
Also at `admin/class-wpfaevent-eventyay-importer.php:3348-3380`, `includes/helpers/class-wpfaevent-event-description-sync.php:135-139`.| data-all-day="<?php echo esc_attr( $event_all_day ? '1' : '0' ); ?>" | ||
| <?php if ( '' !== $description_note ) : ?> | ||
| data-description-note="<?php echo esc_attr( $description_note ); ?>" | ||
| data-description-restore-url="<?php echo esc_url( Wpfaevent_Event_Description_Sync::get_restore_url( $event_id ) ); ?>" |
There was a problem hiding this comment.
🟡 Medium · Cached restore link expires
When a page containing the frontend restore link is cached past the WordPress nonce lifetime, the cached page keeps the expired nonce in data-description-restore-url; handle_restore_request() then fails at check_admin_referer() before restoring, so the frontend restore action is unavailable until the page cache is refreshed.
Avoid serving cached restore URLs, or fetch a fresh nonce-protected URL when the restore action is used.
Prompt for AI agents
In `public/partials/events/event-card.php` at line 143:
**Cached restore link expires**
When a page containing the frontend restore link is cached past the WordPress nonce lifetime, the cached page keeps the expired nonce in `data-description-restore-url`; `handle_restore_request()` then fails at `check_admin_referer()` before restoring, so the frontend restore action is unavailable until the page cache is refreshed.
Avoid serving cached restore URLs, or fetch a fresh nonce-protected URL when the restore action is used.| array( | ||
| 'note' => $note, | ||
| // wp_localize_script() decodes the "&" that wp_nonce_url() adds, so the script gets a usable URL. | ||
| 'restoreUrl' => Wpfaevent_Event_Description_Sync::get_restore_url( $current_post->ID ), |
There was a problem hiding this comment.
🟠 High · Block editor restore URL breaks
When the block-editor notice's restore action is clicked, get_restore_url() returns a display-escaped nonce URL, and wp_localize_script() serializes that value into JavaScript without decoding HTML entities. The & in the URL starts a fragment instead of separating query parameters, so the restore request lacks its post ID and nonce and fails.
Pass an unescaped URL to the localized script, for example by building it with add_query_arg() and adding the nonce without the display-escaping step.
Prompt for AI agents
In `admin/partials/meta-boxes/class-wpfaevent-admin-event-metabox.php` at line 383:
**Block editor restore URL breaks**
When the block-editor notice's restore action is clicked, `get_restore_url()` returns a display-escaped nonce URL, and `wp_localize_script()` serializes that value into JavaScript without decoding HTML entities. The `&` in the URL starts a fragment instead of separating query parameters, so the restore request lacks its post ID and nonce and fails.
Pass an unescaped URL to the localized script, for example by building it with `add_query_arg()` and adding the nonce without the display-escaping step.|
@TheVaibhaw, please address the issues flagged by Sourcery-AI, particularly the high-priority concerns regarding race conditions in concurrent local edits and the URL encoding error that is currently breaking the restore action in the block editor. |
Closes #370
Depends on #355 (still open). This branch is based on it, so #355's commits show here until it's merged; my change is the last commit.
What changed
In the block editor, the "Description edited on this site" note and the "Restore from Eventyay" button now appear as a WordPress editor notice at the top of the editing area, right above the description, so admins no longer have to look for it in the sidebar.
Why editor notices
As suggested in #355 (point 5), I first looked for an existing WordPress solution. Editor notices are what WordPress uses for its own messages and support an action button. Sidebar panels would keep the action in the sidebar.
Testing
composer testpasses (223 tests, 6 new inEventDescriptionNoticeTest.php).I used AI assistance for parts of this PR and reviewed the changes myself.
Screencast (Astra)
wpfaevent-370-astra-screencast.mp4
Summary by Sourcery
Preserve local event description edits across Eventyay syncs and surface a secure restore action wherever events are edited.
New Features:
Bug Fixes:
Enhancements:
Tests:
Summary by CodeRabbit
New Features
Tests