Skip to content

Commit

Permalink
fix: handle lack of labels, avoid duplicates in the labels combo-box (#…
Browse files Browse the repository at this point in the history
…881)

* fix: handle decomposition of empty labels
   Resolves: kubeshop/testkube#4375
* fix: mark duplicated labels as disabled with proper tooltip
   Resolves: kubeshop/testkube#4358
  • Loading branch information
rangoo94 authored Sep 19, 2023
1 parent 7df1a7f commit 69d176c
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@ import {Option} from '@models/form';

import Colors from '@styles/Colors';

export const StyledOption = styled.div`
export const StyledOption = styled.div<{$disabled: boolean}>`
padding: 6px 12px;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: ${Colors.slate700};
}
${({$disabled}) =>
$disabled
? `
cursor: not-allowed;
color: ${Colors.slate500};
&:hover {
background-color: ${Colors.slate850};
}
`
: `
cursor: pointer;
&:hover {
background-color: ${Colors.slate700};
}
`}
`;

export const StyledMultiLabel = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type MultiSelectProps = {
value?: Option[];
defaultValue?: Option[];
onChange?: (value: readonly Option[]) => void;
isOptionDisabled?: (value: Option, selectValue: readonly Option[]) => boolean;
validateCreation?: (inputValue: string) => boolean;
CustomOptionComponent?: (props: OptionProps<Option>) => JSX.Element;
CustomMultiValueLabelComponent?: (props: MultiValueGenericProps<Option>) => JSX.Element;
Expand All @@ -42,6 +43,7 @@ const CreatableMultiSelect: React.FC<MultiSelectProps> = props => {
value,
defaultValue,
onChange,
isOptionDisabled,
validateCreation,
CustomOptionComponent = DefaultOptionComponent,
CustomMultiValueLabelComponent = DefaultMultiValueLabel,
Expand Down Expand Up @@ -80,6 +82,7 @@ const CreatableMultiSelect: React.FC<MultiSelectProps> = props => {
onChange={onChange}
placeholder={placeholder}
options={options}
isOptionDisabled={isOptionDisabled}
createOptionPosition="first"
onKeyDown={event => {
onEvent(event, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {OptionProps} from 'react-select';

import {Tooltip} from 'antd';

import {SplitLabelText} from '@atoms';

import {Option} from '@models/form';
Expand All @@ -10,24 +12,25 @@ import {StyledOption} from '../CreatableMultiSelect.styled';

const LabelsOption = (props: OptionProps<Option>) => {
// @ts-ignore
const {children, innerRef, innerProps, value} = props;
const {children, innerRef, innerProps, isDisabled, value} = props;

const isChildren = typeof children === 'string';
const allowClick = labelRegex.test(value);

if (allowClick && isChildren) {
return (
const option =
allowClick && isChildren ? (
// @ts-ignore
<StyledOption ref={innerRef} {...innerProps} $disabled={isDisabled} data-test={`label-option-${children}`}>
<SplitLabelText value={children} disabled={isDisabled} />
</StyledOption>
) : (
// @ts-ignore
<StyledOption ref={innerRef} {...innerProps} data-test={`label-option-${children}`}>
<SplitLabelText value={children} />
<StyledOption ref={innerRef} $disabled={isDisabled}>
{children}
</StyledOption>
);
}

return (
// @ts-ignore
<StyledOption ref={innerRef}>{children}</StyledOption>
);
return isDisabled ? <Tooltip title="There may be only single value for a key selected.">{option}</Tooltip> : option;
};

export default LabelsOption;
7 changes: 4 additions & 3 deletions src/components/atoms/SplitLabelText/SplitLabelText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import {SplitLabelTextContainer} from './SplitLabelText.styled';
type SplitLabelProps = {
value: string;
textClassName?: string;
disabled?: boolean;
};

const SplitLabelText: React.FC<SplitLabelProps> = props => {
const {value, textClassName = 'regular'} = props;
const {value, textClassName = 'regular', disabled = false} = props;

if (!labelRegex.test(value)) {
return (
Expand All @@ -28,10 +29,10 @@ const SplitLabelText: React.FC<SplitLabelProps> = props => {

return (
<SplitLabelTextContainer>
<Text color={Colors.slate400} className={textClassName}>
<Text color={disabled ? Colors.slate500 : Colors.slate400} className={textClassName}>
{key}:{' '}
</Text>
<Text color={Colors.slate200} className={textClassName} ellipsis>
<Text color={disabled ? Colors.slate500 : Colors.slate200} className={textClassName} ellipsis>
{rest.join(':')}
</Text>
</SplitLabelTextContainer>
Expand Down
42 changes: 31 additions & 11 deletions src/components/molecules/LabelsSelect/LabelsSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const isValidLabel = (value?: string) => {
return value != null && labelRegex.test(value);
};

const getLabelKey = (label: string) => label.match(/^([^:]+:?)/)?.[0];
const isLabelKey = (label: string, keyWithColon?: string) => Boolean(keyWithColon && label.startsWith(keyWithColon));

const LabelsSelect: React.FC<LabelsSelectProps> = props => {
const {
id,
Expand Down Expand Up @@ -61,24 +64,41 @@ const LabelsSelect: React.FC<LabelsSelectProps> = props => {
}, [options, data]);

const change = useLastCallback((newValue: readonly Option[]) => onChange?.(newValue.map(x => x.value as string)));
const isOptionDisabled = useMemo(
() => (option: Option, selected: readonly Option[]) => {
const key = getLabelKey(option.label);
return Boolean(key) && selected.some(x => isLabelKey(`${x.value}`, key));
},
[]
);
const formatCreateLabel = useMemo(
() => (inputString: string) => {
if (typeof inputString === 'string' && inputString.includes(':')) {
if (!isValidLabel(inputString)) {
return 'Incorrect label value';
}

const key = getLabelKey(inputString)!;
if (value?.some(x => x.startsWith(`${key}:`))) {
return `The label may have only a single value for specified key (${key}:)`;
}

return `Create ${inputString}`;
}

return 'Create: You need to add a : separator to create this label';
},
[value]
);

return (
<CreatableMultiSelect
id={id}
value={formattedValue}
onChange={change}
placeholder={placeholder}
formatCreateLabel={(inputString: string) => {
if (typeof inputString === 'string' && inputString.includes(':')) {
if (!isValidLabel(inputString)) {
return 'Incorrect label value';
}

return `Create ${inputString}`;
}

return 'Create: You need to add a : separator to create this label';
}}
isOptionDisabled={isOptionDisabled}
formatCreateLabel={formatCreateLabel}
options={formattedOptions}
CustomOptionComponent={LabelsOption}
CustomMultiValueLabelComponent={LabelsMultiValueLabel}
Expand Down
4 changes: 2 additions & 2 deletions src/components/molecules/LabelsSelect/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const decomposeLabels = (labels: readonly string[]): Record<string, string> => {
return labels.reduce((previousValue, currentValue: string) => {
export const decomposeLabels = (labels?: readonly string[]): Record<string, string> => {
return (labels || []).reduce((previousValue, currentValue: string) => {
if (!currentValue.includes(':')) {
return {...previousValue, [currentValue]: ''};
}
Expand Down

0 comments on commit 69d176c

Please sign in to comment.