Khan / react-multi-select

A multiple select component for React
MIT License
183 stars 97 forks source link

"All items are selected" displayed when all items aren't selected #64

Open jmeyers91 opened 4 years ago

jmeyers91 commented 4 years ago

I've got a form with several multi-select components on it and the options for some of them are dependent on which options are selected in others. The problem I'm running into is when I check and uncheck options in the select that is depended on, the other select seems to end up in a state where some but not all options are selected, but it shows all options as selected.

Here's a screenshot of what I mean: Screen Shot 2020-01-09 at 12 44 00 PM

And the code for the form:

import React, { FC, useState, useMemo, useCallback } from "react";
import styled from "styled-components/macro";
import MultiSelect from "@khanacademy/react-multi-select";
import { IoIosCalendar } from "react-icons/io";
import { FiDownload } from "react-icons/fi";
import Form from "./Form";
import Row from "./Row";
import useAsyncEffect from "../utils/useAsyncEffect";
import getTeachers from "../api/teachers/getTeachers";
import Teacher from "shared/lib/types/Teacher";
import capitalizeFirst from "shared/lib/utils/capitalizeFirst";
import getAssignmentCategories from "../api/assignments/getAssignmentCategories";
import AssignmentCategory from "shared/lib/types/AssignmentCategory";
import getAllUnits from "../api/units/getAllUnits";
import Unit from "shared/lib/types/Unit";
import Assignment from "shared/lib/types/Assignment";
import getAllAssignments from "../api/assignments/getAllAssignments";
import LabeledSwitch from "./LabeledSwitch";
import DateInput from "./DateInput";
import Column from "./Column";
import BlockButton from "./BlockButton";
import ResponseCsvOptions from "shared/lib/types/ResponseCsvOptions";

interface Props {
  onSubmit(value: Value): any;
}

interface Data {
  teachers: Teacher[];
  categories: AssignmentCategory[];
  units: Unit[];
  assignments: Assignment[];
}

export type Value = ResponseCsvOptions;

const CsvExportForm: FC<Props> = props => {
  const { onSubmit, ...rest } = props;
  const [data, setData] = useState<Data | null>(null);
  const [value, setValue] = useState<Value>({
    startDate: null,
    endDate: null,
    teacherIds: [],
    unitIds: [],
    assignmentIds: [],
    categoryIds: [],
    includeTeacherUnits: false
  });

  useAsyncEffect(async () => {
    const teachers = await getTeachers();
    const categories = await getAssignmentCategories();
    const units = await getAllUnits();
    const assignments = await getAllAssignments();

    setData({
      teachers,
      categories,
      units,
      assignments
    });
  }, []);

  const teacherOptions = useMemo(() => {
    if (!data) {
      return [];
    }
    return data.teachers.map(teacher => ({
      value: teacher.id,
      label: `${capitalizeFirst(teacher.firstName)} ${capitalizeFirst(
        teacher.lastName
      )}`
    }));
  }, [data]);

  const unitOptions = useMemo(() => {
    if (!data) {
      return [];
    }
    return data.units
      .filter(unit => {
        // Skip units that belong to unselected teachers
        if (unit.teacherId && !value.teacherIds.includes(unit.teacherId)) {
          return false;
        }

        // Skip units that belong to unselected categories
        if (unit.categoryId) {
          return value.categoryIds.includes(unit.categoryId);
        } else {
          // Only show teacher units if the teacher authored switch is on
          return value.includeTeacherUnits;
        }
      })
      .map(unit => ({
        value: unit.id,
        label: unit.name
      }));
  }, [data, value]);

  const assignmentOptions = useMemo(() => {
    if (!data) {
      return [];
    }
    return data.assignments
      .filter(assignment => {
        return value.unitIds.includes(assignment.unitId);
      })
      .map(assignment => ({
        value: assignment.id,
        label: `${assignment.title} (${assignment.subTitle})`
      }));
  }, [data, value]);

  const handleTeachersChange = useCallback(
    (teacherIds: number[]) => {
      setValue(value => {
        if (!data) {
          return value;
        }
        const newUnitIds = value.unitIds.filter(unitId => {
          const unit = data.units.find(unit => unit.id === unitId);
          if (!unit) {
            return false;
          }
          if (unit.teacherId) {
            return (
              value.includeTeacherUnits && teacherIds.includes(unit.teacherId)
            );
          }
          return true;
        });

        return { ...value, teacherIds, unitIds: newUnitIds };
      });
    },
    [data]
  );

  const handleUnitsChange = useCallback((unitIds: number[]) => {
    setValue(value => ({ ...value, unitIds }));
  }, []);

  const handleAssignmentsChange = useCallback((assignmentIds: number[]) => {
    setValue(value => ({ ...value, assignmentIds }));
  }, []);

  const handleCategoriesChange = useCallback((categoryId: number) => {
    setValue(value => {
      const { categoryIds } = value;
      let newCategoryIds;

      if (categoryIds.includes(categoryId)) {
        newCategoryIds = categoryIds.filter(id => id !== categoryId);
      } else {
        newCategoryIds = [...categoryIds, categoryId];
      }

      return {
        ...value,
        categoryIds: newCategoryIds
      };
    });
  }, []);

  const handleTeacherUnitsChange = useCallback((teacherUnits: boolean) => {
    setValue(value => ({ ...value, includeTeacherUnits: teacherUnits }));
  }, []);

  const handleSubmit = useCallback(() => {
    onSubmit(value);
  }, [onSubmit, value]);

  if (!data) {
    return null;
  }

  return (
    <Form {...rest} onSubmit={handleSubmit}>
      <SectionLabel>Date Range</SectionLabel>
      <Section>
        <DateInputColumn>
          <DateInputLabel>Start</DateInputLabel>
          <StyledDateInput placeholder="mm/dd/yyyy" />
          <IoIosCalendar size={28} />
        </DateInputColumn>

        <DateInputColumn>
          <DateInputLabel>End</DateInputLabel>
          <StyledDateInput placeholder="mm/dd/yyyy" />
          <IoIosCalendar size={28} />
        </DateInputColumn>
      </Section>

      <SectionLabel>Products</SectionLabel>
      <Section>
        {data.categories.map(category => (
          <LabeledSwitch
            key={category.id}
            checked={value.categoryIds.includes(category.id)}
            onChange={() => handleCategoriesChange(category.id)}
          >
            {category.name}
          </LabeledSwitch>
        ))}
        <LabeledSwitch
          checked={value.includeTeacherUnits}
          onChange={handleTeacherUnitsChange}
        >
          TEACHER AUTHORED
        </LabeledSwitch>
      </Section>

      <SectionLabel>Units</SectionLabel>
      <Section>
        <MultiSelect
          selected={value.unitIds}
          options={unitOptions}
          onSelectedChanged={handleUnitsChange}
        />
      </Section>

      <SectionLabel>Assignments</SectionLabel>
      <Section>
        <MultiSelect
          selected={value.assignmentIds}
          options={assignmentOptions}
          onSelectedChanged={handleAssignmentsChange}
        />
      </Section>

      <SectionLabel>Teachers</SectionLabel>
      <Section>
        <MultiSelect
          selected={value.teacherIds}
          options={teacherOptions}
          onSelectedChanged={handleTeachersChange}
        />
      </Section>

      <DownloadButton>
        <FiDownload size={20} />
        Download CSV
      </DownloadButton>
    </Form>
  );
};

