Skip to content

Commit

Permalink
Merge pull request #2526 from NDLANO/refactor/concept-subject-primitives
Browse files Browse the repository at this point in the history
refactor: use primitives for selecting concept subjects
  • Loading branch information
Jonas-C authored Oct 17, 2024
2 parents c4e8ce4 + 10cf12c commit a293f2d
Showing 1 changed file with 121 additions and 13 deletions.
134 changes: 121 additions & 13 deletions src/containers/ConceptPage/components/ConceptMetaData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,45 @@
*/

import { useFormikContext } from "formik";
import keyBy from "lodash/keyBy";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { createListCollection } from "@ark-ui/react";
import { CheckLine } from "@ndla/icons/editor";
import { ComboboxContext, TagsInputContext, createListCollection } from "@ark-ui/react";
import { CloseLine } from "@ndla/icons/action";
import { ArrowDownShortLine } from "@ndla/icons/common";
import {
ComboboxItem,
ComboboxItemIndicator,
ComboboxItemText,
FieldErrorMessage,
FieldHelper,
FieldRoot,
IconButton,
Input,
InputContainer,
TagsInputItem,
TagsInputItemDeleteTrigger,
TagsInputItemInput,
TagsInputItemPreview,
TagsInputItemText,
} from "@ndla/primitives";
import { HStack } from "@ndla/styled-system/jsx";
import { Node } from "@ndla/types-taxonomy";
import { TagSelectorLabel, TagSelectorRoot, useTagSelectorTranslations } from "@ndla/ui";
import {
TagSelectorClearTrigger,
TagSelectorControl,
TagSelectorInputBase,
TagSelectorLabel,
TagSelectorRoot,
TagSelectorTrigger,
useTagSelectorTranslations,
} from "@ndla/ui";
import InlineImageSearch from "./InlineImageSearch";
import MultiSelectDropdown from "../../../components/Dropdown/MultiSelectDropdown";
import { GenericComboboxItemIndicator } from "../../../components/abstractions/Combobox";
import { SearchTagsContent } from "../../../components/Form/SearchTagsContent";
import { SearchTagsTagSelectorInput } from "../../../components/Form/SearchTagsTagSelectorInput";
import { FormField } from "../../../components/FormField";
import FormikField from "../../../components/FormikField";
import { FormContent } from "../../../components/FormikForm";
import { useConceptSearchTags } from "../../../modules/concept/conceptQueries";
import useDebounce from "../../../util/useDebounce";
import { MetaImageSearch } from "../../FormikForm";
Expand All @@ -46,6 +64,7 @@ const ConceptMetaData = ({ subjects, inModal, language }: Props) => {
const tagSelectorTranslations = useTagSelectorTranslations();
const { values } = formikContext;
const [inputQuery, setInputQuery] = useState<string>("");
const [subjectsInputQuery, setSubjectsInputQuery] = useState<string>("");
const debouncedQuery = useDebounce(inputQuery, 300);
const searchTagsQuery = useConceptSearchTags(
{
Expand All @@ -58,6 +77,13 @@ const ConceptMetaData = ({ subjects, inModal, language }: Props) => {
},
);

const keyedSubjects = useMemo(() => keyBy(subjects, (subject) => subject.id), [subjects]);

const filteredSubjects = useMemo(
() => subjects.filter((subject) => subject.name.toLowerCase().includes(subjectsInputQuery)),
[subjects, subjectsInputQuery],
);

const collection = useMemo(() => {
return createListCollection({
items: searchTagsQuery.data?.results ?? [],
Expand All @@ -66,8 +92,16 @@ const ConceptMetaData = ({ subjects, inModal, language }: Props) => {
});
}, [searchTagsQuery.data?.results]);

const subjectsCollection = useMemo(() => {
return createListCollection({
items: filteredSubjects,
itemToString: (item) => item.name,
itemToValue: (item) => item.id,
});
}, [filteredSubjects]);

return (
<>
<FormContent>
{inModal ? (
<InlineImageSearch name="metaImageId" />
) : (
Expand All @@ -85,9 +119,85 @@ const ConceptMetaData = ({ subjects, inModal, language }: Props) => {
)}
</FormikField>
)}
<FormikField name="subjects" label={t("form.subjects.label")} description={t("form.concept.subjects")}>
{({ field }) => <MultiSelectDropdown labelField="name" minSearchLength={1} initialData={subjects} {...field} />}
</FormikField>
<FormField<Node[]> name="subjects">
{({ field, meta, helpers }) => (
<FieldRoot invalid={!!meta.error}>
<TagSelectorRoot
collection={subjectsCollection}
value={field.value.map((subject) => subject.id)}
onValueChange={(details) => {
// only add valid subjects. Triggering the delimiter can lead to an invalid subject being added.
helpers.setValue(details.value.map((id) => keyedSubjects[id]).filter(Boolean));
}}
translations={tagSelectorTranslations}
inputValue={subjectsInputQuery}
onInputValueChange={(details) => setSubjectsInputQuery(details.inputValue)}
// arbitrary delimiter that is hopefully never written
delimiter={"^"}
editable={false}
>
<TagSelectorLabel>{t("form.subjects.label")}</TagSelectorLabel>
<FieldErrorMessage>{meta.error}</FieldErrorMessage>
<FieldHelper>{t("form.concept.subjects")}</FieldHelper>
<HStack gap="3xsmall">
<TagSelectorControl asChild>
<InputContainer>
{field.value.map((value, index) => (
<TagsInputItem index={index} value={value.id} key={value.id}>
<TagsInputItemPreview>
<TagsInputItemText>{value.name}</TagsInputItemText>
<TagsInputItemDeleteTrigger>
<CloseLine />
</TagsInputItemDeleteTrigger>
</TagsInputItemPreview>
<TagsInputItemInput />
</TagsInputItem>
))}
<TagsInputContext>
{(tagsInputApi) => (
<ComboboxContext>
{(comboboxApi) => (
<TagSelectorInputBase
onKeyDown={(e) => {
// only add a new value if the combobox has a highlighted value. We're not allowing custom values.
if (e.key === "Enter" && comboboxApi.highlightedValue) {
tagsInputApi.addValue(comboboxApi.highlightedValue);
}
return;
}}
asChild
>
<Input placeholder={t("form.tags.searchPlaceholder")} />
</TagSelectorInputBase>
)}
</ComboboxContext>
)}
</TagsInputContext>
<TagSelectorClearTrigger asChild>
<IconButton variant="clear">
<CloseLine />
</IconButton>
</TagSelectorClearTrigger>
</InputContainer>
</TagSelectorControl>
<TagSelectorTrigger asChild>
<IconButton variant="secondary">
<ArrowDownShortLine />
</IconButton>
</TagSelectorTrigger>
</HStack>
<SearchTagsContent isFetching={false} hits={subjectsCollection.items.length}>
{subjectsCollection.items.map((item) => (
<ComboboxItem key={item.id} item={item}>
<ComboboxItemText>{item.name}</ComboboxItemText>
<GenericComboboxItemIndicator />
</ComboboxItem>
))}
</SearchTagsContent>
</TagSelectorRoot>
</FieldRoot>
)}
</FormField>
<FormField name="tags">
{({ field, meta, helpers }) => (
<FieldRoot invalid={!!meta.error}>
Expand All @@ -109,17 +219,15 @@ const ConceptMetaData = ({ subjects, inModal, language }: Props) => {
{collection.items.map((item) => (
<ComboboxItem key={item} item={item}>
<ComboboxItemText>{item}</ComboboxItemText>
<ComboboxItemIndicator asChild>
<CheckLine />
</ComboboxItemIndicator>
<GenericComboboxItemIndicator />
</ComboboxItem>
))}
</SearchTagsContent>
</TagSelectorRoot>
</FieldRoot>
)}
</FormField>
</>
</FormContent>
);
};

Expand Down

0 comments on commit a293f2d

Please sign in to comment.