feat(custom-widgets): complete Custom JSX v2 (#6390)

This commit is contained in:
Thomas authored and ajnart committed 2026-08-13 19:48:57 +02:00
1 parent 3a1245cbc5
commit acdfcbb18e
416 files changed
+37853 -18066

No files matched your search

@@ -10,28 +10,70 @@ export const redactPreviewUrl = (value: string): string => {
}
};
export const getPreviewNamedRequests = (displayConfig: Record<string, unknown>): PreviewNamedRequest[] => {
if (!Array.isArray(displayConfig.requests)) return [];
return displayConfig.requests.filter(
(request): request is PreviewNamedRequest =>
request !== null &&
typeof request === "object" &&
"id" in request &&
typeof request.id === "string" &&
"kind" in request &&
(request.kind === "query" || request.kind === "action") &&
"method" in request &&
typeof request.method === "string" &&
"pathTemplate" in request &&
typeof request.pathTemplate === "string",
);
export const getPreviewNamedRequests = (definition: Record<string, unknown>): PreviewNamedRequest[] => {
if (!definition.requests || typeof definition.requests !== "object" || Array.isArray(definition.requests)) return [];
return Object.entries(definition.requests as Record<string, unknown>).flatMap(([id, candidate]) => {
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return [];
const request = candidate as Record<string, unknown>;
if (
(request.kind !== "query" && request.kind !== "action") ||
typeof request.method !== "string" ||
typeof request.path !== "string"
)
return [];
return [
{
id,
kind: request.kind,
method: request.method,
path: request.path,
minimumBoardPermission: typeof request.permission === "string" ? request.permission : undefined,
},
];
});
};
export const getPreviewLegacyMethods = (template: string): string[] => {
const methods = new Set<string>();
for (const match of template.matchAll(/<(?:SubFetch|ActionButton|ToggleSwitch)\b([^>]*)>/giu)) {
const method = match[1]?.match(/\bmethod\s*=\s*["'](GET|POST|PUT|PATCH|DELETE)["']/iu)?.[1];
methods.add(method?.toUpperCase() ?? (match[0].startsWith("<SubFetch") ? "GET" : "POST"));
export type CustomWidgetAccessibilityIssue = "imageAlt" | "actionIconLabel" | "inputLabel";
export function analyzeCustomWidgetAccessibility(template: string): CustomWidgetAccessibilityIssue[] {
const issues = new Set<CustomWidgetAccessibilityIssue>();
const elements = template.matchAll(/<([A-Z][A-Za-z0-9.]*)\b([^>]*)>/gu);
const inputComponents = new Set([
"Autocomplete",
"Checkbox",
"ColorInput",
"DateInput",
"FileInput",
"JsonInput",
"MultiSelect",
"NumberInput",
"PasswordInput",
"Radio",
"Select",
"Slider",
"Switch",
"TagsInput",
"TextInput",
"Textarea",
"TimeInput",
]);
for (const match of elements) {
const component = match[1] ?? "";
const props = match[2] ?? "";
if (component === "Image" && !hasProp(props, "alt")) issues.add("imageAlt");
if (component === "ActionIcon" && !hasAnyProp(props, ["aria-label", "title"])) issues.add("actionIconLabel");
if (inputComponents.has(component) && !hasAnyProp(props, ["label", "aria-label", "aria-labelledby"])) {
issues.add("inputLabel");
}
}
return [...methods];
};
return [...issues];
}
function hasAnyProp(props: string, names: string[]) {
return names.some((name) => hasProp(props, name));
}
function hasProp(props: string, name: string) {
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
return new RegExp(`(?:^|\\s)${escapedName}\\s*=`, "u").test(props);
}