export default styled(CsvExportForm)`
  .multi-select {
    width: 459px;
  }

  .dropdown-heading {
    border-radius: 8px !important;
  }

  .dropdown-heading-dropdown-arrow {
    background-color: #979797;
    width: 35px !important;
    padding-right: 0 !important;
  }

  .dropdown-heading-dropdown-arrow > span {
    border-color: white transparent transparent !important;
  }

  .dropdown[aria-expanded="true"] .dropdown-heading-dropdown-arrow > span {
    border-color: transparent transparent white !important;
  }
`;

const SectionLabel = styled("h3")`
  color: #000000;
  font-family: Lato;
  font-size: 21px;
  line-height: 25px;
  margin-bottom: 20px;
  font-weight: normal;
`;

const Section = styled(Row)`
  padding-left: 23px;
  margin-bottom: 32px;

  ${LabeledSwitch} + ${LabeledSwitch} {
    margin-left: 40px;
  }
`;

const StyledDateInput = styled(DateInput)`
  input {
    height: 37px;
    width: 219px;
    border: 1px solid #979797;
    border-radius: 8px;
    background-color: #ffffff;
    padding-left: 8px;
  }

  input::placeholder {
    color: #000000;
    font-size: 14px;
    line-height: 17px;
  }
`;

const DateInputColumn = styled(Column)`
  position: relative;

  & + & {
    margin-left: 21px;
  }

  svg {
    pointer-events: none;
    position: absolute;
    right: 13px;
    top: 24px;
  }
`;

const DateInputLabel = styled("label")`
  color: #7e7e7e;
  font-size: 14px;
  font-weight: 900;
  margin-bottom: 3px;
  margin-left: 8px;
`;

const DownloadButton = styled(BlockButton)`
  width: 160px;
  background-color: #000000;
  color: #fff;
  font-size: 14px;
  font-weight: 500;
  margin-left: 23px;

  svg {
    margin-right: 8px;
  }
`;
ambreine31 commented 4 years ago

I have the same problem tell me if you found a solution !

sahil-isima commented 4 years ago

I also got into similar issue for couple of minutes. But later figured out that i am only sending wrong value of selected array. Make sure you are sending values with false etc in it. It select Select All option based on following: length of option === length of selected item