Skip to content

Commit

Permalink
Merge pull request #2559 from NDLANO/refactor/subject-page-links
Browse files Browse the repository at this point in the history
refactor: use combobox in subject page links
  • Loading branch information
Jonas-C authored Oct 30, 2024
2 parents 2bcd8cf + 4b96095 commit 02ddf15
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
*/

import { FormikErrors } from "formik";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";

import { PageContent } from "@ndla/primitives";
import { IArticle } from "@ndla/types-backend/draft-api";
import { ILearningPathV2 } from "@ndla/types-backend/learningpath-api";
import { Node } from "@ndla/types-taxonomy";

import SubjectpageAbout from "./SubjectpageAbout";
import SubjectpageArticles from "./SubjectpageArticles";
Expand All @@ -22,7 +20,7 @@ import SubjectpageSubjectlinks from "./SubjectpageSubjectlinks";
import FormAccordion from "../../../components/Accordion/FormAccordion";
import FormAccordions from "../../../components/Accordion/FormAccordions";
import FormikField from "../../../components/FormikField";
import { useSearchNodes } from "../../../modules/nodes/nodeQueries";
import { FormContent } from "../../../components/FormikForm";
import { SubjectPageFormikType } from "../../../util/subjectHelpers";

interface Props {
Expand All @@ -37,37 +35,6 @@ interface Props {
const SubjectpageAccordionPanels = ({ buildsOn, connectedTo, editorsChoices, elementId, errors, leadsTo }: Props) => {
const { t } = useTranslation();

const subjectsLinks = buildsOn.concat(connectedTo).concat(leadsTo);

const { data: nodeData } = useSearchNodes(
{
page: 1,
taxonomyVersion: "default",
nodeType: "SUBJECT",
pageSize: subjectsLinks.length,
ids: subjectsLinks,
},
{ enabled: subjectsLinks.length > 0 },
);

const subjectLinks = useMemo(() => {
if (nodeData && nodeData.results.length > 0) {
return nodeData.results;
}
return null;
}, [nodeData]);

const transformToNodes = (list: string[]) => {
const nodeList: Node[] = [];
for (const i in list) {
const nodeFound = subjectLinks?.find((value) => value.id === list[i]);
if (nodeFound) {
nodeList.push(nodeFound);
}
}
return nodeList;
};

const SubjectPageArticle = () => (
<SubjectpageArticles editorsChoices={editorsChoices} elementId={elementId} fieldName={"editorsChoices"} />
);
Expand Down Expand Up @@ -95,9 +62,11 @@ const SubjectpageAccordionPanels = ({ buildsOn, connectedTo, editorsChoices, ele
title={t("subjectpageForm.subjectlinks")}
hasError={["connectedTo", "buildsOn", "leadsTo"].some((field) => field in errors)}
>
<SubjectpageSubjectlinks subjects={transformToNodes(connectedTo)} fieldName={"connectedTo"} />
<SubjectpageSubjectlinks subjects={transformToNodes(buildsOn)} fieldName={"buildsOn"} />
<SubjectpageSubjectlinks subjects={transformToNodes(leadsTo)} fieldName={"leadsTo"} />
<FormContent>
<SubjectpageSubjectlinks subjectIds={connectedTo} fieldName={"connectedTo"} />
<SubjectpageSubjectlinks subjectIds={buildsOn} fieldName={"buildsOn"} />
<SubjectpageSubjectlinks subjectIds={leadsTo} fieldName={"leadsTo"} />
</FormContent>
</FormAccordion>
<FormAccordion
id="articles"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,35 @@ import { useField } from "formik";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Node } from "@ndla/types-taxonomy";
import { NodeList, NodeSearchDropdown } from "./nodes";
import FieldHeader from "../../../components/Field/FieldHeader";
import { NodeList } from "./nodes";
import { NodeSearchDropdown } from "./nodes/NodeSearchDropdown";
import { searchNodes } from "../../../modules/nodes/nodeApi";
import { useTaxonomyVersion } from "../../StructureVersion/TaxonomyVersionProvider";

interface Props {
subjects: Node[];
subjectIds: string[];
fieldName: string;
}

const SubjectpageSubjectlinks = ({ subjects, fieldName }: Props) => {
const SubjectpageSubjectlinks = ({ subjectIds, fieldName }: Props) => {
const { t } = useTranslation();
const [subjectList, setSubjectList] = useState<Node[]>([]);
const [field, _meta, helpers] = useField<string[]>(fieldName);
const [_field, __, helpers] = useField<string[]>(fieldName);
const { taxonomyVersion } = useTaxonomyVersion();

useEffect(() => {
setSubjectList(subjects);
}, [subjects]);
(async () => {
if (!subjectList.length && subjectIds.length) {
const nodes = await searchNodes({ ids: subjectIds, taxonomyVersion });
setSubjectList(nodes.results);
}
})();
}, [subjectIds, subjectList.length, taxonomyVersion]);

const handleAddToList = (node: Node) => {
const updatedList = [...subjectList, node];
setSubjectList(updatedList);
updateFormik(updatedList.map((subject) => subject.id));
const newValue = subjectList.concat(node);
setSubjectList(newValue);
updateFormik(newValue.map((node) => node.id));
};

const onUpdateNodes = (updatedList: Node[]) => {
Expand All @@ -40,19 +48,17 @@ const SubjectpageSubjectlinks = ({ subjects, fieldName }: Props) => {

const updateFormik = (list: string[]) => {
helpers.setTouched(true, false);
field.onChange({
target: {
name: fieldName,
value: list || null,
},
});
helpers.setValue(list || null, true);
};

return (
<>
<FieldHeader title={t(`subjectpageForm.${fieldName}`)} />
<NodeList nodes={subjectList} nodeSet={fieldName} onUpdate={onUpdateNodes} />
<NodeSearchDropdown selectedItems={subjectList} onChange={handleAddToList} wide={false} />
<NodeSearchDropdown
selectedItems={subjectList}
onChange={handleAddToList}
label={t(`subjectpageForm.${fieldName}`)}
/>
<NodeList nodes={subjectList} onUpdate={onUpdateNodes} />
</>
);
};
Expand Down
90 changes: 35 additions & 55 deletions src/containers/EditSubjectFrontpage/components/nodes/NodeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,73 +8,53 @@

import { useTranslation } from "react-i18next";

import styled from "@emotion/styled";
import { IconButtonV2 } from "@ndla/button";
import { colors, spacing } from "@ndla/core";
import { DeleteForever, DragHorizontal } from "@ndla/icons/editor";
import { DragVertical } from "@ndla/icons/editor";
import { styled } from "@ndla/styled-system/jsx";
import { Node } from "@ndla/types-taxonomy";

import DndList from "../../../../components/DndList";
import { DragHandle } from "../../../../components/DraggableItem";
import ListResource from "../../../../components/Form/ListResource";
import { routes } from "../../../../util/routeHelpers";

const NodeWrapper = styled.div`
align-items: center;
background: ${colors.brand.greyLighter};
display: flex;
justify-content: space-between;
margin: ${spacing.xxsmall};
padding: ${spacing.xxsmall};
padding-left: ${spacing.small};
width: 100%;
`;

const ActionsContainer = styled.div`
display: flex;
`;

const DraggableIconButton = styled(IconButtonV2)`
cursor: grabbing;
`;
const StyledList = styled("ul", {
base: {
listStyle: "none",
},
});

interface Props {
nodes: Node[];
nodeSet: string;
onUpdate: Function;
onUpdate: (value: Node[]) => void;
}

const NodeList = ({ nodes, nodeSet, onUpdate }: Props) => {
const NodeList = ({ nodes, onUpdate }: Props) => {
const { t } = useTranslation();
if (!nodes.length) return null;

return (
<DndList
items={nodes}
onDragEnd={(_, newArray) => onUpdate(newArray)}
renderItem={(node, index) => {
return (
<NodeWrapper key={`${nodeSet}-${node.id}-${index}`}>
{node.name}
<ActionsContainer>
<DraggableIconButton
aria-label={t("subjectpageForm.moveSubject")}
colorTheme="light"
variant="ghost"
title={t("subjectpageForm.moveSubject")}
>
<DragHorizontal />
</DraggableIconButton>
<IconButtonV2
aria-label={t("subjectpageForm.removeSubject")}
colorTheme="danger"
onClick={() => onUpdate(nodes.filter((item) => item.id !== node.id))}
variant="ghost"
title={t("subjectpageForm.removeSubject")}
>
<DeleteForever />
</IconButtonV2>
</ActionsContainer>
</NodeWrapper>
);
}}
/>
<StyledList>
<DndList
items={nodes}
onDragEnd={(_, newArray) => onUpdate(newArray)}
dragHandle={
<DragHandle aria-label={t("form.file.changeOrder")}>
<DragVertical />
</DragHandle>
}
renderItem={(item) => {
return (
<ListResource
key={item.id}
title={item.name}
url={routes.structure(item.url)}
onDelete={() => onUpdate(nodes.filter((node) => node.id !== item.id))}
removeElementTranslation={t("subjectpageForm.removeArticle")}
/>
);
}}
/>
</StyledList>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,59 @@

import { useTranslation } from "react-i18next";

import { ComboboxLabel } from "@ndla/primitives";
import { Node } from "@ndla/types-taxonomy";

import { GenericComboboxInput, GenericComboboxItemContent } from "../../../../components/abstractions/Combobox";
import { GenericSearchCombobox } from "../../../../components/Form/GenericSearchCombobox";
import { useSearchNodes } from "../../../../modules/nodes/nodeQueries";
import SearchDropdown from "../../../StructurePage/folderComponents/sharedMenuOptions/components/SearchDropdown";
import { usePaginatedQuery } from "../../../../util/usePaginatedQuery";
import { useTaxonomyVersion } from "../../../StructureVersion/TaxonomyVersionProvider";

interface Props {
onChange: (t: any) => void;
onChange: (t: Node) => void;
selectedItems: Node[];
wide?: boolean;
label: string;
}

const NodeSearchDropdown = ({ onChange, selectedItems, wide }: Props) => {
export const NodeSearchDropdown = ({ onChange, selectedItems, label }: Props) => {
const { query, delayedQuery, setQuery, page, setPage } = usePaginatedQuery();
const { t } = useTranslation();

const { taxonomyVersion } = useTaxonomyVersion();

const searchQuery = useSearchNodes(
{
taxonomyVersion,
query: delayedQuery,
page,
nodeType: "SUBJECT",
},
{ placeholderData: (prev) => prev },
);

return (
<SearchDropdown
selectedItems={selectedItems}
id="search-dropdown"
onChange={onChange}
placeholder={t("subjectpageForm.addSubject")}
useQuery={useSearchNodes}
params={{ taxonomyVersion, nodeType: "SUBJECT" }}
transform={(res: any) => {
return {
...res,
results: res.results.map((r: any) => ({
originalItem: r,
id: r.id,
name: r.name,
description: r.breadcrumbs?.join(" > "),
disabled: false,
})),
};
}}
wide={wide}
positionAbsolute={false}
isMultiSelect
/>
<GenericSearchCombobox
items={searchQuery.data?.results ?? []}
itemToString={(item) => item.name}
itemToValue={(item) => item.id}
isItemDisabled={(item) => selectedItems.some((selectedItem) => selectedItem.id === item.id)}
onValueChange={(details) => onChange(details.items[details.items.length - 1])}
paginationData={searchQuery.data}
isSuccess={searchQuery.isSuccess}
inputValue={query}
onInputValueChange={(details) => setQuery(details.inputValue)}
onPageChange={(details) => setPage(details.page)}
value={selectedItems.map((item) => item.id)}
selectionBehavior="preserve"
multiple
css={{ width: "100%" }}
renderItem={(item) => (
<GenericComboboxItemContent title={item.name} description={item.breadcrumbs?.join(" > ")} />
)}
>
<ComboboxLabel>{label}</ComboboxLabel>
<GenericComboboxInput placeholder={t("subjectpageForm.addSubject")} isFetching={searchQuery.isFetching} />
</GenericSearchCombobox>
);
};

export default NodeSearchDropdown;
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@
*/

export { default as NodeList } from "./NodeList";
export { default as NodeSearchDropdown } from "./NodeSearchDropdown";

0 comments on commit 02ddf15

Please sign in to comment.