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
1 change: 1 addition & 0 deletions allterrain-forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
require_once ATF_DIR . 'includes/assets.php';
require_once ATF_DIR . 'includes/admin-page.php';
require_once ATF_DIR . 'includes/openstation.php';
require_once ATF_DIR . 'includes/openstation-explorer.php';
require_once ATF_DIR . 'includes/privacy.php';

register_activation_hook( __FILE__, 'atf_activate' );
Expand Down
26 changes: 26 additions & 0 deletions assets/js/dock.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,32 @@ var allTerrainFormsDock = function(exports) {
if (!boot()) {
document.addEventListener("os-init", () => void boot(), { once: true });
}
function registerExplorerActions() {
const hooks = window.wp?.hooks;
if (!hooks?.addFilter) {
return;
}
const windows = {
"allterrain-forms/open-builder": BUILDER,
"allterrain-forms/open-entries": ENTRIES,
"allterrain-forms/open-analytics": ANALYTICS
};
hooks.addFilter(
"os.my-wordpress.preview-actions",
"allterrain-forms/explorer",
(actions) => actions.map((action) => {
const id = action.id ?? "";
if (!windows[id]) {
return action;
}
return {
...action,
onSelect: () => open(windows[id])
};
})
);
}
registerExplorerActions();
exports.submenuFor = submenuFor;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
return exports;
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dock.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 115 additions & 0 deletions docs/wp-explorer-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# WP Explorer — what this plugin needed, and what it still needs

The companion to [`openstation-requests.md`](openstation-requests.md), for the
shell's **My WordPress** window (WP Explorer). It records what the Explorer's
extension surface let us ship, and — for the Explorer's developer — the asks
that would let a forms plugin appear there the way it deserves to.

The vision being asked for, in one sentence: **each form as a folder, with its
preview, its submissions and its report inside** — the way a project reads in
a file manager.

## What the current surface allowed (shipped)

| Surface | What we did with it |
|---|---|
| `openstation_my_wordpress_entities` | Added a **Forms** section at the root — necessary because every type this plugin registers is `show_ui => false` (it has its own windows), and the Explorer only auto-lists `show_ui` types. |
| `rest_prepare_atf_form` (core) | Tiles carry vitals — *12 questions · 493 entries · Clean* — instead of a bare title. (Needed `excerpt` added to the type's `supports`; without it core REST omits the field entirely.) |
| `openstation_my_wordpress_preview_actions` | Three capability-gated actions declared — **Open in the form builder**, **View entries**, **Open the report** — with `onSelect` wired via `os.my-wordpress.preview-actions` in `dock.ts`. Registered correctly on both sides, but see ask 0: the pane never renders them. |

## The asks

### 0. Preview actions never render for a plugin-added section *(bug, with repro)*

Descriptors survive `openstation_my_wordpress_preview_actions` (verified:
`wp eval 'apply_filters(...)'` lists all three with `sections: ['atf-forms']`),
and the JS filter attaches `onSelect` (verified:
`wp.hooks.applyFilters('os.my-wordpress.preview-actions', …)` returns them
wired). Yet the right pane and the tile context menu render only the shell's
defaults — *Explore details / Open in editor / Navigate into / Edit… / Move to
Trash*. Either plugin-added sections aren't matched against `sections`, or the
pane reads a config snapshot taken before plugins filter. Repro: install
AllTerrain Forms, open the Forms section, select a tile.

### 0b. The default "Open in editor" must be overridable per section *(bug)*

On a section whose type has no post-editor screen (`show_ui => false`), **Open
in editor** and **Edit…** try the classic editor URL and fail. Ask: let a
section descriptor declare its own `editAction` (or honor a preview action
flagged as the editor replacement), so a type edited by a native window opens
in it.

### 0c. What the right pane should let a plugin say *(from user testing)*

Three things a person selecting a form immediately wanted, none expressible:

- **A scaled-down live preview** — the section's posts have a front-end render;
a ~50% iframe of it in the pane beats a title and a date. Ask: a section
field naming a preview URL per post (we already serve one), rendered scaled
in the pane.
- **Stat cards above the preview** — submissions, conversion, unread. Ask: a
descriptor for pane stat tiles fed by REST fields the plugin already exposes.
- The action buttons from ask 0 under them.

### 1. Per-post children — a post as a folder *(the big one)*

The Explorer's model is flat: sections of posts. A form is a *container* — its
submissions belong inside it. Ask: let a section declare a `children` resolver
(REST path template per post, e.g. `.../forms/{id}/entries`), so a tile can
open as a folder of plugin-provided items instead of a preview pane. Everything
below gets better if this lands. (The context menu already offers **Navigate
into** backed by `openstation_my_wordpress_attached_media` — so the window can
render a post's children today; what's missing is letting a plugin supply
children that are not attachments.)

### 2. Virtual items inside a folder

With children, a form's folder wants non-post items too: a **Preview** node
that opens the front-end render, an **Export.csv** node that streams the
export, a **Report** node. Ask: a child-item descriptor with `kind:
'action' | 'file' | 'window'` and an `onOpen` contract, so a folder can hold
things that are not posts.

### 3. Capability-scoped bridged sections for non-REST types

Entries are `show_in_rest => false` **by privacy design** — their values are
whatever the form asked for. The shell's own bridge controller
(`desktop-mode/v1/post-type/{type}`) exists, but only for types the shell
itself lists. Ask: a supported way to request a bridged section for a hidden
type, with a declared capability (`atf_read_entries`) enforced by the bridge —
then submissions could be listed without making them world-REST-readable.

### 4. Custom post statuses in section queries

Entry statuses are real post statuses (`atf-unread`, `atf-read`, `atf-spam`),
registered `exclude_from_search`. A `wp/v2`-backed section lists `publish`
only. Ask: let a section descriptor declare `statuses: [...]` that the window
passes through to its list requests.

### 5. Drag payloads on tiles

This plugin's Entries window emits `allterrain-forms/entry` as a public drag
payload (an entry dragged onto AllTerrain Work becomes a task). Explorer tiles
should be able to carry the same: a section-level `dragPayload( post )`
contract handing `wp.os.dragManager` a typed payload, so anything listed in
the Explorer is draggable everywhere its window equivalent already is.

### 6. The selected entity as an argument to preview actions

`onSelect` on a preview action receives nothing. To deep-link — *open the
builder on this form*, not just open the builder — the click needs the entity
it was clicked on. Ask: call `onSelect( action, entity )`, and document it.
(Half of this is ours: the builder should accept an `openWindow` param naming
the form. Tracked on our side.)

### 7. Count badges on sections

The dock tile shows an unread-entries badge; the Explorer section should be
able to as well. Ask: an optional `badge` callback or REST field on the
section descriptor.

## Status

Ask 0 is a bug with a repro; 0b breaks a shipped default on our types; 0c is what user testing asked for within a day. Asks 1–2 are the vision; 3–4 unlock submissions; 5–7 are polish. None of them
required patching the shell to *want* — every shipped piece above went through
existing filters, which is the discipline working exactly as intended.
162 changes: 162 additions & 0 deletions includes/openstation-explorer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php
/**
* WP Explorer — the shell's My WordPress window.
*
* The Explorer browses every `show_ui` post type automatically, and this
* plugin's types are all `show_ui => false` — they have their own windows, so
* putting them in wp-admin's menu twice would be noise. The side effect was
* that the site's forms were invisible in the one place somebody browses
* everything they have. This file adds the Forms section back deliberately:
* a tile per form, wearing what a form is actually about — how many questions
* it asks, how many answers it holds, what it looks like — with the builder,
* the entries window and the report one click away in the preview pane.
*
* Entries are deliberately NOT listed here. They are `show_in_rest => false`
* by privacy design, and the Explorer can only list what REST serves. The
* asks that would make them safe to show — per-post children, capability-
* scoped bridged sections — live in `docs/wp-explorer-requests.md`.
*
* Every filter here is additive and gated: with no shell installed none of
* them ever fire.
*
* @package AllTerrain_Forms
*/

defined( 'ABSPATH' ) || exit;

add_action( 'plugins_loaded', 'atf_explorer_register' );

/**
* Hooks into whichever spelling of the shell is installed.
*
* @since 0.3.0
*
* @return void
*/
function atf_explorer_register() {
foreach ( atf_shell_hooks( 'my_wordpress_entities' ) as $hook ) {
add_filter( $hook, 'atf_explorer_entities' );
}

foreach ( atf_shell_hooks( 'my_wordpress_preview_actions' ) as $hook ) {
add_filter( $hook, 'atf_explorer_preview_actions' );
}

add_filter( 'rest_prepare_atf_form', 'atf_explorer_form_excerpt', 10, 2 );
}

/**
* Adds the Forms section to the Explorer's root.
*
* @since 0.3.0
*
* @param array[] $entities Section descriptors.
* @return array[]
*/
function atf_explorer_entities( $entities ) {
if ( ! atf_can_edit_forms() && ! atf_can_read_entries() ) {
return $entities;
}

$entities[] = array(
'id' => 'atf-forms',
'label' => __( 'Forms', 'allterrain-forms' ),
'icon' => 'dashicons-feedback',
'restPath' => 'wp/v2/atf-forms',
'kind' => 'post',
'post_type' => ATF_FORM_TYPE,
// A form has no featured image, and leaving thumbnails on makes the
// list request embed media on every tile and get nothing back.
'thumbnails' => false,
);

return $entities;
}

/**
* The preview-pane buttons a form deserves.
*
* One per surface, capability-gated server-side so a viewer who may only read
* entries is never shown a builder button that would refuse them. The click
* side lives in `dock.ts` against `os.my-wordpress.preview-actions`, which is
* what the `script` handle loads.
*
* @since 0.3.0
*
* @param array[] $actions Registered preview actions.
* @return array[]
*/
function atf_explorer_preview_actions( $actions ) {
$actions[] = array(
'id' => 'allterrain-forms/open-builder',
'label' => __( 'Open in the form builder', 'allterrain-forms' ),
'icon' => 'dashicons-feedback',
'capability' => 'atf_edit_forms',
'sections' => array( 'atf-forms' ),
'script' => 'allterrain-forms-dock',
);

$actions[] = array(
'id' => 'allterrain-forms/open-entries',
'label' => __( 'View entries', 'allterrain-forms' ),
'icon' => 'dashicons-list-view',
'capability' => 'atf_read_entries',
'sections' => array( 'atf-forms' ),
'script' => 'allterrain-forms-dock',
);

$actions[] = array(
'id' => 'allterrain-forms/open-analytics',
'label' => __( 'Open the report', 'allterrain-forms' ),
'icon' => 'dashicons-chart-bar',
'capability' => 'atf_read_entries',
'sections' => array( 'atf-forms' ),
'script' => 'allterrain-forms-dock',
);

return $actions;
}

/**
* Dresses a form's Explorer tile in what the form is about.
*
* A tile reading only "Team pulse survey" says the least interesting thing a
* form has to say. The excerpt becomes its vitals — questions, answers, theme
* — so the grid reads like an inventory rather than a list of names.
*
* On every `wp/v2/atf-forms` response, not only the Explorer's: a REST
* consumer that asked for an excerpt gets a truthful one either way, and the
* form has no hand-written excerpt to overwrite.
*
* @since 0.3.0
*
* @param WP_REST_Response $response The response.
* @param WP_Post $post The form.
* @return WP_REST_Response
*/
function atf_explorer_form_excerpt( $response, $post ) {
if ( ! isset( $response->data['excerpt'] ) || ! is_array( $response->data['excerpt'] ) ) {
return $response;
}

$schema = atf_get_form_schema( $post->ID );
$themes = atf_get_themes();
$slug = $schema['settings']['theme'];
$theme = isset( $themes[ $slug ]['label'] ) ? $themes[ $slug ]['label'] : $slug;
$fields = count( atf_input_fields( $schema ) );
$entries = atf_count_entries( $post->ID );

$vitals = sprintf(
/* translators: 1: question count, 2: entry count, 3: theme name. */
__( '%1$s · %2$s · %3$s', 'allterrain-forms' ),
/* translators: %d: number of questions. */
sprintf( _n( '%d question', '%d questions', $fields, 'allterrain-forms' ), $fields ),
/* translators: %d: number of entries. */
sprintf( _n( '%d entry', '%d entries', $entries, 'allterrain-forms' ), $entries ),
$theme
);

$response->data['excerpt']['rendered'] = '<p>' . esc_html( $vitals ) . '</p>';

return $response;
}
2 changes: 1 addition & 1 deletion includes/post-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function atf_register_post_types() {
'show_in_menu' => false,
'show_in_rest' => true,
'rest_base' => 'atf-forms',
'supports' => array( 'title', 'revisions', 'author' ),
'supports' => array( 'title', 'excerpt', 'revisions', 'author' ),
'capability_type' => array( 'atf_form', 'atf_forms' ),
'map_meta_cap' => true,
'exclude_from_search' => true,
Expand Down
40 changes: 40 additions & 0 deletions src/dock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,43 @@ function boot(): boolean {
if ( ! boot() ) {
document.addEventListener( 'os-init', () => void boot(), { once: true } );
}

/**
* Wires the Explorer's preview-action buttons to this plugin's windows.
*
* The PHP side declares the buttons (`includes/openstation-explorer.php`);
* the shell filters them through `os.my-wordpress.preview-actions` so each
* plugin attaches its own `onSelect`. This runs at module load rather than
* behind `boot()`, because `wp.hooks` filters can be added before the shell
* reads them — order is the point of a hook.
*/
function registerExplorerActions(): void {
const hooks = ( window as unknown as { wp?: { hooks?: { addFilter?: ( h: string, ns: string, cb: ( a: unknown[] ) => unknown[] ) => void } } } ).wp?.hooks;

if ( ! hooks?.addFilter ) {
return;
}

const windows: Record< string, string > = {
'allterrain-forms/open-builder': BUILDER,
'allterrain-forms/open-entries': ENTRIES,
'allterrain-forms/open-analytics': ANALYTICS,
};

hooks.addFilter( 'os.my-wordpress.preview-actions', 'allterrain-forms/explorer', ( actions: unknown[] ) =>
actions.map( ( action ) => {
const id = ( action as { id?: string } ).id ?? '';

if ( ! windows[ id ] ) {
return action;
}

return {
...( action as object ),
onSelect: () => open( windows[ id ] ),
};
} )
);
}

registerExplorerActions();
Loading
Loading