Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

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

Description

@jmeyers91

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;
  }
`;

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions