Add governed module and interface controls

This commit is contained in:
2026-08-04 05:20:47 +02:00
parent 5bc7d748f8
commit d6e7c8b0b1
27 changed files with 1700 additions and 74 deletions
+4 -3
View File
@@ -1,12 +1,13 @@
import type { ButtonHTMLAttributes, ReactNode } from "react";
import DisabledActionTooltip from "./DisabledActionTooltip";
import type { PlatformInterfaceIdentityProps } from "../types";
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & PlatformInterfaceIdentityProps & {
variant?: "primary" | "secondary" | "ghost" | "danger";
disabledReason?: ReactNode;
};
export default function Button({ variant = "secondary", className = "", disabledReason, disabled, ...props }: ButtonProps) {
const button = <button className={`btn btn-${variant} ${className}`} disabled={disabled || Boolean(disabledReason)} {...props} />;
export default function Button({ variant = "secondary", className = "", disabledReason, disabled, interfaceId, helpTopicId, ...props }: ButtonProps) {
const button = <button data-interface-id={interfaceId} data-help-topic-id={helpTopicId} className={`btn btn-${variant} ${className}`} disabled={disabled || Boolean(disabledReason)} {...props} />;
return <DisabledActionTooltip reason={disabledReason}>{button}</DisabledActionTooltip>;
}
+8 -7
View File
@@ -1,8 +1,9 @@
import { CalendarDays, ChevronLeft, ChevronRight, Clock } from "lucide-react";
import { useEffect, useMemo, useRef, useState, type InputHTMLAttributes } from "react";
import useOutsideDismiss from "../hooks/useOutsideDismiss";
import type { PlatformInterfaceIdentityProps } from "../types";
type BaseProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange" | "min" | "max"> & {
type BaseProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange" | "min" | "max"> & PlatformInterfaceIdentityProps & {
value: string;
onChange: (value: string) => void;
min?: string;
@@ -49,7 +50,7 @@ function combineDateTime(date: string, time: string): string {
return `${date || dateString(new Date())}T${time || "00:00"}`;
}
export function DateField({ value, onChange, min, max, disabled, className = "", placeholder = "i18n:govoplan-core.yyyy_mm_dd.d3f8f7b8", ...props }: BaseProps) {
export function DateField({ value, onChange, min, max, disabled, className = "", placeholder = "i18n:govoplan-core.yyyy_mm_dd.d3f8f7b8", interfaceId, helpTopicId, ...props }: BaseProps) {
const selectedDate = parseDate(value);
const [open, setOpen] = useState(false);
const [visibleMonth, setVisibleMonth] = useState<Date>(() => selectedDate ?? new Date());
@@ -93,7 +94,7 @@ export function DateField({ value, onChange, min, max, disabled, className = "",
}
return (
<div ref={rootRef} className={`date-field ${className}`.trim()}>
<div ref={rootRef} className={`date-field ${className}`.trim()} data-interface-id={interfaceId} data-help-topic-id={helpTopicId}>
<input
{...props}
ref={inputRef}
@@ -145,7 +146,7 @@ export function DateField({ value, onChange, min, max, disabled, className = "",
}
export function TimeField({ value, onChange, min, max, className = "", placeholder = "i18n:govoplan-core.hh_mm.a4c7ee9b", ...props }: BaseProps) {
export function TimeField({ value, onChange, min, max, className = "", placeholder = "i18n:govoplan-core.hh_mm.a4c7ee9b", interfaceId, helpTopicId, ...props }: BaseProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const input = inputRef.current;
@@ -158,7 +159,7 @@ export function TimeField({ value, onChange, min, max, className = "", placehold
}, [value, min, max]);
return (
<div className={`time-field ${className}`.trim()}>
<div className={`time-field ${className}`.trim()} data-interface-id={interfaceId} data-help-topic-id={helpTopicId}>
<input
{...props}
ref={inputRef}
@@ -174,7 +175,7 @@ export function TimeField({ value, onChange, min, max, className = "", placehold
}
export function DateTimeField({ value, onChange, min, max, disabled, className = "", ...props }: BaseProps) {
export function DateTimeField({ value, onChange, min, max, disabled, className = "", interfaceId, helpTopicId, ...props }: BaseProps) {
const parts = datePartsFromDateTime(value);
const minParts = datePartsFromDateTime(min || "");
const maxParts = datePartsFromDateTime(max || "");
@@ -188,7 +189,7 @@ export function DateTimeField({ value, onChange, min, max, disabled, className =
}
return (
<div className={`date-time-field ${className}`.trim()}>
<div className={`date-time-field ${className}`.trim()} data-interface-id={interfaceId} data-help-topic-id={helpTopicId}>
<DateField
{...props}
value={parts.date}
+10 -2
View File
@@ -3,12 +3,20 @@ import FieldLabel from "./help/FieldLabel";
import type { DocumentationHelpReference } from "./help/documentationHelp";
import { helpForFieldLabel } from "../utils/fieldHelp";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
export default function FormField({ label, help, documentation, children }: { label: ReactNode; help?: ReactNode; documentation?: DocumentationHelpReference; children: ReactNode }) {
type FormFieldProps = PlatformInterfaceIdentityProps & {
label: ReactNode;
help?: ReactNode;
documentation?: DocumentationHelpReference;
children: ReactNode;
};
export default function FormField({ label, help, documentation, children, interfaceId, helpTopicId }: FormFieldProps) {
const { translateText } = usePlatformLanguage();
const renderedLabel = typeof label === "string" ? translateText(label) : label;
return (
<label className="form-field">
<label className="form-field" data-interface-id={interfaceId} data-help-topic-id={helpTopicId}>
<FieldLabel className="form-label" help={help ?? helpForFieldLabel(label)} documentation={documentation}>{renderedLabel}</FieldLabel>
{children}
</label>
+1 -1
View File
@@ -43,7 +43,7 @@ export default class ModuleLoadBoundary extends Component<
if (this.state.error) {
return (
<div className="content-pad module-load-error">
<DismissibleAlert tone="danger" dismissible={false}>
<DismissibleAlert tone="danger" compact resetKey={`${this.props.resetKey}:${this.state.error.message}`}>
<p>i18n:govoplan-core.the_resource_could_not_be_loaded.0d1b6cbf</p>
<Button type="button" onClick={() => window.location.reload()}>
i18n:govoplan-core.reload.cce71553
+7 -2
View File
@@ -9,6 +9,7 @@ import {
} from "react";
import { ChevronDown, Search } from "lucide-react";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
export type SearchableSelectOption = {
value: string;
@@ -27,7 +28,7 @@ export type SearchableSelectCreateCustomOption = (
value: string
) => SearchableSelectOption | null;
export type SearchableSelectProps = {
export type SearchableSelectProps = PlatformInterfaceIdentityProps & {
id?: string;
value: string;
onChange: (
@@ -95,7 +96,9 @@ export default function SearchableSelect({
minQueryLength = 0,
searchLimit = 50,
debounceMs = 200,
className = ""
className = "",
interfaceId,
helpTopicId
}: SearchableSelectProps) {
const { translateText } = usePlatformLanguage();
const generatedId = useId().replace(/[^a-zA-Z0-9_-]/g, "-");
@@ -294,6 +297,8 @@ export default function SearchableSelect({
<div
ref={rootRef}
className={rootClassName}
data-interface-id={interfaceId}
data-help-topic-id={helpTopicId}
onBlur={closeOnFocusLeave}
>
<div className="searchable-select-control">
+4 -3
View File
@@ -2,8 +2,9 @@ import type { ReactNode } from "react";
import FieldLabel from "./help/FieldLabel";
import { helpForFieldLabel } from "../utils/fieldHelp";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
type ToggleSwitchProps = {
type ToggleSwitchProps = PlatformInterfaceIdentityProps & {
label: ReactNode;
activeLabel?: ReactNode;
inactiveLabel?: ReactNode;
@@ -13,7 +14,7 @@ type ToggleSwitchProps = {
help?: ReactNode;
};
export default function ToggleSwitch({ label, activeLabel, inactiveLabel, checked, onChange, disabled = false, help }: ToggleSwitchProps) {
export default function ToggleSwitch({ label, activeLabel, inactiveLabel, checked, onChange, disabled = false, help, interfaceId, helpTopicId }: ToggleSwitchProps) {
const { translateText } = usePlatformLanguage();
const hasStateLabels = activeLabel !== undefined || inactiveLabel !== undefined;
const renderedLabel = typeof label === "string" ? translateText(label) : label;
@@ -21,7 +22,7 @@ export default function ToggleSwitch({ label, activeLabel, inactiveLabel, checke
const renderedActiveLabel = typeof activeLabel === "string" ? translateText(activeLabel) : activeLabel;
const inputLabel = typeof renderedLabel === "string" ? renderedLabel : undefined;
return (
<label className={`toggle-switch-row ${disabled ? "disabled" : ""}`}>
<label className={`toggle-switch-row ${disabled ? "disabled" : ""}`} data-interface-id={interfaceId} data-help-topic-id={helpTopicId}>
<input
className="toggle-switch-input"
type="checkbox"
@@ -1,6 +1,7 @@
import { i18nMessage, usePlatformLanguage } from "../../i18n/LanguageContext";
import { useEffect, useId, useMemo, useRef, useState } from "react";
import type { CSSProperties, KeyboardEvent } from "react";
import type { PlatformInterfaceIdentityProps } from "../../types";
import { createPortal } from "react-dom";
import Button from "../Button";
import {
@@ -12,7 +13,7 @@ import {
type MailboxAddress } from
"../../utils/emailAddresses";
type EmailAddressInputProps = {
type EmailAddressInputProps = PlatformInterfaceIdentityProps & {
value: MailboxAddress[];
onChange?: (value: MailboxAddress[]) => void;
onAddressAdded?: (address: MailboxAddress) => void;
@@ -43,7 +44,9 @@ export default function EmailAddressInput({
emailPlaceholder = "email@example.org",
emptyText = "i18n:govoplan-core.no_address_added_yet.809c4247",
compact = false,
showAddButton
showAddButton,
interfaceId,
helpTopicId
}: EmailAddressInputProps) {
const { translateText } = usePlatformLanguage();
const inputId = useId();
@@ -199,7 +202,7 @@ export default function EmailAddressInput({
) : null;
return (
<div className={`email-address-input ${compact ? "compact" : ""} ${disabled ? "disabled" : ""} ${canUseAddButton ? "has-add-button" : ""}`}>
<div className={`email-address-input ${compact ? "compact" : ""} ${disabled ? "disabled" : ""} ${canUseAddButton ? "has-add-button" : ""}`} data-interface-id={interfaceId} data-help-topic-id={helpTopicId}>
<div className={`email-address-editor ${error ? "has-error" : ""}`}>
<div className="email-chip-list" aria-live="polite">
{normalizedValue.length === 0 && !entryText && <span className="email-chip-empty">{translateText(emptyText)}</span>}