Skip to content

Commit 375ac1f

Browse files
committed
separate topic and resource type filter
1 parent 5516091 commit 375ac1f

File tree

7 files changed

+173
-96
lines changed

7 files changed

+173
-96
lines changed

src/containers/SearchPageV2/ResourceTypeFilter.tsx

+137-84
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,23 @@ import {
2323
CheckboxRoot,
2424
Heading,
2525
IconButton,
26+
RadioGroupItem,
27+
RadioGroupItemControl,
28+
RadioGroupItemHiddenInput,
29+
RadioGroupItemText,
30+
RadioGroupLabel,
31+
RadioGroupRoot,
2632
Spinner,
2733
Text,
2834
} from "@ndla/primitives";
2935
import { styled } from "@ndla/styled-system/jsx";
3036
import { FilterContainer } from "./FilterContainer";
37+
import { RESOURCE_NODE_TYPE, TOPIC_NODE_TYPE } from "./searchUtils";
3138
import {
32-
GQLResourceTypeDefinition,
3339
GQLResourceTypeFilter_BucketResultFragment,
3440
GQLResourceTypeFilter_ResourceTypeDefinitionFragment,
3541
} from "../../graphqlTypes";
42+
import { useLtiContext } from "../../LtiContext";
3643
import { useStableSearchParams } from "../../util/useStableSearchParams";
3744

3845
const DELIMITER = "//";
@@ -74,9 +81,30 @@ const StyledAccordionItemContent = styled(AccordionItemContent, {
7481
},
7582
});
7683

