Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions client/src/app/components/HookFormPFFields/HookFormPFAddLabels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React from "react";

import {
Card,
FormHelperText,
HelperText,
HelperTextItem,
Label,
LabelGroup,
Stack,
StackItem,
TextInput,
} from "@patternfly/react-core";
import type { FieldValues, Path } from "react-hook-form";

import {
type BaseHookFormPFGroupControllerProps,
HookFormPFGroupController,
extractGroupControllerProps,
} from "./HookFormPFGroupController";

export type HookFormPFAddLabelsProps<
TFieldValues extends FieldValues,
TName extends Path<TFieldValues>,
> = BaseHookFormPFGroupControllerProps<TFieldValues, TName> & {
/**
* Placeholder text for the label input.
*/
inputPlaceholder?: string;
/**
* Aria-label for the label input.
*/
inputAriaLabel?: string;
/**
* Restricted labels for adding, f.e Product inside SBOM create group form.
*/
restrictedLabels?: string[];
};

export const HookFormPFAddLabels = <
TFieldValues extends FieldValues = FieldValues,
TName extends Path<TFieldValues> = Path<TFieldValues>,
>(
props: HookFormPFAddLabelsProps<TFieldValues, TName>,
) => {
const { extractedProps, remainingProps } = extractGroupControllerProps<
TFieldValues,
TName,
HookFormPFAddLabelsProps<TFieldValues, TName>
>(props);

const {
inputPlaceholder = "Add label",
inputAriaLabel = "add-label-input",
restrictedLabels = [],
} = remainingProps as {
inputPlaceholder?: string;
inputAriaLabel?: string;
restrictedLabels?: string[];
};

const [newLabel, setNewLabel] = React.useState<string>("");
const [labelError, setLabelError] = React.useState<string | null>(null);

return (
<HookFormPFGroupController<TFieldValues, TName>
{...extractedProps}
renderInput={({ field: { value, onChange } }) => {
const labels = (value ?? []) as string[];

const handleAdd = () => {
const trimmed = newLabel.trim();
if (!trimmed) {
return;
}
if (restrictedLabels.includes(trimmed)) {
setLabelError(`The label '${trimmed}' is reserved`);
return;
}
if (labels.includes(trimmed)) {
setLabelError("Label already exists");
return;
}
onChange([...labels, trimmed]);
setNewLabel("");
setLabelError(null);
};

const handleDelete = (labelToRemove: string) => {
onChange(labels.filter((l) => l !== labelToRemove));
};

return (
<Stack hasGutter>
<StackItem>Add metadata labels</StackItem>
<StackItem>
<Card style={{ padding: 10, minHeight: 100, borderRadius: 8 }}>
<LabelGroup numLabels={10}>
{labels.map((label) => (
<Label
key={label}
color="blue"
onClose={() => handleDelete(label)}
>
{label}
</Label>
))}
</LabelGroup>
</Card>
</StackItem>
<StackItem>
<TextInput
value={newLabel}
aria-label={inputAriaLabel}
placeholder={inputPlaceholder}
onChange={(_event, value) => {
setNewLabel(value);
if (labelError) {
setLabelError(null);
}
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
handleAdd();
}
}}
/>
{labelError && (
<FormHelperText>
<HelperText>
<HelperTextItem variant="error">
{labelError}
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
</StackItem>
</Stack>
);
}}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export interface BaseHookFormPFGroupControllerProps<
TName extends Path<TFieldValues>,
> {
control: Control<TFieldValues>;
/**
* Extra props forwarded directly to react-hook-form's Controller
* (e.g. rules, defaultValue, shouldUnregister, disabled, etc.).
*/
controllerProps?: Omit<
ControllerProps<TFieldValues, TName>,
"render" | "name" | "control"
>;
label?: React.ReactNode;
labelIcon?: React.ReactElement;
name: TName;
Expand All @@ -45,6 +53,7 @@ export const HookFormPFGroupController = <
TName extends Path<TFieldValues> = Path<TFieldValues>,
>({
control,
controllerProps,
label,
labelIcon,
name,
Expand All @@ -59,6 +68,7 @@ export const HookFormPFGroupController = <
<Controller<TFieldValues, TName>
control={control}
name={name}
{...controllerProps}
render={({ field, fieldState, formState }) => {
const { isDirty, isTouched, error } = fieldState;
const shouldDisplayError =
Expand Down Expand Up @@ -107,6 +117,7 @@ export const extractGroupControllerProps = <
} => {
const {
control,
controllerProps,
label,
labelIcon,
name,
Expand All @@ -121,6 +132,7 @@ export const extractGroupControllerProps = <
return {
extractedProps: {
control,
controllerProps,
labelIcon,
label,
name,
Expand Down
1 change: 1 addition & 0 deletions client/src/app/components/HookFormPFFields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./HookFormPFGroupController";
export * from "./HookFormPFTextInput";
export * from "./HookFormPFTextArea";
export * from "./HookFormPFSelect";
export * from "./HookFormPFAddLabels";
Loading