feat(16.2-01): host plugin field type elements in the admin SPA
- loadFieldTypes fetches GET /field-types after sign-in, loads each script once and adds the styles as global links activateStyles never disables - PluginField creates the type's element, sets the value property and the contract attributes, takes summer-change as the form value and never writes back a value the element just emitted - rendererForField and isRegisteredField route any field with an element to PluginField in forms, previews and the save payload - rebuilt modules/boardwalk/dist
This commit is contained in:
52
admin/src/app/fieldTypes.ts
Normal file
52
admin/src/app/fieldTypes.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Plugin field types (pact.HasFormFieldTypes, D-13). After sign-in the SPA
|
||||
// asks GET {api}/field-types once and loads every type's scripts as module
|
||||
// scripts (each URL once, through loadScript) and its stylesheets as global
|
||||
// links that no navigation disables, so a type's element works in any
|
||||
// controller's form. Nothing here ever rejects: a script that fails to load
|
||||
// surfaces later as that field's failure state.
|
||||
import { api } from '../api/client'
|
||||
import type { FieldTypeAssets } from '../api/types'
|
||||
import { loadGlobalStyles, loadScript } from './pluginAssets'
|
||||
|
||||
// Element tag -> the script URLs of its type that failed to load.
|
||||
const scriptsByElement = new Map<string, Promise<string[]>>()
|
||||
|
||||
/**
|
||||
* Fetches the registered field types, starts loading their scripts and
|
||||
* enables their styles. Resolves with the list (empty on any failure)
|
||||
* without waiting for the scripts.
|
||||
*/
|
||||
export async function loadFieldTypes(): Promise<FieldTypeAssets[]> {
|
||||
let types: FieldTypeAssets[] = []
|
||||
try {
|
||||
const { data, response } = await api.GET('/field-types')
|
||||
types = response.ok && data ? data.data : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
for (const type of types) {
|
||||
loadGlobalStyles(type.styles ?? [])
|
||||
const urls = type.scripts ?? []
|
||||
scriptsByElement.set(
|
||||
type.element,
|
||||
Promise.allSettled(urls.map((url) => loadScript(url))).then((results) =>
|
||||
urls.filter((_, index) => results[index]?.status === 'rejected'),
|
||||
),
|
||||
)
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves once the scripts of the field type that defines `element` have
|
||||
* loaded or failed, with the URLs that failed; at once with none when the
|
||||
* element belongs to no loaded field type.
|
||||
*/
|
||||
export function fieldTypeScripts(element: string): Promise<string[]> {
|
||||
return scriptsByElement.get(element) ?? Promise.resolve([])
|
||||
}
|
||||
|
||||
/** Forgets the loaded field types (tests and a new session). */
|
||||
export function resetFieldTypes(): void {
|
||||
scriptsByElement.clear()
|
||||
}
|
||||
@@ -7,7 +7,10 @@
|
||||
// every navigation to another controller or to a screen that is not a
|
||||
// controller view, so plugin CSS never styles a view it was not written for.
|
||||
// Only the router picks the enabled controller; a schema response that
|
||||
// arrives after its view was left adds its links disabled.
|
||||
// arrives after its view was left adds its links disabled. Stylesheets of
|
||||
// plugin field types are global instead (GLOBAL_STYLE_OWNER): a field type's
|
||||
// element can appear in any controller's form, so its links are never
|
||||
// disabled on navigation.
|
||||
// Files load only through script and link elements; nothing here fetches.
|
||||
import type { ControllerAssets } from '../api/types'
|
||||
import { runtime } from './runtime'
|
||||
@@ -15,6 +18,9 @@ import { runtime } from './runtime'
|
||||
/** Attribute naming the controller that owns a plugin stylesheet link. */
|
||||
export const OWNER_ATTRIBUTE = 'data-summer-controller'
|
||||
|
||||
/** Owner of stylesheet links that stay enabled on every screen (field types). */
|
||||
export const GLOBAL_STYLE_OWNER = '*'
|
||||
|
||||
const scripts = new Map<string, Promise<void>>()
|
||||
// Links by controller and URL: two controllers of one plugin may share a
|
||||
// file, and each keeps its own link so disabling one never hides the other.
|
||||
@@ -90,7 +96,7 @@ export function loadStyles(controllerId: string, urls: readonly string[]): void
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.setAttribute(OWNER_ATTRIBUTE, controllerId)
|
||||
link.disabled = controllerId !== active
|
||||
link.disabled = !styleEnabled(controllerId)
|
||||
link.href = url
|
||||
styles.set(key, { owner: controllerId, link })
|
||||
document.head.appendChild(link)
|
||||
@@ -105,10 +111,23 @@ export function loadStyles(controllerId: string, urls: readonly string[]): void
|
||||
export function activateStyles(controllerId: string): void {
|
||||
active = controllerId
|
||||
for (const { owner, link } of styles.values()) {
|
||||
link.disabled = owner !== active
|
||||
link.disabled = !styleEnabled(owner)
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether links of this owner are enabled now: the active controller's and the global ones. */
|
||||
function styleEnabled(owner: string): boolean {
|
||||
return owner === GLOBAL_STYLE_OWNER || owner === active
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds stylesheet links that stay enabled on every screen: the styles of
|
||||
* plugin field types, whose elements may render in any controller's form.
|
||||
*/
|
||||
export function loadGlobalStyles(urls: readonly string[]): void {
|
||||
loadStyles(GLOBAL_STYLE_OWNER, urls)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the controller's stylesheet links and loads its scripts. The links
|
||||
* are enabled only while the router shows that controller, so a response
|
||||
|
||||
Reference in New Issue
Block a user