Skip to content

Add JavaScript frontend extensibility#754

Open
malberts wants to merge 5 commits intomasterfrom
frontend-extensibility
Open

Add JavaScript frontend extensibility#754
malberts wants to merge 5 commits intomasterfrom
frontend-extensibility

Conversation

@malberts
Copy link
Copy Markdown
Collaborator

@malberts malberts commented Apr 21, 2026

Fixes #686

Implements Option 1 from the design discussion on #686mw.hook('neowiki.registration') with plain-object registrations, MW-native ResourceLoader modules, and plain .vue SFCs for extensions. No bundler or TypeScript required on the extension side.

Summary

Lets external MediaWiki extensions register custom frontend property types at runtime, without touching NeoWiki source or rebuilding its bundle. This PR adds the mechanism only; no consumer is in-tree. Companion PRs exercise it:

What this PR ships

  • JS hook mw.hook('neowiki.registration') — subscribers receive a FrontendRegistrar and call registrar.registerPropertyType({...}) with a plain-object PropertyTypeRegistration. An internal PropertyTypeAdapter wraps each registration as a BasePropertyType; the registrar inserts it into NeoWiki's component and property-type registries.
  • PHP hook NeoWikiGetFrontendModules( array &$modules, OutputPage $out, Skin $skin ) — extensions append their ResourceLoader module names; NeoWiki adds them alongside ext.neowiki on any page that loads it. Consumed in onBeforePageDisplay plus the Special:Schemas and Special:Layouts entry points.
  • Public-API barrel resources/ext.neowiki/src/public-api.ts — re-exports every TS module and Vue component under resources/ext.neowiki/src/ (83 TS files, 52 Vue files, ~146 runtime names). 0.x / alpha stability contract: anything may change without migration guidance, narrowed before production stabilisation.
  • Memoized registriesNeoWikiExtension.getTypeSpecificComponentRegistry() and Neo.getPropertyTypeRegistry() return the same instance per call, so the registry the hook populates is the same one NeoWikiServices provides to Vue.
  • Mount deferral — each initialize* function in neowiki.ts wraps its body in queueMicrotask and fires the registration hook before deserialization and mount, giving extension init.js files a chance to subscribe first.

Jeroen's three usage modes (from this comment)

The mechanism itself supports all three. The consumer PRs exercise them end-to-end:

