Skip to content

feat(global-classes): Elementor v4 Global Classes write tools (CRUD + apply)#12

Merged
BenKalsky merged 11 commits into
mainfrom
feat/v4-global-classes-crud
Jul 6, 2026
Merged

feat(global-classes): Elementor v4 Global Classes write tools (CRUD + apply)#12
BenKalsky merged 11 commits into
mainfrom
feat/v4-global-classes-crud

Conversation

@BenKalsky

Copy link
Copy Markdown
Member

What

Adds 4 MCP tools that let an agent create, update, delete, and apply Elementor v4 Global Classes — closing the biggest remaining page-building gap vs Novamira Pro (which already ships headless global-classes CRUD). The plugin had a read-only list-global-classes; this adds the write half.

New tools (all manage_options-gated):

  • elementor-mcp/create-global-class — label + ergonomic styles map (+ optional responsive/state variants)
  • elementor-mcp/update-global-class — preserves id (bindings survive); replaces only the base variant, keeps others
  • elementor-mcp/delete-global-class
  • elementor-mcp/apply-global-class — binds a class to an atomic element; hard-rejects non-atomic targets with schema-in-error

How it works (from a 3-agent recon of Elementor internals + the Novamira reference)

  • Storage via the official API, never raw meta: Global_Classes_Repository::make()->put($items, $order), read-modify-write (a bad single write can't clobber the set).
  • Editor visibility — after every write, mirrors the kit's _elementor_global_classes_labels/_order meta into their _preview counterparts, else classes render as raw g- ids with no CSS in the editor canvas (the non-obvious bit Novamira also does).
  • Ids minted as g-<7hex> to match the Elementor editor format.
  • Props accept ergonomic scalars ({"color":"#111","padding":24}), wrapped to atomic {$$type,value} via the existing Atomic_Props primitives; when Style_Schema is present, unknown props / type mismatches are rejected with a schema-embedding WP_Error (self-correction without a round-trip).
  • files_manager->clear_cache() after each write for CSS regen. Everything guarded with class_exists/method_exists so it no-ops cleanly off a real Elementor.

Gating / defaults

Self-guards on class_exists(Global_Classes_Repository) (Elementor 4.x atomic). Enabled by default (no pro badge; delete badged destructive). Wired end-to-end: require + registrar + admin Tools catalog (atomic block) + version 1.14.0.

Tests

+16 (4 capability + 12 functional), suite 555 green. Functional tests stub an in-memory repository and assert create/update(base-variant-preserving)/delete/apply(non-atomic rejection)/permission gating.

Notes for review (flagged during implementation)

  • element_supports_classes is a heuristic (settings has classes key OR e--prefixed type OR has styles map) — no controls-introspection on the raw tree.
  • Keyword size values (e.g. width:"auto") wrap to a string $$type; if the schema expects size this could false-reject — worth confirming against a live v4 site.
  • put() assumes the ($items, $order) array signature (matches the Novamira reference call); revisit if real Elementor wants a context object.

🤖 Generated with Claude Code

Add four MCP write abilities for Elementor 4.0+ Global Classes in a new
Elementor_MCP_Global_Classes_Write_Abilities class, gated on manage_options
and self-guarded on the Global_Classes_Repository:

- create-global-class: mint g-<7hex> id, wrap ergonomic CSS-prop->value styles
  into atomic $$type props, validate against Style_Schema when present
  (schema-in-error on unknown props / type mismatch), build base + variant
  entries, put + mirror kit preview meta + clear cache.
- update-global-class: preserve id; replace only the base/desktop variant when
  styles given (other variants kept); replace matching breakpoint/state
  variants; patch label.
- delete-global-class: remove from items + order (no cascade on elements).
- apply-global-class: append class to an atomic element's settings.classes
  (dedupe no-op if present); reject non-atomic elements with compact
  schema-in-error.

Wiring: require in elementor-mcp.php, register after the read group in the
ability registrar, add a Global Classes group to the admin Tools catalog inside
the atomic block (no 'pro' badge -> enabled by default), bump to 1.14.0 across
header/constant/readme + CHANGELOG.

Tests: capability test (manage_options gate + 4-tool registry) and functional
test driving an in-memory Global_Classes_Repository stub (declared in bootstrap
so it wins over the capability suites' bare eval). 539 -> 555 tests, all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef745338b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return $base_props;
}

$variants = array( $this->build_variant( null, null, $base_props ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Store desktop breakpoint as a string

When create-global-class creates the base variant, this passes null for meta.breakpoint. Elementor's own style parser requires breakpoint to be present and string-valued (the built-in style variant builder uses desktop for the base variant), so classes created here can fail to round-trip through the Class Manager/REST parser and lose or reject their base styles once Elementor sanitizes or edits them. Use the desktop breakpoint string for the stored variant instead of null.

Useful? React with 👍 / 👎.

* CSS property names whose ergonomic value is a color.
*/
const COLOR_PROPS = array(
'color', 'background-color', 'border-color', 'border-top-color',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Translate background-color to background

background-color is advertised as an ergonomic CSS prop here, but Elementor 4's style schema represents backgrounds through the structured background prop rather than a top-level background-color. On sites where Style_Schema is loaded, a normal payload like styles: {"background-color":"#fff"} is rejected as invalid_styles; without validation it saves a prop the renderer ignores, so agents cannot author background-color classes through the new tool.

Useful? React with 👍 / 👎.

…-color (Codex P2)

- Base variant breakpoint stored as the string 'desktop' instead of null.
  Elementor's atomic style parser expects a string breakpoint (the editor's
  own variant builder + this fork's create_local_class use 'desktop'); a null
  base can fail to round-trip through the Class Manager/REST parser. Reads
  normalize either way, and update's base-variant matching still resolves it
  via norm_breakpoint.
- Removed 'background-color' from the ergonomic COLOR_PROPS: Elementor v4
  represents backgrounds through the structured 'background' prop, not a
  top-level 'background-color', so it would be rejected by Style_Schema or
  silently ignored by the renderer. Updated tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16c791a6f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

private function put( array $items, array $order ) {
$repo = self::REPOSITORY;
try {
$repo::make()->put( $items, array_values( $order ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the real repository write contract

On current Elementor 4.x, Global_Classes_Repository::put() is a per-class API (put(string $id, array $value)), not a bulk put($items, $order) call. With this call shape, every create/update/delete path reaches this helper and the first argument is an array, so real sites return write_failed from the caught TypeError and never persist the global class change; the test bootstrap masks this by stubbing a different bulk signature.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified false positive. Elementor core Global_Classes_Repository::put() is bulk, not per-class:

public function put( array $items, array $order )

(elementor/elementor modules/global-classes/global-classes-repository.php, current main). The shipping Novamira Pro 1.6.0 also calls $repo->put($items, $order) at all three write sites. The (string $id, array $value) signature does not exist on this class. Keeping the bulk call.

Comment on lines +781 to +782
'word-spacing', 'border-radius', 'border-width', 'gap', 'row-gap',
'column-gap', 'top', 'right', 'bottom', 'left', 'flex-basis',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wrap v4 gap as layout-direction

When a user creates or updates a global class with styles: {"gap": 24}, this list routes gap through wrap_size(), producing a plain $$type: size. The existing v4 atomic builder documents and tests that gap must be layout-direction with row/column sizes (includes/class-atomic-styles.php:92-107, tests/unit/regression/AtomicV4SchemaTest.php:83-89), and flat gap/row-gap/column-gap are dropped, so global classes using one of the most common flex properties will be saved but not render correctly.

Useful? React with 👍 / 👎.

On Elementor v4 atomic, flex gap is the structured layout-direction prop
{row,column} (each a Size); a flat gap Size is dropped by the renderer
(class-atomic-styles.php). Removed gap/row-gap/column-gap from the ergonomic
SIZE_PROPS so they aren't silently mis-wrapped — a gap passed anyway now
falls through to a string $$type and is rejected by Style_Schema (honest
failure). Structured gap support is a follow-up.

(Also: the P1 'per-class put()' finding was a verified false positive —
Elementor core Global_Classes_Repository::put(array $items, array $order)
is bulk, matching this code and the shipping Novamira Pro 1.6.0; replied on
the thread with the source.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb1cbc5be8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1036 to +1039
$pairs = array(
'_elementor_global_classes_labels' => '_elementor_global_classes_labels_preview',
'_elementor_global_classes_order' => '_elementor_global_classes_order_preview',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror the 4.0 preview class store

On Elementor 4.0 installs that still use the single _elementor_global_classes store plus _elementor_global_classes_preview, CRUD writes update the default frontend repository but this sync only copies the newer labels/order meta keys. In that context those keys do not contain the class definitions, so a class created or updated through MCP is missing from the editor preview context and applied elements render with stale/no class styling until something else refreshes the preview store; mirror _elementor_global_classes as well or write through the repository's preview context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified false positive against Elementor core. Global_Classes_Repository::put() (default frontend context) already synchronizes the preview store itself: put_to_posts() clears the preview metadata for touched items and calls set_preview(true) on the order object, so after a write the touched classes have no stale preview override and the editor reads the current frontend definition (META_KEY_FRONTEND=_elementor_global_classes, META_KEY_PREVIEW=_elementor_global_classes_preview, Has_Preview_Context). Manually copying _elementor_global_classes_elementor_global_classes_preview would fight that touched-item clearing. The labels/order preview mirror we do keep matches the shipping Novamira Pro 1.6.0 and is belt-and-suspenders for the editor list; the definitions need no manual mirror. Not changing.

@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb1cbc5be8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +840 to +841
// Fallback: plain string prop (font-weight, text-align, display, ...).
return Elementor_MCP_Atomic_Props::string( (string) $value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wrap numeric style props before validation

When a global class uses numeric-only CSS properties such as z-index, order, or column-count, those names are not in COLOR_PROPS or SIZE_PROPS, so they hit this fallback and are cast to $$type: string. On Elementor installs where Style_Schema is loaded, validate_against_schema() rejects these otherwise valid styles as type mismatches; without the schema they are persisted with the wrong atomic type, so common numeric class styles cannot be authored through create-global-class or update-global-class.

Useful? React with 👍 / 👎.

Props like z-index / order / flex-grow / column-count are the atomic number
prop type, but fell through to the string fallback — rejected by Style_Schema
(type mismatch) or persisted with the wrong type. Added NUMBER_PROPS + a
number-wrapping branch (ints stay ints; numeric strings coerce; non-numeric
falls through to string for an honest schema mismatch). +1 test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7cdfe1bdcd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

private function put( array $items, array $order ) {
$repo = self::REPOSITORY;
try {
$repo::make()->put( $items, array_values( $order ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Write Global Classes into the preview context

When a class is created/updated/deleted from the editor, this only calls put() on the repository's default (published/frontend) context; Elementor keeps Global Classes in separate frontend and preview contexts (set_preview(true) for draft/preview, per the Elementor data-structure docs). The later sync_preview_context() copies only kit labels/order, not the per-class preview JSON/post lookup, so the editor canvas can show stale or missing CSS for newly written classes until something else syncs the preview context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Underlying observation is fair, but declining the "write both contexts" prescription — it introduces a regression. Rationale:

  • Clobber risk: the preview context (_elementor_global_class_data_preview) is the editor draft. Writing it from a headless MCP call would overwrite a users unpublished in-editor Global Class changes. Frontend-context writes respect the publish boundary.
  • Precedent: the shipping Novamira Pro 1.6.0 writes frontend-only + mirrors labels/order (exactly what we do). If frontend-only broke editor visibility, their flagship design-system feature would be broken.
  • Behavior: changes are live on the published frontend immediately and load in the editor on next open; an already-open editor needs a refresh — standard for any external edit. The Elementor docs explicitly do not document a reconciliation requirement either way.

Documented this as a deliberate context decision in the class docblock. Keeping frontend-context writes.

…decision (Codex P2)

Writes use the repository default (frontend/published) context, not the
editor preview context — matching the shipping Novamira Pro reference and
respecting the publish boundary (a preview write would clobber a user's
unpublished in-editor Global Class draft). Changes are live on the frontend
immediately and load in the editor on next open. Documented in the class
docblock; declined the 'write both contexts' suggestion as a draft-clobber
regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f87a77ec50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

try {
$ctx = $repo::make()->all();

$items = method_exists( $ctx, 'get_items' ) ? $ctx->get_items() : ( is_array( $ctx ) && isset( $ctx['items'] ) ? $ctx['items'] : $ctx );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard array contexts before method_exists

When a repository implementation returns the array shape that this fallback is trying to support from all(), this calls method_exists($ctx, ...) before checking is_array($ctx). On PHP 8+, method_exists() throws a TypeError for arrays, which is caught as read_failed, so create/update/delete fail instead of using the array fallback; guard with is_object($ctx)/is_string($ctx) first, and apply the same pattern to the get_order check below.

Useful? React with 👍 / 👎.

…te (Codex P2)

On PHP 8, method_exists() TypeErrors on an array argument (param type
object|string), so the array-shape fallback in read_state() was dead — it
threw before reaching the is_array branch and was swallowed as read_failed.
Guard both the get_items and get_order checks with is_object($ctx) first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b11d1e405e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

'label' => $label,
'variants' => $variants,
);
$order[] = $id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the Elementor class cap before creating

When the site is already at Elementor's Global Classes limit, this path still appends another id and writes directly through the repository, bypassing the editor REST API's MAX_ITEMS validation. That can leave the class set above the supported limit, after which the Elementor editor's own global-classes save calls fail until a class is removed; mirror the limit check before adding to $order.

Useful? React with 👍 / 👎.

private function put( array $items, array $order ) {
$repo = self::REPOSITORY;
try {
$repo::make()->put( $items, array_values( $order ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve preview drafts when writing classes

Each create/update/delete call passes the complete map from read_state() into Global_Classes_Repository::put(). In Elementor's frontend context, put() treats every existing id in that full map as modified and clears preview data for modified classes, so creating or deleting one MCP class can discard unrelated unpublished Class Manager edits that are still in preview; use a change-aware write path or limit the mutation to touched ids.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified false positive against Elementor core. Global_Classes_Repository::put() computes a diff, not a wholesale touch:

$changes = [
  "added"    => array_values( array_diff( $new_ids, $current_ids ) ),
  "deleted"  => $deleted_class_ids,
  "modified" => array_values( array_intersect( $new_ids, $current_ids ) ),
];

and preview clearing is applied only to added ∪ updated ∪ deleted. Unchanged ids in the full map are excluded from the diff and their preview drafts are left intact. So passing the complete map (as this code and the shipping Novamira Pro 1.6.0 both do) does not discard unrelated unpublished Class Manager edits. Not changing.

…e (Codex P2)

Elementor V4 caps Global Classes (100 currently; 50 in alpha, GitHub #35864)
at its REST/save layer, not in the repository's put() — so a direct write can
push past it and then wedge the editor's own Class Manager saves until a class
is removed. create-global-class now refuses up front when the count is at the
cap, with a clear error. The limit reads Elementor's own constant when a build
exposes one, defaults to 100, and is filterable
(elementor_mcp_global_classes_limit) for forward-compat. +1 test.

(Also: the paired 'preview drafts clobbered' P2 was a verified false positive
— Elementor's put() diffs added/updated/deleted and clears preview only for
those, leaving unchanged classes' preview drafts intact; replied with source.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57cf6c7f5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* rejected by Style_Schema / persisted with the wrong type.
*/
const NUMBER_PROPS = array(
'z-index', 'order', 'flex-grow', 'flex-shrink', 'opacity',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat opacity as a size value

When a caller creates or updates a class with an opacity style, this list makes wrap_value() persist it as $$type:number. Elementor 4's atomic style schema treats opacity as a Size/percent prop, so schema validation rejects the style when available, and the renderer can drop it when validation is absent; the new tools therefore cannot author a common opacity rule.

Useful? React with 👍 / 👎.

);
foreach ( $pairs as $source => $target ) {
$value = get_post_meta( $kit_id, $source, true );
update_post_meta( $kit_id, $target, $value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't overwrite preview global-class drafts

Every create/update/delete calls this helper, and this write unconditionally replaces the kit's _preview labels/order with the published values. If a user has unpublished Global Class edits in an open Elementor editor, running any of these MCP writes will clobber those preview drafts even though the repository write itself targets the frontend context.

Useful? React with 👍 / 👎.

…ew mirror (Codex P2 x2)

- opacity: its atomic prop type (Size/percent vs plain number) is version-
  dependent and not confidently determinable, so it was mis-typed as number.
  Removed from the ergonomic lists — passed anyway it falls through to string
  and the schema surfaces an honest mismatch, rather than persisting a wrong
  type the renderer drops.
- preview mirror: sync_preview_context() unconditionally copied the kit's
  frontend labels/order into the _preview meta after every write. That both
  (a) clobbered the preview drafts of unrelated classes a user was editing,
  and (b) is redundant/counter-productive — Elementor's own put() already
  reconciles the preview context for touched ids by CLEARING their preview
  overrides so they fall back to the frontend overlay; re-populating preview
  from frontend defeats that fallback for other classes. Removed the mirror
  (and the now-dead active_kit_id helper); rely on put()'s own preview sync.
  Documented the reasoning in the class docblock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0744f60a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

private function put( array $items, array $order ) {
$repo = self::REPOSITORY;
try {
$repo::make()->put( $items, array_values( $order ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Limit repository writes to touched classes

When the site already has global classes and an editor has unpublished preview changes to any unrelated class, this helper passes the full $items map from read_state() into Elementor's repository for every create/update/delete. Elementor's current Global_Classes_Repository::put() derives modified as every existing id present in that map and clears preview overrides for all modified ids on frontend writes, so a one-class MCP change can discard unrelated Class Manager drafts and rewrite every existing class. Please use the repository's touched-item change API where available (or otherwise write only the actual mutation) instead of treating the whole set as modified.

Useful? React with 👍 / 👎.

…not bulk put() (Codex P1)

Confirmed against Elementor core source (modules/global-classes/
global-classes-repository.php): put() derives modified = array_intersect(
new_ids, current_ids) — every existing id in the passed map, NOT a value diff
— and on frontend writes runs bulk_clear_preview_meta(to_update) +
clear_preview_labels_for_ids(...) over all of them. So passing the full
read-modify-write map on any create/update/delete discarded a user's
unpublished in-editor Global Class drafts for unrelated classes and rewrote
every class.

Switched each mutation to Elementor's touched-item API apply_changes(
$touched, $changes, $order), naming only the added/modified/deleted id so
only that class's preview override is reconciled; other classes' drafts are
left intact. Falls back to put() only on older builds lacking apply_changes()
(method_exists guard). Added a stub apply_changes() + a test asserting the
touched set and that unrelated classes are preserved across create/update/
delete.

(This reverses my earlier incorrect 'false positive' call on the same
finding — the bulk-put preview clobber is real; verified from the raw
source this time, not a summary.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7579fd5a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if ( method_exists( $r, 'apply_changes' ) ) {
$r->apply_changes( $touched, $changes, array_values( $order ) );
} else {
$r->put( $items, array_values( $order ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the older repository put() signature

When apply_changes() is absent, this fallback calls put() as a bulk write with ($items, $order), but Elementor 4 builds that only expose put() use the per-class API shape (put( string $id, array $value )). On those supported 4.x installs, create/update/delete will hit the fallback and return write_failed instead of mutating global classes, so the compatibility path needs to call the repository's older per-item/delete/order methods or explicitly fail availability.

Useful? React with 👍 / 👎.

… API exists (Codex P2)

Verified the full history of Elementor's global-classes-repository.php:
- 2024-11: get/delete/patch (no put)
- 2024-11-28 … 2025-01-14: put( string $id, array $value )  [per-class]
- 2025-02+: put( array $items, array $order )               [bulk]
- 2026-05+: apply_changes( $touched, $changes, $order )

apply_change() supports only the touched-item apply_changes() and the bulk
put(array, ...) fallback. On the 2024-11…2025-01 per-class-put() window (and
the earlier get/delete/patch builds) the bulk-put fallback would TypeError at
call time. is_available() now requires apply_changes() OR a reflection-verified
bulk put(array, ...) first parameter, so the write tools don't register on
those ancient alphas instead of failing mid-write. (Confirms Codex's earlier
per-class-put() observation, which I had wrongly dismissed as a flat false
positive after checking only current main.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 8766dfe4de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BenKalsky BenKalsky merged commit e9fc3be into main Jul 6, 2026
7 checks passed
@BenKalsky BenKalsky deleted the feat/v4-global-classes-crud branch July 6, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant