Add governed module and interface controls
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>}
|
||||
|
||||
Reference in New Issue
Block a user