Mode Mechanism support in this PR Example consumer
Register a property type mw.hook('neowiki.registration') + FrontendRegistrar datetime-example (#777); upcoming Color PR
Import and use a NeoWiki Vue component ✓ Broad public-API barrel re-exports every Vue component datetime-example uses NeoNestedField; Color PR plans NeoMultiTextInput, EditSummary
Call a NeoWiki service from JS ✓ Broad barrel re-exports every TS module datetime-example calls NeoWikiServices.getPropertyTypeRegistry().getType(...).validate(...); Color PR plans stores + composables + a repository

Commits

  1. Introduce public-API barrel and memoize registries — preparatory; no user-visible behaviour change.
  2. Add neowiki.registration JS hook and NeoWikiGetFrontendModules PHP hook — full mechanism. Integration-test (resources/ext.neowiki/tests/integration/HookRegistration.spec.ts) proves end-to-end via a fake registration.
  3. Expose broad public API surface — turns the narrow barrel into a wildcard re-export of every TS module and Vue component under src/.

Test plan

  • make tsci green — 771 Vitest tests across 78 files, 266 bundler modules transformed
  • make phpunit full suite — pre-existing unrelated failure in Neo4jConstraintUpdaterTest::testDefaultConstraintsAreCreated (version drift UNIQUENESS vs NODE_PROPERTY_UNIQUENESS) reproduces on master
  • make cs phpcs + phpstan clean
  • Integration test HookRegistration.spec.ts exercises .fire() before subscribe + .add() after .fire() (mw.hook replay semantics) — both code paths green
  • Public-API barrel coverage audit: 83 TS files on disk ↔ 83 TS exports; 52 Vue files on disk ↔ 52 Vue exports — exact match

Manual browser check

Because this PR has no in-tree consumer, end-to-end UI verification lives in the consumer PRs. From the repo root, make import-demo-data (if needed) and docker compose restart mediawiki. Log in as AdminName / AdminPassword.

Mechanism-only checks

  • http://localhost:8484/index.php/Main_Page loads without console errors.
  • http://localhost:8484/index.php/Special:Schemas, Special:Layouts, and any existing Schema: or Layout: page render without console errors.
  • curl 'http://localhost:8484/load.php?modules=ext.neowiki&only=scripts&lang=en&debug=true' returns 200 and the response includes the 146 named exports on module.exports (inspect the tail of the bundle).
  • From a browser console on any NeoWiki page:
    const nw = mw.loader.require( 'ext.neowiki' );
    Object.keys( nw ).length; // ≥ 146
    typeof nw.NeoWikiServices; // 'object'
    typeof nw.FrontendRegistrar; // 'function'
  • Pre-existing property types (Text / Number / URL / Select / Relation / DateTime) still work end-to-end — no regressions from the barrel refactor, registry memoization, or queueMicrotask deferral.

Full end-to-end via a consumer

For end-to-end verification of the registration flow itself, either check out datetime-example (PR #777) or the upcoming Color PR and follow its test plan. The integration test in this PR covers the same flow at the unit level.

Follow-ups

  • Curation pass before production — audit which of the 146 barrel exports are actually useful to consumers vs. internal-only; narrow the surface and document the stable contract.
  • Name-collision regression guard — export * errors at build when two modules grow an identically-named export. Fine for now; worth a script/check if the source tree grows a lot.
  • Graceful degradation when a type-owning extension is disabled — observed in datetime-example: disabling the extension leaves stored data using that type unrenderable and the NeoWiki UI container empty. Architecturally expected for extension-owned types, but worth a "unknown property type" placeholder component rather than aborting the whole view.

@malberts malberts changed the title Add frontend extensibility via mw.hook('neowiki.registration') Add frontend extensibility Apr 21, 2026
@malberts malberts changed the title Add frontend extensibility Add JavaScript frontend extensibility Apr 21, 2026
@malberts malberts force-pushed the frontend-extensibility branch from 4672e37 to 6da22a2 Compare April 21, 2026 21:34
@malberts
Copy link
Copy Markdown
Collaborator Author

So far Claude has done browser tests, and I have also done manual tests for a full end-to-end flow: Create Schema -> Create Subject -> View Subject -> Edit Schema -> Edit Subject -> View Subject.

This does not consider the recent comments in #686 (comment).

@JeroenDeDauw
Copy link
Copy Markdown
Member

Result of extensive back and forth with @JeroenDeDauw reviewing this PR against a parallel architectural options analysis.
Context: the NeoWiki codebase, the discussion on #686, and the trade-off space around the ext.neowiki public API surface.
Written by Claude Code, Opus 4.7

Strongly aligned with #686's design direction. Structural choices match; a few things worth calling out.

Aligned

  • Public-API barrel at resources/ext.neowiki/src/public-api.ts
  • mw.hook('neowiki.registration') + FrontendRegistrar + plain-object PropertyTypeRegistration + internal PropertyTypeAdapter
  • Memoized registries — the fresh-per-getter pattern was a concrete blocker, this fixes it
  • Hook-fire timing resolved via queueMicrotask wrapping in neowiki.ts's initialize functions
  • DateTime moved from core to RedHerb as end-to-end proof
  • All three usage modes exercised by RedHerb's DateTime (register, consume component via NeoNestedField, call service via PropertyTypeRegistry)

Points worth flagging

  1. Three commits in one PR rather than three mergeables. Combining them is a reasonable judgment call — validation loop stays intact. Does mean the standalone benefit of the barrel commit (consumption unblocked independently of the hook) is deferred until the whole thing lands.

  2. Public API starts narrow vs broad. The PR describes the barrel as "starts narrow… and grows organically as real usage demands". Alternative position: broad-and-provisional during alpha — expose a superset, narrow before stability. Arguable both ways. Narrow-and-grow is conservative and nothing accidentally becomes public; it does mean downstream consumers will hit "not exported" friction more often during the alpha build-out. Worth an explicit call, either way.

  3. NeoWikiGetFrontendModules PHP hook is a good addition. Not in the original sketch but solves the real problem of how does ext.redherb actually get loaded on a NeoWiki page. Cleanly separates discovery from registration.

Open items worth checking in the diff

  • Pinia instance sharing. Doesn't matter for DateTime since it doesn't touch stores, but the moment an extension component binds to useSubjectStore or similar, a mismatched Pinia instance would silently break reactivity. Worth verifying a single instance is shared across NeoWiki's mounts and reachable by extension components.
  • Whether PropertyTypeRegistration is derived from BasePropertyType via TS utility types (issue Add frontend extension mechanism for property types #686 mechanism 3) or written by hand. Former self-syncs with class changes; latter drifts silently.

Observations worth preserving

  • The "silent UI failure when a type-owning extension is disabled" follow-up is architecturally important. Disabled extension + stored statements of its type = blank NeoWiki UI. Placeholder rows (or similar graceful degradation) deserve to happen before production. Also a reason core types belong in core.
  • Flagging that DateTime should return to core rather than live permanently in RedHerb is correct. Temporary forcing function = fine; permanent home = wrong.

Overall: tracks the agreed direction and makes two sensible judgment calls on top. The narrow-vs-broad API surface is the one decision I'd want confirmed explicitly before it solidifies.

@malberts malberts mentioned this pull request Apr 22, 2026
6 tasks
@malberts malberts force-pushed the frontend-extensibility branch from 6da22a2 to bd8f5d3 Compare April 22, 2026 22:57
malberts added a commit that referenced this pull request Apr 22, 2026
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes —
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 218 KB → 229 KB raw (+11 KB / +5%), 59 KB → 62 KB
gzipped (+3 KB). Acceptable for the flexibility it provides during
alpha build-out.

Stacks on top of #754.
malberts added a commit that referenced this pull request Apr 22, 2026
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes —
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 218 KB → 229 KB raw (+11 KB / +5%), 59 KB → 62 KB
gzipped (+3 KB). Acceptable for the flexibility it provides during
alpha build-out.

Stacks on top of #754.
@malberts
Copy link
Copy Markdown
Collaborator Author

Three commits in one PR rather than three mergeables.

Seems better to have a fully working initial example in one PR.

Public API starts narrow vs broad.

#760

That adds everything (for now).

Pinia instance sharing

Likely TODO in a follow-up. Maybe as part of the same non-DateTime example.

The "silent UI failure when a type-owning extension is disabled" follow-up is architecturally important.

This should be split off into a new issue. While more likely to happen with extensions, it can still theoretically happen in core if we removed a type.


I need to do some final reviews on this PR, and see if I can come up with an alternative to DateTime so we do not have to jump around between core and RedHerb. Otherwise, if we keep the revertable commit, we can just undo this a little bit later once we have a different example to keep in RedHerb.

malberts added a commit that referenced this pull request Apr 22, 2026
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes —
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 218 KB -> 239 KB raw (+21 KB / +10%), 59 KB -> 64 KB
gzipped (+5 KB / +8%). Acceptable for the flexibility it provides
during alpha build-out.

Stacks on top of #754.
@malberts malberts force-pushed the frontend-extensibility branch from bd8f5d3 to bb0782c Compare April 22, 2026 23:36
malberts added a commit that referenced this pull request Apr 22, 2026
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes -
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 240 KB -> 251 KB raw (+11 KB / +4%), 63 KB -> 66 KB
gzipped (+3 KB / +5%). Acceptable for the flexibility it provides
during alpha build-out.

Stacks on top of #754.
malberts added a commit that referenced this pull request Apr 23, 2026
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes -
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 240 KB -> 251 KB raw (+11 KB / +4%), 63 KB -> 66 KB
gzipped (+3 KB / +5%). Acceptable for the flexibility it provides
during alpha build-out.

Stacks on top of #754.
@malberts malberts force-pushed the frontend-extensibility branch from dc3dce6 to e9fc307 Compare April 23, 2026 18:26
malberts added a commit that referenced this pull request Apr 23, 2026
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes -
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 240 KB -> 251 KB raw (+11 KB / +4%), 63 KB -> 66 KB
gzipped (+3 KB / +5%). Acceptable for the flexibility it provides
during alpha build-out.

Stacks on top of #754.
@malberts malberts force-pushed the frontend-extensibility branch from e9fc307 to 136ee5e Compare April 24, 2026 09:43
malberts added a commit that referenced this pull request Apr 24, 2026
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes -
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 240 KB -> 251 KB raw (+11 KB / +4%), 63 KB -> 66 KB
gzipped (+3 KB / +5%). Acceptable for the flexibility it provides
during alpha build-out.

Stacks on top of #754.
malberts added a commit that referenced this pull request Apr 24, 2026
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes -
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 240 KB -> 251 KB raw (+11 KB / +4%), 63 KB -> 66 KB
gzipped (+3 KB / +5%). Acceptable for the flexibility it provides
during alpha build-out.

Stacks on top of #754.
@malberts malberts force-pushed the frontend-extensibility branch from 136ee5e to 933e95e Compare April 24, 2026 20:26
malberts and others added 3 commits April 24, 2026 22:52
Split Vite entry: src/public-api.ts is now the barrel that re-exports
runtime symbols MW-native extensions can require('ext.neowiki'). It
imports src/neowiki.ts for its mount side-effects.

Memoize NeoWikiExtension.getTypeSpecificComponentRegistry() and
PropertyTypeRegistry on Neo so every caller sees the same instance —
prerequisite for the extension-registration hook being added next,
which needs to mutate the registry that Vue later receives.

For #686

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extensions can now register custom property types at runtime:

1. PHP: implement NeoWikiGetFrontendModules to append their RL module
   name to NeoWiki's module list for any page that loads ext.neowiki.
2. JS: subscribe to mw.hook('neowiki.registration') and call
   registrar.registerPropertyType({...}) with a plain-object
   PropertyTypeRegistration.

NeoWiki wraps incoming registrations via PropertyTypeAdapter into
BasePropertyType instances; FrontendRegistrar inserts them into the
memoized property-type and component registries.

Mount is deferred via queueMicrotask so extension init.js files get a
chance to subscribe before the hook fires. No consumer of the hook
exists yet — DateTime still lives in core and will move in a
follow-up commit.

For #686

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes -
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 240 KB -> 251 KB raw (+11 KB / +4%), 63 KB -> 66 KB
gzipped (+3 KB / +5%). Acceptable for the flexibility it provides
during alpha build-out.

Stacks on top of #754.
@malberts malberts force-pushed the frontend-extensibility branch from 933e95e to efc2dfc Compare April 24, 2026 20:54
@malberts malberts marked this pull request as ready for review April 24, 2026 21:01
@malberts
Copy link
Copy Markdown
Collaborator Author

malberts commented Apr 24, 2026

I decided to split out the DateTime implementation into #777. It seems like we would just be moving that up and down for the sake of an example. I'm keeping that PR, but will follow-up with a Color example instead, which can stay in RedHerb and not require us to mess around with a property type that is a requirement. After the Color PR we should move all the DateTime code in RedHerb into core instead.

malberts added a commit that referenced this pull request Apr 25, 2026
First extension-provided property-type frontend built on the
neowiki.registration hook surface. For #686. Stacks on #754.

This is an alternative path to PR #777, not a stack on it.
PR #777 is a reference-only example that moves the existing
core DateTime frontend into RedHerb to demonstrate the
migration path; the intent for DateTime itself is to remain
in NeoWiki core as a built-in. This PR introduces Color as a
permanent example of an extension-defined property type, i.e.
the shape any third-party extension would take, without
disturbing core's built-in types.

What it adds:

* ColorDisplay.vue renders a swatch + hex via a format-only
  check (intentionally tolerant of input-time constraints
  like allowedColors so previously-valid values keep
  rendering); falls back through I18nSlot for unparseable
  values.
* ColorInput.vue is a cdx-text-input plus a live preview
  swatch and the type's start icon. Mirrors the
  useStringValueInput convention: invalid mid-typing stays
  visible to the user but does not propagate through
  update:modelValue or getCurrentValue().
* ColorAttributesEditor.vue wraps an optional allowedColors
  palette in NeoNestedField, with a drag-reorderable list via
  useSortable and a dirty marker driven by useChangeDetection.
* RedHerbFrontendHook.php implements NeoWikiGetFrontendModules
  so the new ext.redherb ResourceLoader module loads alongside
  ext.neowiki.
* ColorProperty gains an optional allowedColors attribute
  (validated as 6-digit hex strings), with round-trip
  coverage in tests/phpunit/RedHerb/ColorPropertyTest.php.

Integration points exercised:

* mw.hook('neowiki.registration') as a second consumer
  alongside core property types.
* NeoWikiGetFrontendModules PHP hook.
* NeoWikiServices.getPropertyTypeRegistry and
  NeoWikiServices.getComponentRegistry.
* Composables: useValueValidation, useChangeDetection,
  useSortable.
* Vue components from the public-API barrel: NeoNestedField,
  I18nSlot.

NeoWikiHooksTest::testAddsCoreModuleWhenNoExtensionsHandleHook
now clearHook()'s NeoWikiGetFrontendModules so it tests what
its name claims regardless of which extensions the test
environment loads.

Known gaps:

* Persistence-side schema validation rejects extension-defined
  type names; saves through the schema editor return
  hookaborted until the JSON Schema in
  src/Persistence/MediaWiki/schemaContentSchema.json is
  loosened or built from the registry. See #779.
* Pinia store sharing not directly probed in this PR; deferred
  to a Vitest integration test in NeoWiki core.
* SchemaDeserializer / RestSchemaRepository / RestLayoutRepository
  do not have a natural fit in the Color flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each initialize*Page() in neowiki.ts previously called createPinia() and
got its own root, so two Vue apps mounted on the same page could not
observe each other's store state. Centralize ownership of the Pinia in
NeoWikiExtension, expose getPinia(), and route every init through it.

Tested via a behavioral spec that mutates a store via one consumer and
reads it via another using the same Pinia, which is the actual contract
the shared instance is meant to guarantee.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malberts added a commit that referenced this pull request Apr 27, 2026
Adds a SidebarBeforeOutput hook handler contributing a "RedHerb"
sidebar section with a "Find a subject" link to a new
Special:RedHerbSubjectFinder. The special page mounts a RedHerb-owned
Vue panel via Vue.createMwApp that lets the user pick a subject by
schema and label, then renders the selected subject using NeoWiki's
Infobox component.

Exercises several integration points through the public-API barrel
introduced by #754: RestSubjectLabelSearch (REST repository) via the
embedded SubjectLookup component, the SubjectLabelSearch service via
NeoWiki's service injection, the StoreStateLoader for bulk-loading
subjects and schemas, the Infobox view component, and the shared
Pinia owned by NeoWikiExtension.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
testAddsCoreModuleWhenNoExtensionsHandleHook asserts that only
ext.neowiki is queued when no extension contributes via the
NeoWikiGetFrontendModules hook. Real loaded extensions (such as
RedHerb in CI) register handlers that contribute their own modules,
so the assertion fails. Clear the hook at the start of the test so
its premise holds regardless of which extensions are loaded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malberts added a commit that referenced this pull request Apr 27, 2026
First extension-provided property-type frontend built on the
neowiki.registration hook surface. For #686. Stacks on #754.

This is an alternative path to PR #777, not a stack on it.
PR #777 is a reference-only example that moves the existing
core DateTime frontend into RedHerb to demonstrate the
migration path; the intent for DateTime itself is to remain
in NeoWiki core as a built-in. This PR introduces Color as a
permanent example of an extension-defined property type, i.e.
the shape any third-party extension would take, without
disturbing core's built-in types.

What it adds:

* ColorDisplay.vue renders a swatch + hex via a format-only
  check (intentionally tolerant of input-time constraints
  like allowedColors so previously-valid values keep
  rendering); falls back through I18nSlot for unparseable
  values.
* ColorInput.vue is a cdx-text-input plus a live preview
  swatch and the type's start icon. Mirrors the
  useStringValueInput convention: invalid mid-typing stays
  visible to the user but does not propagate through
  update:modelValue or getCurrentValue().
* ColorAttributesEditor.vue wraps an optional allowedColors
  palette in NeoNestedField, with a drag-reorderable list via
  useSortable and a dirty marker driven by useChangeDetection.
* RedHerbFrontendHook.php implements NeoWikiGetFrontendModules
  so the new ext.redherb ResourceLoader module loads alongside
  ext.neowiki.
* ColorProperty gains an optional allowedColors attribute
  (validated as 6-digit hex strings), with round-trip
  coverage in tests/phpunit/RedHerb/ColorPropertyTest.php.

Integration points exercised:

* mw.hook('neowiki.registration') as a second consumer
  alongside core property types.
* NeoWikiGetFrontendModules PHP hook.
* NeoWikiServices.getPropertyTypeRegistry and
  NeoWikiServices.getComponentRegistry.
* Composables: useValueValidation, useChangeDetection,
  useSortable.
* Vue components from the public-API barrel: NeoNestedField,
  I18nSlot.

NeoWikiHooksTest::testAddsCoreModuleWhenNoExtensionsHandleHook
now clearHook()'s NeoWikiGetFrontendModules so it tests what
its name claims regardless of which extensions the test
environment loads.

Known gaps:

* Persistence-side schema validation rejects extension-defined
  type names; saves through the schema editor return
  hookaborted until the JSON Schema in
  src/Persistence/MediaWiki/schemaContentSchema.json is
  loosened or built from the registry. See #779.
* Pinia store sharing not directly probed in this PR; deferred
  to a Vitest integration test in NeoWiki core.
* SchemaDeserializer / RestSchemaRepository / RestLayoutRepository
  do not have a natural fit in the Color flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
malberts added a commit that referenced this pull request Apr 27, 2026
Adds a SidebarBeforeOutput hook handler contributing a "RedHerb"
sidebar section with a "Find a subject" link to a new
Special:RedHerbSubjectFinder. The special page mounts a RedHerb-owned
Vue panel via Vue.createMwApp that lets the user pick a subject by
schema and label, then renders the selected subject using NeoWiki's
Infobox component.

Exercises several integration points through the public-API barrel
introduced by #754: RestSubjectLabelSearch (REST repository) via the
embedded SubjectLookup component, the SubjectLabelSearch service via
NeoWiki's service injection, the StoreStateLoader for bulk-loading
subjects and schemas, the Infobox view component, and the shared
Pinia owned by NeoWikiExtension.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Add frontend extension mechanism for property types

2 participants