/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import { ReactNode, useRef } from 'react';
import styled, { useTheme } from 'styled-components';
import Box from 'design/Box';
import { ButtonSecondary } from 'design/Button';
import ButtonIcon from 'design/ButtonIcon';
import Flex from 'design/Flex';
import * as Icon from 'design/Icon';
import { LabelContent } from 'design/LabelInput/LabelInput';
import { useRule } from 'shared/components/Validation';
import {
precomputed,
Rule,
ValidationResult,
} from 'shared/components/Validation/rules';
import FieldInput from '../FieldInput';
type StringListValidationResult = ValidationResult & {
/**
* A list of validation results, one per list item. Note: results are
* optional just because `useRule` by default returns only
* `ValidationResult`. For the actual validation, it's not optional; if it's
* undefined, or there are fewer results in this list than the list items,
* the corresponding items will be treated as valid.
*/
results?: ValidationResult[];
};
export type FieldMultiInputProps = {
label?: string;
value: string[];
disabled?: boolean;
/** Adds a required field indicator to the label. */
required?: boolean;
tooltipContent?: ReactNode;
tooltipSticky?: boolean;
onChange?(val: string[]): void;
rule?: Rule;
};
/**
* Allows editing a list of strings, one value per row. Use instead of
* `FieldSelectCreatable` when:
*
* - There are no predefined values to be picked from.
* - Values are expected to be relatively long and would be unreadable after
* being truncated.
*/
export function FieldMultiInput({
label,
value,
disabled,
required,
tooltipContent,
tooltipSticky,
onChange,
rule = defaultRule,
}: FieldMultiInputProps) {
// It's important to first validate, and then treat an empty array as a
// single-item list with an empty string, since this "synthetic" empty
// string is technically not a part of the model and should not be
// validated.
const validationResult: StringListValidationResult = useRule(rule(value));
if (value.length === 0) {
value = [''];
}
const theme = useTheme();
// Index of the input to be focused after the next rendering.
const toFocus = useRef();
const setFocus = (element: HTMLInputElement) => {
element?.focus();
toFocus.current = undefined;
};
function insertItem(index: number) {
onChange?.(value.toSpliced(index, 0, ''));
}
function removeItem(index: number) {
onChange?.(value.toSpliced(index, 1));
}
function handleKeyDown(index: number, e: React.KeyboardEvent) {
if (e.key === 'Enter') {
insertItem(index + 1);
toFocus.current = index + 1;
}
}
return (
);
}
const defaultRule = () => () => ({ valid: true });
const Fieldset = styled.fieldset`
border: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: ${props => props.theme.space[2]}px;
`;
const Legend = styled.legend`
margin: 0 0 ${props => props.theme.space[1]}px 0;
padding: 0;
${props => props.theme.typography.body3}
`;