84+
const RadioButtonWrapper = styled("div", {
85+
base: {
86+
display: "flex",
87+
gap: "small",
88+
flexWrap: "wrap",
89+
},
90+
});
91+
92+
const StyledRadioGroupRoot = styled(RadioGroupRoot, {
93+
base: {
94+
_horizontal: {
95+
flexDirection: "column",
96+
},
97+
},
98+
});
99+
100+
const NODE_TYPES = [RESOURCE_NODE_TYPE, TOPIC_NODE_TYPE];
101+
77102
export const ResourceTypeFilter = ({ bucketResult, resourceTypes: resourceTypesProp, resourceTypesLoading }: Props) => {
78103
const [searchParams, setSearchParams] = useStableSearchParams();
79104
const { t } = useTranslation();
105+
const isLti = useLtiContext();
106+
107+
const nodeType = useMemo(() => searchParams.get("type") ?? RESOURCE_NODE_TYPE, [searchParams]);
80108

81109
const keyedBucketResult = useMemo(() => {
82110
return bucketResult.reduce<Record<string, number>>((acc, curr) => {
@@ -86,20 +114,15 @@ export const ResourceTypeFilter = ({ bucketResult, resourceTypes: resourceTypesP
86114
}, [bucketResult]);
87115

88116
const resourceTypes = useMemo(() => {
89-
const types = resourceTypesProp;
90-
const topicArticleType: GQLResourceTypeDefinition = {
91-
id: "topic-article",
92-
name: t("contentTypes.topic-article"),
93-
};
94-
return [topicArticleType].concat(types).map((type) => ({
117+
return resourceTypesProp.map((type) => ({
95118
...type,
96119
id: type.id.replace("urn:resourcetype:", ""),
97120
subtypes: type.subtypes?.map((subtype) => ({
98121
...subtype,
99122
id: subtype.id.replace("urn:resourcetype:", ""),
100123
})),
101124
}));
102-
}, [resourceTypesProp, t]);
125+
}, [resourceTypesProp]);
103126

104127
const currentResourceTypeIds = useMemo(() => searchParams.get("resourceTypes")?.split(",") ?? [], [searchParams]);
105128

@@ -129,25 +152,109 @@ export const ResourceTypeFilter = ({ bucketResult, resourceTypes: resourceTypesP
129152
[currentResourceTypeIds, resourceTypes, setSearchParams],
130153
);
131154

155+
const onChangeNodeType = useCallback(
156+
(id: string) => {
157+
setSearchParams({ resourceTypes: null, type: id === RESOURCE_NODE_TYPE ? null : id });
158+
},
159+
[setSearchParams],
160+
);
161+
132162
return (
133163
<FilterContainer>
134164
<Heading textStyle="label.medium" fontWeight="bold" asChild consumeCss>
135165
<h3>{t("searchPage.resourceTypeFilter.title")}</h3>
136166
</Heading>
137-
{resourceTypesLoading ? (
138-
<Spinner />
139-
) : (
140-
<StyledAccordionRoot variant="clean" multiple>
141-
{resourceTypes?.map((resourceType) =>
142-
resourceType.subtypes?.length ? (
143-
<AccordionItem key={resourceType.id} value={resourceType.id}>
144-
<FilterWrapper>
167+
{!isLti && (
168+
<StyledRadioGroupRoot
169+
orientation="horizontal"
170+
value={nodeType}
171+
onValueChange={(details) => onChangeNodeType(details.value)}
172+
>
173+
<RadioGroupLabel textStyle="label.small">{t("searchPage.resourceTypeFilter.radioLabel")}</RadioGroupLabel>
174+
<RadioButtonWrapper>
175+
{NODE_TYPES.map((type) => (
176+
<RadioGroupItem key={type} value={type}>
177+
<RadioGroupItemControl />
178+
<RadioGroupItemText>{t(`searchPage.resourceTypeFilter.${type}Label`)}</RadioGroupItemText>
179+
<RadioGroupItemHiddenInput />
180+
</RadioGroupItem>
181+
))}
182+
</RadioButtonWrapper>
183+
</StyledRadioGroupRoot>
184+
)}
185+
<div hidden={nodeType !== RESOURCE_NODE_TYPE}>
186+
{resourceTypesLoading ? (
187+
<Spinner />
188+
) : (
189+
<StyledAccordionRoot variant="clean" multiple>
190+
{resourceTypes?.map((resourceType) =>
191+
resourceType.subtypes?.length ? (
192+
<AccordionItem key={resourceType.id} value={resourceType.id}>
193+
<FilterWrapper>
194+
<CheckboxRoot
195+
value={resourceType.id}
196+
checked={
197+
currentResourceTypeIds.includes(resourceType.id) ||
198+
resourceType.subtypes.some((s) => currentResourceTypeIds.includes(s.id))
199+
}
200+
onCheckedChange={(details) => onToggleResourceType(resourceType.id, details.checked === true)}
201+
>
202+
<CheckboxControl>
203+
<CheckboxIndicator asChild>
204+
<CheckLine />
205+
</CheckboxIndicator>
206+
</CheckboxControl>
207+
<CheckboxLabel>{resourceType.name}</CheckboxLabel>
208+
<CheckboxHiddenInput />
209+
</CheckboxRoot>
210+
<AccordionItemTrigger asChild>
211+
<IconButton
212+
variant="tertiary"
213+
size="small"
214+
aria-label={t("searchPage.resourceTypeFilter.showSubtypes", { parent: resourceType.name })}
215+
title={t("searchPage.resourceTypeFilter.showSubtypes", { parent: resourceType.name })}
216+
>
217+
<AccordionItemIndicator asChild>
218+
<ArrowDownShortLine size="medium" />
219+
</AccordionItemIndicator>
220+
</IconButton>
221+
</AccordionItemTrigger>
222+
</FilterWrapper>
223+
<StyledAccordionItemContent>
224+
{resourceType.subtypes.map((subtype) => (
225+
<FilterWrapper key={subtype.id}>
226+
<CheckboxRoot
227+
value={subtype.id}
228+
checked={currentResourceTypeIds.includes(subtype.id)}
229+
onCheckedChange={(details) =>
230+
onToggleResourceType(
231+
`${resourceType.id}${DELIMITER}${subtype.id}`,
232+
details.checked === true,
233+
)
234+
}
235+
>
236+
<CheckboxControl>
237+
<CheckboxIndicator asChild>
238+
<CheckLine />
239+
</CheckboxIndicator>
240+
</CheckboxControl>
241+
<CheckboxLabel>{subtype.name}</CheckboxLabel>
242+
<CheckboxHiddenInput />
243+
</CheckboxRoot>
244+
{keyedBucketResult[subtype.id] != null && (
245+
<Text asChild consumeCss color="text.subtle" textStyle="label.medium">
246+
<span>{keyedBucketResult[subtype.id]}</span>
247+
</Text>
248+
)}
249+
</FilterWrapper>
250+
))}
251+
</StyledAccordionItemContent>
252+
</AccordionItem>
253+
) : (
254+
<FilterWrapper key={resourceType.id}>
145255
<CheckboxRoot
146256
value={resourceType.id}
147-
checked={
148-
currentResourceTypeIds.includes(resourceType.id) ||
149-
resourceType.subtypes.some((s) => currentResourceTypeIds.includes(s.id))
150-
}
257+
checked={currentResourceTypeIds.includes(resourceType.id)}
151258
onCheckedChange={(details) => onToggleResourceType(resourceType.id, details.checked === true)}
152259
>
153260
<CheckboxControl>
@@ -158,71 +265,17 @@ export const ResourceTypeFilter = ({ bucketResult, resourceTypes: resourceTypesP
158265
<CheckboxLabel>{resourceType.name}</CheckboxLabel>
159266
<CheckboxHiddenInput />
160267
</CheckboxRoot>
161-
<AccordionItemTrigger asChild>
162-
<IconButton
163-
variant="tertiary"
164-
size="small"
165-
aria-label={t("searchPage.resourceTypeFilter.showSubtypes", { parent: resourceType.name })}
166-
title={t("searchPage.resourceTypeFilter.showSubtypes", { parent: resourceType.name })}
167-
>
168-
<AccordionItemIndicator asChild>
169-
<ArrowDownShortLine size="medium" />
170-
</AccordionItemIndicator>
171-
</IconButton>
172-
</AccordionItemTrigger>
268+
{keyedBucketResult[resourceType.id] != null && (
269+
<TopLevelCountText asChild consumeCss color="text.subtle" textStyle="label.medium">
270+
<span>{keyedBucketResult[resourceType.id]}</span>
271+
</TopLevelCountText>
272+
)}
173273
</FilterWrapper>
174-
<StyledAccordionItemContent>
175-
{resourceType.subtypes.map((subtype) => (
176-
<FilterWrapper key={subtype.id}>
177-
<CheckboxRoot
178-
value={subtype.id}
179-
checked={currentResourceTypeIds.includes(subtype.id)}
180-
onCheckedChange={(details) =>
181-
onToggleResourceType(`${resourceType.id}${DELIMITER}${subtype.id}`, details.checked === true)
182-
}
183-
>
184-
<CheckboxControl>
185-
<CheckboxIndicator asChild>
186-
<CheckLine />
187-
</CheckboxIndicator>
188-
</CheckboxControl>
189-
<CheckboxLabel>{subtype.name}</CheckboxLabel>
190-
<CheckboxHiddenInput />
191-
</CheckboxRoot>
192-
{keyedBucketResult[subtype.id] != null && (
193-
<Text asChild consumeCss color="text.subtle" textStyle="label.medium">
194-
<span>{keyedBucketResult[subtype.id]}</span>
195-
</Text>
196-
)}
197-
</FilterWrapper>
198-
))}
199-
</StyledAccordionItemContent>
200-
</AccordionItem>
201-
) : (
202-
<FilterWrapper key={resourceType.id}>
203-
<CheckboxRoot
204-
value={resourceType.id}
205-
checked={currentResourceTypeIds.includes(resourceType.id)}
206-
onCheckedChange={(details) => onToggleResourceType(resourceType.id, details.checked === true)}
207-
>
208-
<CheckboxControl>
209-
<CheckboxIndicator asChild>
210-
<CheckLine />
211-
</CheckboxIndicator>
212-
</CheckboxControl>
213-
<CheckboxLabel>{resourceType.name}</CheckboxLabel>
214-
<CheckboxHiddenInput />
215-
</CheckboxRoot>
216-
{keyedBucketResult[resourceType.id] != null && (
217-
<TopLevelCountText asChild consumeCss color="text.subtle" textStyle="label.medium">
218-
<span>{keyedBucketResult[resourceType.id]}</span>
219-
</TopLevelCountText>
220-
)}
221-
</FilterWrapper>
222-
),
223-
)}
224-
</StyledAccordionRoot>
225-
)}
274+
),
275+
)}
276+
</StyledAccordionRoot>
277+
)}
278+
</div>
226279
</FilterContainer>
227280
);
228281
};

src/containers/SearchPageV2/SearchContainer.tsx

+13-12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { HomeBreadcrumb, usePaginationTranslations } from "@ndla/ui";
3131
import { GrepFilter } from "./GrepFilter";
3232
import { ResourceTypeFilter } from "./ResourceTypeFilter";
3333
import { SearchResult } from "./SearchResult";
34+
import { RESOURCE_NODE_TYPE } from "./searchUtils";
3435
import { SubjectFilter } from "./SubjectFilter";
3536
import { TraitFilter } from "./TraitFilter";
3637
import { LanguageSelector } from "../../components/LanguageSelector";
@@ -193,21 +194,18 @@ const StyledButton = styled(Button, {
193194
const getTypeVariables = (
194195
resourceTypes: string | null,
195196
allResourceTypes: GQLSearchContainer_ResourceTypeDefinitionFragment[],
196-
isLti: boolean,
197+
nodeType: string,
197198
) => {
198-
const types = resourceTypes?.split(",");
199-
const withoutTopicArticle = types?.filter((rt) => rt !== "topic-article");
200-
const contextTypes = types?.includes("topic-article") ? "topic-article" : undefined;
201-
202-
if (!isLti && types?.length && types?.length !== withoutTopicArticle?.length) {
199+
if (nodeType !== RESOURCE_NODE_TYPE) {
203200
return {
204-
contextTypes,
201+
contextTypes: nodeType === "topic" ? "topic-article" : nodeType,
205202
};
206203
}
207204

208-
const actualResourceTypes = withoutTopicArticle?.length
209-
? withoutTopicArticle.map((id) => `urn:resourcetype:${id}`).join(",")
210-
: undefined;
205+
const actualResourceTypes = resourceTypes
206+
?.split(",")
207+
.map((id) => `urn:resourcetype:${id}`)
208+
.join(",");
211209

212210
const flattenedResourceTypes = allResourceTypes
213211
.flatMap((rt) => (rt.subtypes?.length ? rt.subtypes.map((st) => st.id) : rt.id))
@@ -242,11 +240,14 @@ export const SearchContainer = ({ resourceTypes, resourceTypesLoading }: Props)
242240
page: parseInt(searchParams.get("page") ?? "1") ?? undefined,
243241
subjects: searchParams.get("subjects") ?? undefined,
244242
pageSize: 10,
245-
// TODO: We need to aggregate topic articles
246243
aggregatePaths: ["contexts.resourceTypes.id"],
247244
traits: searchParams.get("traits") ?? undefined,
248245
filterInactive: !searchParams.get("subjects")?.split(",").length,
249-
...getTypeVariables(searchParams.get("resourceTypes"), resourceTypes, isLti),
246+
...getTypeVariables(
247+
searchParams.get("resourceTypes"),
248+
resourceTypes,
249+
searchParams.get("type") ?? RESOURCE_NODE_TYPE,
250+
),
250251
},
251252
});
252253

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) 2025-present, NDLA.
3+
*
4+
* This source code is licensed under the GPLv3 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
export const RESOURCE_NODE_TYPE = "resource";
10+
export const TOPIC_NODE_TYPE = "topic";
11+
export const SUBJECT_NODE_TYPE = "subject";

src/messages/messagesEN.ts

+3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ const messages = {
9898
resourceTypeFilter: {
9999
title: "Choose page type",
100100
showSubtypes: "Show subtypes for {{parent}}",
101+
radioLabel: "Resource type",
102+
resourceLabel: "Resource",
103+
topicLabel: "Topic",
101104
},
102105
},
103106
myNdla: {

src/messages/messagesNB.ts

+3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ const messages = {
9898
resourceTypeFilter: {
9999
title: "Velg sidetype",
100100
showSubtypes: "Vis undertyper for {{parent}}",
101+
radioLabel: "Ressurstype",
102+
resourceLabel: "Ressurs",
103+
topicLabel: "Emne",
101104
},
102105
},
103106
myNdla: {

src/messages/messagesNN.ts

+3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ const messages = {
9898
resourceTypeFilter: {
9999
title: "Vel sidetype",
100100
showSubtypes: "Vis undertypar for {{parent}}",
101+
radioLabel: "Ressurstype",
102+
resourceLabel: "Ressurs",
103+
topicLabel: "Emne",
101104
},
102105
},
103106
myNdla: {

src/messages/messagesSE.ts

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ const messages = {
9797
resourceTypeFilter: {
9898
title: "Velg sidetype",
9999
showSubtypes: "Vis undertyper for {{parent}}",
100+
radioLabel: "Ressurstype",
101+
resourceLabel: "Ressurs",
102+
topicLabel: "Emne",
100103
},
101104
},
102105
myNdla: {

0 commit comments

Comments
 (0)