WP Manifestindependent plugin directory
manifest / editor / gateway

Gateway

Second attempt at building a structured data plugin for WordPress with Laravel model integration.

by Gateway · github.com/caseyjmilne/gateway · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/caseyjmilne/gateway/archive/refs/heads/claude%2Fwordpress-datatable-block-pl580v.zip

Readme

Gateway

A WordPress plugin for custom Gutenberg blocks, starting with a Data Table block: a sortable, searchable grid of posts for any registered post type, powered by DataTables.

Requirements

  • WordPress 6.3+
  • PHP 8.2+ (raised from 7.4 by the vendored illuminate/database package -- see "Laravel Models (Illuminate/Eloquent)" below)
  • Node.js 18+ (build tooling only -- not required at runtime)

Getting started

npm install
npm run build

Compiled assets are committed to the repo (blocks/*/build/), so the plugin works immediately after activation even without running a build. Run npm run build after changing anything under blocks/*/src/.

Available scripts:

Script Purpose
npm run build Clean + production build of every block.
npm run start Watch mode for development.
npm run clean Remove all blocks/*/build output.
npm run format Format block source with wp-scripts format.
npm run lint:js Lint block JS.
npm run lint:css Lint block styles.

Architecture

gateway.php                     Plugin bootstrap: constants + boots Block_Loader, Column_Registry, Columns_REST_Controller, Facet_Query
includes/
  class-block-loader.php        Scans /blocks and register_block_type()'s every block found
  class-column-registry.php     Discovers columns (core fields + meta/ACF) for a post type; renders cell values
  class-columns-rest-controller.php  GET /gateway/v1/columns/<post_type> (the block editor's column/facet picker)
  class-facet-query.php         Applies validated facets to a WP_Query; distinct values for select/checkboxes facets
blocks/
  shared/                       Cross-block JS, NOT a block itself (no block.json -- webpack's glob skips it)
    datatable.js                Shared DataTables init/destroy helpers (jQuery + datatables.net-dt)
    dom.js                      Pure DOM helpers (getColumnIndexByKey) -- no jQuery/DataTables dependency
    wait-for-datatable.js       "Find the sibling table, wait for a DataTable, hide a native widget" (jQuery only)
    use-live-datatable-sync.js  Editor-only: keeps a live preview wired to the (possibly-replaced) sibling DataTable instance
    use-available-columns.js    Fetches the field list for a post type (shared by both blocks below)
  datatable/
    block.json                  Block metadata, attributes, providesContext, asset + render wiring
    render.php                  PHP render callback: renders the Facets position (a core/group Row, or whatever a site owner put there) then header/body/footer, in that order
    src/
      index.js                  Editor registration (editorScript)
      edit.js                   Editor UI: InspectorControls + one self-healing InnerBlocks skeleton
      save.js                   Persists InnerBlocks content only -- render.php doesn't use it
      view.js                   Front-end entry (viewScript): finds tables, inits DataTables
      style.scss                Shared styles (front end + editor)
      controls/
        post-type-control.js    Reusable "Post Type" SelectControl
        limit-control.js        "Limit" numeric field
        page-size-control.js    "Page Size" numeric field
        columns-panel.js        Renders the two below for the selected columns
        facets-panel.js         Renders the two below for the selected facets
        available-columns-list.js  Click-to-toggle field selection (shared by columns + facets)
        column-config-table.js  Drag-to-reorder + click-to-toggle-sortable table
        facet-config-table.js   Drag-to-reorder table; "Default" button opens a modal with compare/value
        facet-compare-options.js  Facet comparison operator list (=, !=, >, ...)
      hooks/
        use-reconcile-field-list.js   Drops selections no longer valid for the current post type
        use-required-inner-blocks.js React hook: name-based self-heal for the 4 required children (see below)
    build/                      Compiled output (generated, do not hand-edit)
  datatable-header/
    block.json                  Block metadata, parent restricting it to gateway/datatable
    render.php                  PHP render callback: echoes $content as-is (see "Header/body/footer" below)
    src/
      index.js                  Editor registration
      edit.js                   Editor UI: an InnerBlocks area restricted to gateway/datatable-page-size + gateway/datatable-search
      save.js                   Persists InnerBlocks content -- this block's own wrapper + classes
      view.js                   No front-end behavior -- exists only so style.scss gets built
      style.scss                Layout for the page-size/search children (flex row, spread to opposite ends)
    build/                      Compiled output (generated, do not hand-edit)
  datatable-body/
    block.json                  Block metadata, attributes (mirrored context, SSR-preview fallback only), usesContext
    render.php                  PHP render callback: the actual <table> -- headings + rows -- moved here from datatable/render.php
    src/
      index.js                  Editor registration
      edit.js                   Editor UI: mirrors context into attributes, then <ServerSideRender> + DataTables init
      save.js                   Persists nothing (leaf, dynamic block)
      hooks/
        use-datatable-init.js   React hook: (re)inits DataTables against an async-rendered container
    build/                      Compiled output (generated, do not hand-edit)
  datatable-footer/
    block.json                  Block metadata, parent restricting it to gateway/datatable
    render.php                  PHP render callback: echoes $content as-is (see "Header/body/footer" below)
    src/
      index.js                  Editor registration
      edit.js                   Editor UI: an InnerBlocks area restricted to gateway/pagination + gateway/datatable-results
      save.js                   Persists InnerBlocks content -- this block's own wrapper + classes
      view.js                   No front-end behavior -- exists only so style.scss gets built
      style.scss                Layout for the pagination/results children
    build/                      Compiled output (generated, do not hand-edit)
  facet/
    block.json                  Block metadata, ancestor + usesContext restricting it to gateway/datatable
    render.php                  PHP render callback: the input/select/checkboxes control
    src/
      index.js                  Editor registration
      edit.js                   Editor UI: pick a facet + a UI type, with validity warnings
      view.js                   Front-end entry: hooks the control into the sibling DataTable instance
      style.scss                Facet control styles
      controls/
        facet-key-control.js    "Facet" select, from the parent's configured facets (context)
        ui-type-control.js      "UI Type" select: input / select / checkboxes
        compare-control.js      "Compare" select (Contains/Equals), Input UI type only
    build/                      Compiled output (generated, do not hand-edit)
  datatable-page-size/
    block.json                  Block metadata, parent restricting it to gateway/datatable-header
    render.php                  PHP render callback: an empty, disabled <select> skeleton
    src/
      index.js                  Editor registration
      edit.js                   Editor UI: a *live* preview -- see "A live editor preview" below
      view.js                   Front-end entry: finds+waits for the table, hands off to attach-page-size.js
      attach-page-size.js       Shared option-populating/wiring logic, used by both edit.js and view.js
      style.scss                Page Size control styles
    build/                      Compiled output (generated, do not hand-edit)
  datatable-search/
    block.json                  Block metadata, parent restricting it to gateway/datatable-header
    render.php                  PHP render callback: a disabled search <input> skeleton
    src/
      index.js                  Editor registration
      edit.js                   Editor UI: a static preview (no settings to configure)
      view.js                   Front-end entry: drives the sibling DataTable's search() API
      style.scss                Search control styles
    build/                      Compiled output (generated, do not hand-edit)
  pagination/
    block.json                  Block metadata, parent restricting it to gateway/datatable-footer
    render.php                  PHP render callback: an empty Prev/Next + page-number skeleton
    src/
      index.js                  Editor registration
      edit.js                   Editor UI: a *live* preview -- see "Pagination and Results" below
      view.js                   Front-end entry: finds+waits for the table, hands off to attach-pagination.js
      attach-pagination.js      Shared button-building/wiring logic, used by both edit.js and view.js
      style.scss                Pagination control styles
    build/                      Compiled output (generated, do not hand-edit)
  datatable-results/
    block.json                  Block metadata, parent restricting it to gateway/datatable-footer
    render.php                  PHP render callback: an empty "Showing X to Y of Z entries" skeleton
    src/
      index.js                  Editor registration
      edit.js                   Editor UI: a *live* preview -- see "A live editor preview" below
      view.js                   Front-end entry: finds+waits for the table, hands off to attach-results.js
      attach-results.js         Shared info-text-building logic, used by both edit.js and view.js
      style.scss                Results text styles
    build/                      Compiled output (generated, do not hand-edit)

Block discovery / autoloading

Block_Loader::register_blocks() (hooked on init) globs every directory under /blocks and calls register_block_type() on each one that has a block.json. Nothing in PHP needs to change to add a new block -- drop a new blocks/<slug>/ directory in place (with its own block.json, src/, and optional render.php) and it's picked up automatically. webpack.config.js does the equivalent for the build: it globs blocks/*/block.json and wires up index.js/view.js entries per block automatically, compiling each into that block's own build/ directory.

A dedicated "Gateway" block category, for the four top-level blocks

Block_Loader::register_category() (on block_categories_all, alongside register_blocks()'s own init hook) registers one new inserter category, gateway/"Gateway" -- prepended ahead of core's own categories (Text/Media/Design/etc.), rather than appended after them, so a plugin whose main purpose IS these blocks gets surfaced first, not buried below groupings most of this plugin's own users have no reason to browse.

Only the four block.json files a site owner actually starts a layout with set their own category to "gateway": gateway/data-cards, gateway/data-display, gateway/datatable, gateway/single-record -- recognizable as exactly the four with neither a parent nor an ancestor restriction, the only ones ever offered from the top-level inserter to begin with. Every other block this plugin ships (every datatable-*/data-cards-* child, gateway/card-field-text/-number/ -image, gateway/related-items, gateway/facet/gateway/card-facet, gateway/pagination) deliberately keeps its existing "widgets" category untouched -- none of them are ever reachable from that top-level list regardless of which category they claim (a parent -restricted block only ever appears nested inside its one named parent; an ancestor-restricted one only once already inside a matching ancestor), so grouping them under "Gateway" too would just be dead weight in a category no one browses looking for them.

Verified with a new standalone PHP smoke test (register_category() returns core's own categories untouched plus exactly one new, well-formed entry, prepended ahead of them) plus a second pass reading every real blocks/*/block.json on disk and confirming, block by block, that a top-level block (no parent/ancestor) is in "gateway" and a child block (either restriction) is not -- alongside the full existing regression suite. No build step needed: category is plain block.json metadata register_block_type() reads directly, never something webpack/@wordpress/scripts compiles into a block's own build/ output.

Dynamic blocks, server-rendered

Every block in this plugin has no meaningful client-side save() markup (each returns null, or -- for the three InnerBlocks wrappers -- persists only the child delimiter comments). All markup -- on both the front end and in the block editor -- comes from each block's own render.php via block.json's "render" field:

  • Front end: WordPress calls render.php when the block is rendered in a post/page -- normal recursive block rendering, context and all.
  • Editor: the one block whose editor preview needs to show real, server-computed content -- gateway/datatable-body, the actual <table> -- renders <ServerSideRender block="gateway/datatable-body" />, which calls that same render.php through the block-renderer REST endpoint. This guarantees the editor preview is never out of sync with what actually ships to the front end. (gateway/datatable itself doesn't use <ServerSideRender> at all any more -- see "Header/body/footer" below for why that moved.)

Read the full README on GitHub →