portainer/portainer

View on GitHub
app/react/components/TagSelector/TagSelector.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
import _ from 'lodash';

import { TagId } from '@/portainer/tags/types';
import { useCreateTagMutation, useTags } from '@/portainer/tags/queries';

import { Creatable, Select } from '@@/form-components/ReactSelect';
import { FormControl } from '@@/form-components/FormControl';
import { Link } from '@@/Link';

import { TagButton } from '../TagButton';

interface Props {
  value: TagId[];
  allowCreate?: boolean;
  onChange(value: TagId[]): void;
}

interface Option {
  value: TagId;
  label: string;
}

export function TagSelector({ value, allowCreate = false, onChange }: Props) {
  // change the struct because react-select has a bug with Creatable (https://github.com/JedWatson/react-select/issues/3417#issuecomment-461868989)
  const tagsQuery = useTags({
    select: (tags) => tags?.map((opt) => ({ label: opt.Name, value: opt.ID })),
  });

  const createTagMutation = useCreateTagMutation();

  if (!tagsQuery.data) {
    return null;
  }

  const { data: tags } = tagsQuery;

  const selectedTags = _.compact(
    value.map((id) => tags.find((tag) => tag.value === id))
  );

  const SelectComponent = allowCreate ? Creatable : Select;

  if (!tags.length && !allowCreate) {
    return (
      <div className="form-group">
        <div className="col-sm-12 small text-muted">
          No tags available. Head over to the
          <Link
            to="portainer.tags"
            className="space-right space-left"
            data-cy="environment-tags-view-link"
          >
            Tags view
          </Link>
          to add tags
        </div>
      </div>
    );
  }

  return (
    <>
      {value.length > 0 && (
        <FormControl label="Selected tags">
          {selectedTags.map((tag) => (
            <TagButton
              key={tag.value}
              title="Remove tag"
              value={tag.value}
              label={tag.label}
              onRemove={() => handleRemove(tag.value)}
            />
          ))}
        </FormControl>
      )}

      <FormControl label="Tags" inputId="tags-selector">
        <SelectComponent
          inputId="tags-selector"
          value={[] as { label: string; value: number }[]}
          hideSelectedOptions
          options={tags.filter((tag) => !value.includes(tag.value))}
          closeMenuOnSelect={false}
          onChange={handleAdd}
          noOptionsMessage={() => 'No tags available'}
          formatCreateLabel={(inputValue) => `Create "${inputValue}"`}
          onCreateOption={handleCreateOption}
          aria-label="Tags"
          data-cy="environment-tags-selector"
        />
      </FormControl>
    </>
  );

  function handleAdd(tag?: Option | null) {
    if (!tag) {
      return;
    }
    onChange([...value, tag.value]);
  }

  function handleRemove(tagId: TagId) {
    onChange(value.filter((id) => id !== tagId));
  }

  function handleCreateOption(inputValue: string) {
    if (!allowCreate) {
      return;
    }

    // Prevent the new tag composed of space from being added
    if (!inputValue.replace(/\s/g, '').length) {
      return;
    }

    createTagMutation.mutate(inputValue, {
      onSuccess(tag) {
        handleAdd({ label: tag.Name, value: tag.ID });
      },
    });
  }
}