Skip to content

Commit

Permalink
Merge pull request #1344 from concord-consortium/187782786-filter-per…
Browse files Browse the repository at this point in the history
…mission-forms-refactor

187782786 filter permission forms, fix student modal text wrap and scroll
  • Loading branch information
pjanik committed Jul 3, 2024
2 parents f166902 + 2cded13 commit 7a74a0c
Show file tree
Hide file tree
Showing 9 changed files with 71 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IStudent } from "../students-tab/types";
import { CurrentSelectedProject, IPermissionForm } from "./types";

export const filteredByProject = (forms: IPermissionForm[], projectId: CurrentSelectedProject) => {
return projectId === null
? forms
: forms.filter((form: IPermissionForm) => form.project_id === projectId);
};

export const nonArchived = (forms: IPermissionForm[]) => {
return forms.filter(form => !form.is_archived);
};

export const sortedByArchiveAndName = (forms: IPermissionForm[]) => forms.sort((a, b) => {
if (a.is_archived === b.is_archived) {
return a.name.localeCompare(b.name);
}
return a.is_archived ? 1 : -1;
});

export const formsOfStudent = (forms: IPermissionForm[], studentInfo: IStudent) => {
const ids = studentInfo.permission_forms.map(form => form.id);
return forms.filter(form => ids.includes(form.id));
};
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import React from "react";
import { IProject } from "./types";
import { CurrentSelectedProject, IProject } from "./types";

interface IProjectSelectProps {
projects: IProject[];
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
value?: string | number;
value?: CurrentSelectedProject;
}

