diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index 821164a00d..09d442f00e 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -21272,7 +21272,7 @@ export const TopicsApiAxiosParamCreator = function ( * @param {number} [limit] Number of results to return per page. * @param {Array} [name] Multiple values may be separated by commas. * @param {number} [offset] The initial index from which to return the results. - * @param {Array} [parent_topic_name] Multiple values may be separated by commas. + * @param {Array} [parent_topic_id] Multiple values may be separated by commas. * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -21281,7 +21281,7 @@ export const TopicsApiAxiosParamCreator = function ( limit?: number, name?: Array, offset?: number, - parent_topic_name?: Array, + parent_topic_id?: Array, options: RawAxiosRequestConfig = {}, ): Promise => { const localVarPath = `/api/v1/topics/` @@ -21316,8 +21316,8 @@ export const TopicsApiAxiosParamCreator = function ( localVarQueryParameter["offset"] = offset } - if (parent_topic_name) { - localVarQueryParameter["parent_topic_name"] = parent_topic_name.join( + if (parent_topic_id) { + localVarQueryParameter["parent_topic_id"] = parent_topic_id.join( COLLECTION_FORMATS.csv, ) } @@ -21399,7 +21399,7 @@ export const TopicsApiFp = function (configuration?: Configuration) { * @param {number} [limit] Number of results to return per page. * @param {Array} [name] Multiple values may be separated by commas. * @param {number} [offset] The initial index from which to return the results. - * @param {Array} [parent_topic_name] Multiple values may be separated by commas. + * @param {Array} [parent_topic_id] Multiple values may be separated by commas. * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -21408,7 +21408,7 @@ export const TopicsApiFp = function (configuration?: Configuration) { limit?: number, name?: Array, offset?: number, - parent_topic_name?: Array, + parent_topic_id?: Array, options?: RawAxiosRequestConfig, ): Promise< ( @@ -21421,7 +21421,7 @@ export const TopicsApiFp = function (configuration?: Configuration) { limit, name, offset, - parent_topic_name, + parent_topic_id, options, ) const index = configuration?.serverIndex ?? 0 @@ -21497,7 +21497,7 @@ export const TopicsApiFactory = function ( requestParameters.limit, requestParameters.name, requestParameters.offset, - requestParameters.parent_topic_name, + requestParameters.parent_topic_id, options, ) .then((request) => request(axios, basePath)) @@ -21556,10 +21556,10 @@ export interface TopicsApiTopicsListRequest { /** * Multiple values may be separated by commas. - * @type {Array} + * @type {Array} * @memberof TopicsApiTopicsList */ - readonly parent_topic_name?: Array + readonly parent_topic_id?: Array } /** @@ -21601,7 +21601,7 @@ export class TopicsApi extends BaseAPI { requestParameters.limit, requestParameters.name, requestParameters.offset, - requestParameters.parent_topic_name, + requestParameters.parent_topic_id, options, ) .then((request) => request(this.axios, this.basePath)) diff --git a/frontends/mit-learn/src/pages/ChannelPage/ChannelPage.test.tsx b/frontends/mit-learn/src/pages/ChannelPage/ChannelPage.test.tsx index 98f6230fff..39643f66ce 100644 --- a/frontends/mit-learn/src/pages/ChannelPage/ChannelPage.test.tsx +++ b/frontends/mit-learn/src/pages/ChannelPage/ChannelPage.test.tsx @@ -11,6 +11,7 @@ import { } from "../../test-utils" import ChannelSearch from "./ChannelSearch" import { assertHeadings } from "ol-test-utilities" +import invariant from "tiny-invariant" jest.mock("./ChannelSearch", () => { const actual = jest.requireActual("./ChannelSearch") @@ -104,6 +105,21 @@ const setupApis = ( results: [], }) + if ( + channel.channel_type === ChannelTypeEnum.Topic && + channel.topic_detail.topic + ) { + const subTopics = factories.learningResources.topics({ count: 5 }) + setMockResponse.get( + urls.topics.list({ parent_topic_id: [channel.topic_detail.topic] }), + subTopics, + ) + return { + channel, + subTopics, + } + } + return { channel, } @@ -250,6 +266,25 @@ describe.each(NON_UNIT_CHANNEL_TYPES)( }, ) +describe("Channel Pages, Topic only", () => { + test("Subtopics display", async () => { + const { channel, subTopics } = setupApis({ + search_filter: "topic=Physics", + channel_type: ChannelTypeEnum.Topic, + }) + renderTestApp({ url: `/c/${channel.channel_type}/${channel.name}` }) + + invariant(subTopics) + const links = await screen.findAllByRole("link", { + // name arg can be string, regex, or function + name: (name) => subTopics?.results.map((t) => t.name).includes(name), + }) + links.forEach((link, i) => { + expect(link).toHaveAttribute("href", subTopics.results[i].channel_url) + }) + }) +}) + describe("Channel Pages, Unit only", () => { it("Sets the expected meta tags", async () => { const { channel } = setupApis({ diff --git a/frontends/mit-learn/src/pages/ChannelPage/ChannelPage.tsx b/frontends/mit-learn/src/pages/ChannelPage/ChannelPage.tsx index 4e68174a03..022e35ebe4 100644 --- a/frontends/mit-learn/src/pages/ChannelPage/ChannelPage.tsx +++ b/frontends/mit-learn/src/pages/ChannelPage/ChannelPage.tsx @@ -9,16 +9,71 @@ import type { BooleanFacets, } from "@mitodl/course-search-utils" import { ChannelTypeEnum } from "api/v0" +import { useLearningResourceTopics } from "api/hooks/learningResources" +import { ChipLink, Container, styled, Typography } from "ol-components" +import { propsNotNil } from "ol-utilities" + +const SubTopicsContainer = styled(Container)(({ theme }) => ({ + marginBottom: "60px", + [theme.breakpoints.down("sm")]: { + marginBottom: "24px", + }, +})) + +const SubTopicsHeader = styled(Typography)(({ theme }) => ({ + marginBottom: "10px", + ...theme.typography.subtitle1, +})) + +const ChipsContainer = styled.div({ + display: "flex", + flexWrap: "wrap", + gap: "12px", +}) type RouteParams = { channelType: ChannelTypeEnum name: string } +type SubTopicDisplayProps = { + parentTopicId: number +} + +const SubTopicsDisplay: React.FC = (props) => { + const { parentTopicId } = props + const topicsQuery = useLearningResourceTopics({ + parent_topic_id: [parentTopicId], + }) + const totalSubtopics = topicsQuery.data?.results.length ?? 0 + const subTopics = topicsQuery.data?.results.filter( + propsNotNil(["channel_url"]), + ) + return ( + totalSubtopics > 0 && ( + + Related Topics + + {subTopics?.map((topic) => ( + + ))} + + + ) + ) +} + const ChannelPage: React.FC = () => { const { channelType, name } = useParams() const channelQuery = useChannelDetail(String(channelType), String(name)) const searchParams: Facets & BooleanFacets = {} + const publicDescription = channelQuery.data?.public_description if (channelQuery.data?.search_filter) { const urlParams = new URLSearchParams(channelQuery.data.search_filter) @@ -37,7 +92,15 @@ const ChannelPage: React.FC = () => { channelType && ( <> -

{channelQuery.data?.public_description}

+ {publicDescription && ( + {publicDescription} + )} + {channelQuery.data?.channel_type === ChannelTypeEnum.Topic && + channelQuery.data?.topic_detail?.topic ? ( + + ) : null} {channelQuery.data?.search_filter && ( theme.breakpoints.down("md")} { padding-bottom: 35px; - margin-top: 40px; } ` diff --git a/frontends/mit-learn/src/pages/ChannelPage/DefaultChannelTemplate.tsx b/frontends/mit-learn/src/pages/ChannelPage/DefaultChannelTemplate.tsx index 1a997ddafe..eade19f177 100644 --- a/frontends/mit-learn/src/pages/ChannelPage/DefaultChannelTemplate.tsx +++ b/frontends/mit-learn/src/pages/ChannelPage/DefaultChannelTemplate.tsx @@ -12,6 +12,13 @@ import { } from "./ChannelPageTemplate" import MetaTags from "@/page-components/MetaTags/MetaTags" +const ChildrenContainer = styled.div(({ theme }) => ({ + paddingTop: "40px", + [theme.breakpoints.down("sm")]: { + paddingTop: "24px", + }, +})) + const ChannelControlsContainer = styled.div(({ theme }) => ({ display: "flex", flexDirection: "row", @@ -107,7 +114,7 @@ const DefaultChannelTemplate: React.FC = ({ } /> - {children} + {children} ) } diff --git a/frontends/mit-learn/src/pages/ChannelPage/EditChannelAppearanceForm.test.tsx b/frontends/mit-learn/src/pages/ChannelPage/EditChannelAppearanceForm.test.tsx index 6fcdb13e5a..5c18977909 100644 --- a/frontends/mit-learn/src/pages/ChannelPage/EditChannelAppearanceForm.test.tsx +++ b/frontends/mit-learn/src/pages/ChannelPage/EditChannelAppearanceForm.test.tsx @@ -9,7 +9,7 @@ import { factories, urls, setMockResponse } from "api/test-utils" import { channels as factory } from "api/test-utils/factories" import { makeChannelViewPath, makeChannelEditPath } from "@/common/urls" import { makeWidgetListResponse } from "ol-widgets/src/factories" -import type { Channel } from "api/v0" +import { ChannelTypeEnum, type Channel } from "api/v0" const setupApis = (channelOverrides: Partial) => { const channel = factory.channel({ is_moderator: true, ...channelOverrides }) @@ -33,6 +33,16 @@ const setupApis = (channelOverrides: Partial) => { results: [], }) + if ( + channel.channel_type === ChannelTypeEnum.Topic && + channel.topic_detail.topic + ) { + setMockResponse.get( + urls.topics.list({ parent_topic_id: [channel.topic_detail.topic] }), + factories.learningResources.topics({ count: 5 }), + ) + } + return channel } diff --git a/learning_resources/filters.py b/learning_resources/filters.py index 45c362283b..ca5635bb9f 100644 --- a/learning_resources/filters.py +++ b/learning_resources/filters.py @@ -264,9 +264,9 @@ class TopicFilter(FilterSet): label="Topic name", method="filter_name", ) - parent_topic_name = CharInFilter( - label="Parent topic name", - method="filter_parent_topic_name", + parent_topic_id = NumberInFilter( + label="Parent topic ID", + method="filter_parent_topic_id", ) is_toplevel = BooleanFilter( label="Filter top-level topics", @@ -281,18 +281,6 @@ def filter_toplevel(self, queryset, _, value): """Filter by top-level (parent == null)""" return queryset.filter(parent__isnull=value) - def filter_parent_topic_name(self, queryset, _, values): - """Filter by parent topic name (up to 2 levels deep)""" - - nested_topic_filter = Q() - - for topic in values: - nested_topic_filter |= Q( - parent__isnull=False, parent__name__iexact=topic - ) | Q( - parent__isnull=False, - parent__parent__isnull=False, - parent__parent__name__iexact=topic, - ) - - return queryset.filter(nested_topic_filter) + def filter_parent_topic_id(self, queryset, _, values): + """Get direct children of a topic""" + return queryset.filter(parent_id__in=values) diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 4c1e3ab103..dbf8de09b2 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -6308,11 +6308,11 @@ paths: schema: type: integer - in: query - name: parent_topic_name + name: parent_topic_id schema: type: array items: - type: string + type: number description: Multiple values may be separated by commas. explode: false style: form