export const ProjectSelect = ({ projects, value, onChange }: IProjectSelectProps) => {
const sortedProjects = projects.sort((a, b) => a.name.localeCompare(b.name));
return (
<>
<label>Project:</label>
<select data-testid="project-select" value={value} name="project_id" onChange={onChange}>
<select data-testid="project-select" value={value ?? undefined} name="project_id" onChange={onChange}>
<option value="">Select a project...</option>
{ sortedProjects?.map((p: IProject) => <option key={p.id} value={p.id}>{ p.name }</option>) }
</select>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export type CurrentSelectedProject = number | null;
export interface IPermissionForm {
id: string;
name: string;
is_archived: boolean;
can_delete: boolean;
project_id?: number | string; // need to fix this
project_id: CurrentSelectedProject;
url?: string;
}

Expand All @@ -12,4 +13,3 @@ export interface IProject {
name: string;
}

export type CurrentSelectedProject = number | "";
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const CreateEditPermissionForm = ({ projects, currentSelectedProject, exi
const [formData, setFormData] = useState<IPermissionFormFormData>({
id: existingFormData?.id || undefined,
name: existingFormData?.name || "",
project_id: existingFormData?.project_id || (currentSelectedProject ? Number(currentSelectedProject) : ""),
project_id: existingFormData?.project_id || (currentSelectedProject ? currentSelectedProject : null),
url: existingFormData?.url || ""
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { clsx } from "clsx";
import { useFetch } from "../../../hooks/use-fetch";
import { CreateEditPermissionForm } from "./create-edit-permission-form";
import { IPermissionForm, IPermissionFormFormData, IProject, CurrentSelectedProject } from "./types";
import PermissionFormRow from "./permission-form-row";
import { ProjectSelect } from "../common/project-select";
import ModalDialog from "../../shared/modal-dialog";
import { request } from "../../../helpers/api/request";
import { filteredByProject, sortedByArchiveAndName } from "../common/permission-utils";
import PermissionFormRow from "./permission-form-row";
import ModalDialog from "../../shared/modal-dialog";

import css from "./manage-forms-tab.scss";

Expand All @@ -30,19 +31,6 @@ const deletePermissionForm = async (permissionFormId: string) =>
method: "DELETE"
});

const getFilteredForms = (forms: IPermissionForm[], projectId: string | number) =>
projectId === "" ? forms : forms.filter((form: IPermissionForm) => form.project_id === Number(projectId));

const sortByName = (a: { name: string }, b: { name: string }) => a.name.localeCompare(b.name);

// Sort forms by is_archived first and then by name
const sortForms = (forms: IPermissionForm[]) => forms.sort((a, b) => {
if (a.is_archived === b.is_archived) {
return sortByName(a, b);
}
return a.is_archived ? 1 : -1;
});

export default function ManageFormsTab() {
// Fetch projects and permission forms (with refetch function) on initial load
const { data: permissionsData, refetch: refetchPermissions } = useFetch<IPermissionForm[]>(Portal.API_V1.PERMISSION_FORMS, []);
Expand All @@ -51,10 +39,10 @@ export default function ManageFormsTab() {
// State for UI
const [showCreateNewFormModal, setShowCreateNewFormModal] = useState(false);
const [editForm, setEditForm] = useState<IPermissionForm | false>(false);
const [currentSelectedProject, setCurrentSelectedProject] = useState<number | "">("");
const [currentSelectedProject, setCurrentSelectedProject] = useState<CurrentSelectedProject>(null);

const handleProjectSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setCurrentSelectedProject(e.target.value as CurrentSelectedProject);
setCurrentSelectedProject(e.target.value === "" ? null : Number(e.target.value));
};

const handleCreateFormClick = () => {
Expand Down Expand Up @@ -95,7 +83,7 @@ export default function ManageFormsTab() {
refetchPermissions();
};

const processedForms = sortForms(getFilteredForms(permissionsData, currentSelectedProject));
const processedForms = sortedByArchiveAndName(filteredByProject(permissionsData, currentSelectedProject));
const cantDeleteAnyForm = processedForms.every((form: IPermissionForm) => form.can_delete === false);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,17 @@
padding: 10px;
}

.scrollableWrapper {
max-height: 300px;
overflow-y: scroll;
}

.formRow {
display: flex;
align-items: center;
padding: 10px 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

label {
width: 80px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,22 @@ export const EditStudentPermissionsForm = ({ student, permissionForms, onFormCan
<div className={css.formTop}>
{ `EDIT: ${student.name}` }
</div>
<div className={css.scrollableWrapper}>
{ permissionForms.map((p, i) => {
const isChecked = localPermissions.some(lp => lp.id === p.id);

{ permissionForms.map((p, i) => {
const isChecked = localPermissions.some(lp => lp.id === p.id);

return (
<div key={i} className={css.formRow}>
<input
type="checkbox"
checked={isChecked}
onChange={() => handlePermissionChange(p.id)}
/>
{ p.name }
</div>
);
}) }
return (
<div key={i} className={css.formRow}>
<input
type="checkbox"
checked={isChecked}
onChange={() => handlePermissionChange(p.id)}
/>
{ p.name }
</div>
);
}) }
</div>

<div className={css.formButtonArea}>
<button className={css.cancelButton} onClick={onFormCancel}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ export default function StudentsTab() {
const[teacherName, setTeacherName] = useState<string>("");

// State for UI
const [currentSelectedProject, setCurrentSelectedProject] = useState<number | "">("");
const [currentSelectedProject, setCurrentSelectedProject] = useState<CurrentSelectedProject>(null);

const handleProjectSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setCurrentSelectedProject(e.target.value as CurrentSelectedProject);
setCurrentSelectedProject(e.target.value === "" ? null : Number(e.target.value));
};

const handleTeacherNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import Select from "react-select";
import { useFetch } from "../../../hooks/use-fetch";
import { request } from "../../../helpers/api/request";
import { CurrentSelectedProject, IPermissionForm, IStudent } from "./types";
import ModalDialog from "../../shared/modal-dialog";
import { EditStudentPermissionsForm } from "./edit-student-permissions-form";

Check warning on line 6 in rails/react-components/src/library/components/permission-forms-v2/students-tab/students-table.tsx

View workflow job for this annotation

GitHub Actions / Run react-components tests

Dependency cycle detected
import { filteredByProject, formsOfStudent, nonArchived } from "../common/permission-utils";
import ModalDialog from "../../shared/modal-dialog";

import css from "./students-table.scss";

Expand All @@ -18,7 +19,6 @@ type PermissionFormOption = {
label: string;
};

const nonArchived = (forms: IPermissionForm[]) => forms.filter(form => !form.is_archived);

export const bulkUpdatePermissionForms = async (
{ classId, selectedStudentIds, addFormIds, removeFormIds }:
Expand All @@ -35,7 +35,7 @@ export const bulkUpdatePermissionForms = async (
})
});

export const StudentsTable = ({ classId }: IProps) => {
export const StudentsTable = ({ classId, currentSelectedProject }: IProps) => {
const { data: studentsData, isLoading: studentsLoading, refetch: refetchStudentsData } =
useFetch<IStudent[]>(Portal.API_V1.permissionFormsClassPermissionForms(classId), []);
const { data: permissionForms, isLoading: permissionFormsLoading } = useFetch<IPermissionForm[]>(Portal.API_V1.PERMISSION_FORMS, []);
Expand All @@ -46,13 +46,14 @@ export const StudentsTable = ({ classId }: IProps) => {
const [requestInProgress, setRequestInProgress] = useState(false);
const [permissionsExpanded, setPermissionsExpanded] = useState(false);

const nonArchivedPermissionForms = nonArchived(permissionForms);
const currentForms = filteredByProject(nonArchived(permissionForms), currentSelectedProject);

const permissionFormToAddOptions = Object.freeze(
nonArchivedPermissionForms.filter(pf => !permissionFormsToRemove.find(pfr => pfr.value === pf.id)).map(pf => ({ value: pf.id, label: pf.name }))
currentForms.filter(pf => !permissionFormsToRemove.find(pfr => pfr.value === pf.id)).map(pf => ({ value: pf.id, label: pf.name }))
);

const permissionFormToRemoveOptions = Object.freeze(
nonArchivedPermissionForms.filter(pf => !permissionFormsToAdd.find(pfr => pfr.value === pf.id)).map(pf => ({ value: pf.id, label: pf.name }))
currentForms.filter(pf => !permissionFormsToAdd.find(pfr => pfr.value === pf.id)).map(pf => ({ value: pf.id, label: pf.name }))
);

// studentsData.length === 0 prevents the "Loading..." message from showing up when the students are re-fetched after update.
Expand Down Expand Up @@ -159,6 +160,7 @@ export const StudentsTable = ({ classId }: IProps) => {
<tbody>
{
studentsData.map((studentInfo) => {
const studentForms = formsOfStudent(currentForms, studentInfo);
return (
<tr key={studentInfo.id}>
<td className={css.checkboxColumn}>
Expand All @@ -168,7 +170,7 @@ export const StudentsTable = ({ classId }: IProps) => {
<td>{ studentInfo.login }</td>
<td className={css.permissionFormsColumn}>
{
nonArchived(studentInfo.permission_forms).map((pf, i, forms) => (
studentForms.map((pf, i, forms) => (
<React.Fragment key={pf.id}>
{ pf.name }
{ i < forms.length - 1 && (permissionsExpanded ? <br /> : ", ") }
Expand Down Expand Up @@ -244,7 +246,7 @@ export const StudentsTable = ({ classId }: IProps) => {
<ModalDialog borderColor="teal">
<EditStudentPermissionsForm
student={editStudent}
permissionForms={permissionForms}
permissionForms={currentForms}
onFormCancel={() => setEditStudent(null)}
onFormSave={handleSaveStudentPermissionsSuccess}
classId={classId}
Expand Down

0 comments on commit 7a74a0c

Please sign in to comment.