diff --git a/RELEASE.rst b/RELEASE.rst index 99f2562ff2..6eeb52e3dc 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,14 @@ Release Notes ============= +Version 0.22.0 +-------------- + +- Make add to list dialog scrollable (#1689) +- make search defaults settable (#1681) +- Shuffling around where the search_update event is fired so it happens in more places (#1679) +- prevent featured course carousel from re-randomizing (#1673) + Version 0.21.3 (Released October 15, 2024) -------------- diff --git a/app.json b/app.json index 4fe3feccae..e02f108f58 100644 --- a/app.json +++ b/app.json @@ -91,6 +91,10 @@ "description": "Default max incompleteness penalty value for the search API and frontend", "required": false }, + "DEFAULT_SEARCH_CONTENT_FILE_SCORE_WEIGHT": { + "description": "Default score weight for content file search match", + "required": false + }, "EDX_API_ACCESS_TOKEN_URL": { "description": "URL to retrieve a MITx access token", "required": false diff --git a/frontends/api/src/clients.ts b/frontends/api/src/clients.ts index 2f4781910c..d56cd7a56f 100644 --- a/frontends/api/src/clients.ts +++ b/frontends/api/src/clients.ts @@ -20,6 +20,7 @@ import { NewsEventsApi, ProfilesApi, TestimonialsApi, + LearningResourcesSearchAdminParamsApi, } from "./generated/v0/api" import axiosInstance from "./axios" @@ -40,6 +41,9 @@ const learningResourcesSearchApi = new LearningResourcesSearchApi( axiosInstance, ) +const learningResourcesSearchAdminParamsApi = + new LearningResourcesSearchAdminParamsApi(undefined, BASE_PATH, axiosInstance) + const featuredApi = new FeaturedApi(undefined, BASE_PATH, axiosInstance) const learningpathsApi = new LearningpathsApi( @@ -102,4 +106,5 @@ export { newsEventsApi, featuredApi, testimonialsApi, + learningResourcesSearchAdminParamsApi, } diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 6d2b7f1290..3db3d6ef94 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -4520,6 +4520,143 @@ export class CkeditorApi extends BaseAPI { } } +/** + * LearningResourcesSearchAdminParamsApi - axios parameter creator + * @export + */ +export const LearningResourcesSearchAdminParamsApiAxiosParamCreator = function ( + configuration?: Configuration, +) { + return { + /** + * Learning resource search default admin param values + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + learningResourcesSearchAdminParamsRetrieve: async ( + options: RawAxiosRequestConfig = {}, + ): Promise => { + const localVarPath = `/api/v0/learning_resources_search_admin_params/` + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL) + let baseOptions + if (configuration) { + baseOptions = configuration.baseOptions + } + + const localVarRequestOptions = { + method: "GET", + ...baseOptions, + ...options, + } + const localVarHeaderParameter = {} as any + const localVarQueryParameter = {} as any + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + } +} + +/** + * LearningResourcesSearchAdminParamsApi - functional programming interface + * @export + */ +export const LearningResourcesSearchAdminParamsApiFp = function ( + configuration?: Configuration, +) { + const localVarAxiosParamCreator = + LearningResourcesSearchAdminParamsApiAxiosParamCreator(configuration) + return { + /** + * Learning resource search default admin param values + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async learningResourcesSearchAdminParamsRetrieve( + options?: RawAxiosRequestConfig, + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.learningResourcesSearchAdminParamsRetrieve( + options, + ) + const index = configuration?.serverIndex ?? 0 + const operationBasePath = + operationServerMap[ + "LearningResourcesSearchAdminParamsApi.learningResourcesSearchAdminParamsRetrieve" + ]?.[index]?.url + return (axios, basePath) => + createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration, + )(axios, operationBasePath || basePath) + }, + } +} + +/** + * LearningResourcesSearchAdminParamsApi - factory interface + * @export + */ +export const LearningResourcesSearchAdminParamsApiFactory = function ( + configuration?: Configuration, + basePath?: string, + axios?: AxiosInstance, +) { + const localVarFp = LearningResourcesSearchAdminParamsApiFp(configuration) + return { + /** + * Learning resource search default admin param values + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + learningResourcesSearchAdminParamsRetrieve( + options?: RawAxiosRequestConfig, + ): AxiosPromise { + return localVarFp + .learningResourcesSearchAdminParamsRetrieve(options) + .then((request) => request(axios, basePath)) + }, + } +} + +/** + * LearningResourcesSearchAdminParamsApi - object-oriented interface + * @export + * @class LearningResourcesSearchAdminParamsApi + * @extends {BaseAPI} + */ +export class LearningResourcesSearchAdminParamsApi extends BaseAPI { + /** + * Learning resource search default admin param values + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LearningResourcesSearchAdminParamsApi + */ + public learningResourcesSearchAdminParamsRetrieve( + options?: RawAxiosRequestConfig, + ) { + return LearningResourcesSearchAdminParamsApiFp(this.configuration) + .learningResourcesSearchAdminParamsRetrieve(options) + .then((request) => request(this.axios, this.basePath)) + } +} + /** * NewsEventsApi - axios parameter creator * @export diff --git a/frontends/api/src/hooks/adminSearchParams/index.ts b/frontends/api/src/hooks/adminSearchParams/index.ts new file mode 100644 index 0000000000..08a46f49cf --- /dev/null +++ b/frontends/api/src/hooks/adminSearchParams/index.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query" +import { learningResourcesSearchAdminParamsApi } from "../../clients" + +const useAdminSearchParams = (enabled: boolean) => + useQuery({ + queryKey: ["adminParams"], + queryFn: async () => { + const response = + await learningResourcesSearchAdminParamsApi.learningResourcesSearchAdminParamsRetrieve() + return response.data + }, + enabled: enabled, + }) + +export { useAdminSearchParams } diff --git a/frontends/api/src/hooks/learningResources/index.test.ts b/frontends/api/src/hooks/learningResources/index.test.ts index 5c4c6364f4..2b96653247 100644 --- a/frontends/api/src/hooks/learningResources/index.test.ts +++ b/frontends/api/src/hooks/learningResources/index.test.ts @@ -211,7 +211,7 @@ describe("LearningPath CRUD", () => { } test("useLearningpathCreate calls correct API", async () => { - const { path, pathUrls, keys } = makeData() + const { path, pathUrls } = makeData() const url = pathUrls.list const requestData = { title: path.title } @@ -226,9 +226,12 @@ describe("LearningPath CRUD", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(makeRequest).toHaveBeenCalledWith("post", url, requestData) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith( - keys.learningResources, - ) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith([ + "learningResources", + "learningpaths", + "learning_paths", + "list", + ]) }) test("useLearningpathDestroy calls correct API", async () => { diff --git a/frontends/api/src/hooks/learningResources/index.ts b/frontends/api/src/hooks/learningResources/index.ts index 5f6cd22ac4..7633c806f8 100644 --- a/frontends/api/src/hooks/learningResources/index.ts +++ b/frontends/api/src/hooks/learningResources/index.ts @@ -41,6 +41,7 @@ import learningResources, { invalidateResourceWithUserListQueries, updateListParentsOnAdd, updateListParentsOnDestroy, + updateListParents, } from "./keyFactory" import { ListType } from "../../common/constants" @@ -123,9 +124,9 @@ const useLearningpathCreate = () => { LearningPathResourceRequest: params, }), onSettled: () => { - // Invalidate everything: this is over-aggressive, but the new resource - // could appear in most lists - queryClient.invalidateQueries(learningResources._def) + queryClient.invalidateQueries( + learningResources.learningpaths._ctx.list._def, + ) }, }) } @@ -343,12 +344,19 @@ const useLearningResourceSetUserListRelationships = () => { ) => learningResourcesApi.learningResourcesUserlistsPartialUpdate(params), onSettled: (_response, _err, vars) => { invalidateResourceQueries(queryClient, vars.id, { - skipFeatured: false, + skipFeatured: true, }) vars.userlist_id?.forEach((userlistId) => { invalidateUserListQueries(queryClient, userlistId) }) }, + onSuccess: (response, vars) => { + queryClient.setQueriesData( + learningResources.featured({}).queryKey, + (featured) => + updateListParents(vars.id, featured, response.data, "userlist"), + ) + }, }) } @@ -361,14 +369,21 @@ const useLearningResourceSetLearningPathRelationships = () => { learningResourcesApi.learningResourcesLearningPathsPartialUpdate(params), onSettled: (_response, _err, vars) => { invalidateResourceQueries(queryClient, vars.id, { - skipFeatured: false, + skipFeatured: true, }) vars.learning_path_id?.forEach((learningPathId) => { invalidateResourceQueries(queryClient, learningPathId, { - skipFeatured: false, + skipFeatured: true, }) }) }, + onSuccess: (response, vars) => { + queryClient.setQueriesData( + learningResources.featured({}).queryKey, + (featured) => + updateListParents(vars.id, featured, response.data, "learningpath"), + ) + }, }) } diff --git a/frontends/api/src/hooks/learningResources/keyFactory.ts b/frontends/api/src/hooks/learningResources/keyFactory.ts index fd84a8b093..82a81cbf23 100644 --- a/frontends/api/src/hooks/learningResources/keyFactory.ts +++ b/frontends/api/src/hooks/learningResources/keyFactory.ts @@ -371,6 +371,41 @@ const updateListParentsOnDestroy = ( } } +/** + * Given + * - a LearningResource ID + * - a paginated list of current resources + * - a list of new relationships + * - the type of list + * Update the resources' user_list_parents field to include the new relationships + */ +const updateListParents = ( + resourceId: number, + staleResources?: PaginatedLearningResourceList, + newRelationships?: MicroUserListRelationship[], + listType?: "userlist" | "learningpath", +) => { + if (!resourceId || !staleResources || !newRelationships || !listType) + return staleResources + const matchIndex = staleResources.results.findIndex( + (res) => res.id === resourceId, + ) + if (matchIndex === -1) return staleResources + const updatedResults = [...staleResources.results] + const newResource = { ...updatedResults[matchIndex] } + if (listType === "userlist") { + newResource.user_list_parents = newRelationships + } + if (listType === "learningpath") { + newResource.learning_path_parents = newRelationships + } + updatedResults[matchIndex] = newResource + return { + ...staleResources, + results: updatedResults, + } +} + export default learningResources export { invalidateResourceQueries, @@ -378,4 +413,5 @@ export { invalidateResourceWithUserListQueries, updateListParentsOnAdd, updateListParentsOnDestroy, + updateListParents, } diff --git a/frontends/api/src/test-utils/urls.ts b/frontends/api/src/test-utils/urls.ts index f440423d7f..bedc2f275b 100644 --- a/frontends/api/src/test-utils/urls.ts +++ b/frontends/api/src/test-utils/urls.ts @@ -203,6 +203,11 @@ const search = { const userMe = { get: () => `${API_BASE_URL}/api/v0/users/me/`, } + +const adminSearchParams = { + get: () => `${API_BASE_URL}/api/v0/learning_resources_search_admin_params/`, +} + const profileMe = { get: () => `${API_BASE_URL}/api/v0/profiles/me/`, patch: () => `${API_BASE_URL}/api/v0/profiles/me/`, @@ -232,4 +237,5 @@ export { departments, newsEvents, testimonials, + adminSearchParams, } diff --git a/frontends/mit-learn/jest.config.ts b/frontends/mit-learn/jest.config.ts index 54925827a7..c767e8583f 100644 --- a/frontends/mit-learn/jest.config.ts +++ b/frontends/mit-learn/jest.config.ts @@ -19,11 +19,6 @@ const config: Config.InitialOptions = { MITOL_API_BASE_URL: "https://api.test.learn.mit.edu", PUBLIC_URL: "", SITE_NAME: "MIT Learn", - DEFAULT_SEARCH_MODE: "phrase", - DEFAULT_SEARCH_SLOP: 6, - DEFAULT_SEARCH_STALENESS_PENALTY: 2.5, - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF: 0, - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY: 90, }, }, } diff --git a/frontends/mit-learn/src/page-components/Dialogs/AddToListDialog.test.tsx b/frontends/mit-learn/src/page-components/Dialogs/AddToListDialog.test.tsx index c6f9e4755f..b197330469 100644 --- a/frontends/mit-learn/src/page-components/Dialogs/AddToListDialog.test.tsx +++ b/frontends/mit-learn/src/page-components/Dialogs/AddToListDialog.test.tsx @@ -64,7 +64,10 @@ const setupLearningPaths = ({ urls.learningResources.details({ id: resource.id }), resource, ) - setMockResponse.get(urls.learningPaths.list(), paginatedLearningPaths) + setMockResponse.get( + urls.learningPaths.list({ limit: 100 }), + paginatedLearningPaths, + ) const view = renderWithProviders(null) if (dialogOpen) { act(() => { @@ -100,7 +103,7 @@ const setupUserLists = ({ urls.learningResources.details({ id: resource.id }), resource, ) - setMockResponse.get(urls.userLists.list(), paginatedUserLists) + setMockResponse.get(urls.userLists.list({ limit: 100 }), paginatedUserLists) const view = renderWithProviders(null) if (dialogOpen) { act(() => { diff --git a/frontends/mit-learn/src/page-components/Dialogs/AddToListDialog.tsx b/frontends/mit-learn/src/page-components/Dialogs/AddToListDialog.tsx index 239b34563e..060dd7d1b0 100644 --- a/frontends/mit-learn/src/page-components/Dialogs/AddToListDialog.tsx +++ b/frontends/mit-learn/src/page-components/Dialogs/AddToListDialog.tsx @@ -14,11 +14,7 @@ import { usePostHog } from "posthog-js/react" import * as NiceModal from "@ebay/nice-modal-react" -import { - type LearningPathResource, - type LearningResource, - type UserList, -} from "api" +import type { LearningPathResource, LearningResource, UserList } from "api" import { useLearningResourceSetUserListRelationships, @@ -31,6 +27,8 @@ import { manageListDialogs } from "@/page-components/ManageListDialogs/ManageLis import { ListType } from "api/constants" import { useFormik } from "formik" +const LIST_LIMIT = 100 + const ResourceTitle = styled.span({ fontStyle: "italic", }) @@ -202,7 +200,7 @@ const AddToLearningPathDialogInner: React.FC = ({ }) => { const resourceQuery = useLearningResourcesDetail(resourceId) const resource = resourceQuery.data - const listsQuery = useLearningPathsList() + const listsQuery = useLearningPathsList({ limit: LIST_LIMIT }) const isReady = !!(resource && listsQuery.isSuccess) const lists = listsQuery.data?.results ?? [] @@ -221,7 +219,7 @@ const AddToUserListDialogInner: React.FC = ({ }) => { const resourceQuery = useLearningResourcesDetail(resourceId) const resource = resourceQuery.data - const listsQuery = useUserListList() + const listsQuery = useUserListList({ limit: LIST_LIMIT }) const isReady = !!(resource && listsQuery.isSuccess) const lists = listsQuery.data?.results ?? [] diff --git a/frontends/mit-learn/src/page-components/SearchDisplay/SearchDisplay.tsx b/frontends/mit-learn/src/page-components/SearchDisplay/SearchDisplay.tsx index 1b021312c6..c68d7f4a19 100644 --- a/frontends/mit-learn/src/page-components/SearchDisplay/SearchDisplay.tsx +++ b/frontends/mit-learn/src/page-components/SearchDisplay/SearchDisplay.tsx @@ -30,6 +30,7 @@ import { SearchModeEnumDescriptions, } from "api" import { useLearningResourcesSearch } from "api/hooks/learningResources" +import { useAdminSearchParams } from "api/hooks/adminSearchParams" import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" import { AvailableFacets, @@ -51,6 +52,7 @@ import type { TabConfig } from "./ResourceCategoryTabs" import { ResourceCard } from "../ResourceCard/ResourceCard" import { useSearchParams } from "@mitodl/course-search-utils/react-router" import { useUserMe } from "api/hooks/user" +import { usePostHog } from "posthog-js/react" const StyledResourceTabs = styled(ResourceCategoryTabs.TabList)` margin-top: 0 px; @@ -516,14 +518,6 @@ interface SearchDisplayProps { filterHeadingEl: React.ElementType } -const { - DEFAULT_SEARCH_MODE, - DEFAULT_SEARCH_SLOP, - DEFAULT_SEARCH_STALENESS_PENALTY, - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF, - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY, -} = APP_SETTINGS - const SearchDisplay: React.FC = ({ page, setPage, @@ -532,16 +526,20 @@ const SearchDisplay: React.FC = ({ constantSearchParams, hasFacets, requestParams, - setParamValue, - clearAllFacets, - toggleParamValue, + setParamValue: actuallySetParamValue, + clearAllFacets: actuallyClearAllFacets, + toggleParamValue: actuallyToggleParamValue, showProfessionalToggle, - setSearchParams, + setSearchParams: actuallySetSearchParams, resultsHeadingEl, filterHeadingEl, }) => { const [searchParams] = useSearchParams() const [expandAdminOptions, setExpandAdminOptions] = useState(false) + + const { data: adminParams, isLoading: isAdminParamsLoading } = + useAdminSearchParams(expandAdminOptions) + const scrollHook = useRef(null) const activeTab = TABS.find( @@ -588,28 +586,44 @@ const SearchDisplay: React.FC = ({ const [mobileDrawerOpen, setMobileDrawerOpen] = React.useState(false) + const posthog = usePostHog() + const { POSTHOG } = APP_SETTINGS + const toggleMobileDrawer = (newOpen: boolean) => () => { setMobileDrawerOpen(newOpen) } - const searchModeDropdown = ( - - setSearchParams((prev) => { - const next = new URLSearchParams(prev) - next.set("search_mode", e.target.value as string) - if (e.target.value !== "phrase") { - next.delete("slop") - } - return next - }) - } - options={searchModeDropdownOptions} - className="search-mode-dropdown" - /> - ) + const captureSearchEvent = () => { + if (!(!POSTHOG?.api_key || POSTHOG.api_key.length < 1)) { + posthog.capture("search_update") + } + } + + const setParamValue = (value: string, prev: string | string[]) => { + captureSearchEvent() + actuallySetParamValue(value, prev) + } + + const clearAllFacets = () => { + captureSearchEvent() + actuallyClearAllFacets() + } + + const setSearchParams = ( + value: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams), + ) => { + captureSearchEvent() + actuallySetSearchParams(value) + } + + const toggleParamValue = ( + name: string, + rawValue: string, + checked: boolean, + ) => { + captureSearchEvent() + actuallyToggleParamValue(name, rawValue, checked) + } const sortDropdown = ( = ({ /> ) - const AdminOptions: React.FC = ( - expandAdminOptions, - setExpandAdminOptions, + type adminParamsType = { + search_mode: string + slop: number + yearly_decay_percent: number + min_score: number + max_incompleteness_penalty: number + content_file_score_weight: number + } + + const AdminOptions = ( + expandAdminOptions: boolean, + setExpandAdminOptions: (value: boolean) => void, + adminParams: adminParamsType | void | undefined, ) => { const titleLineIcon = expandAdminOptions ? "expand_less" : "expand_more" + const searchModeDropdown = ( + + setSearchParams((prev) => { + const next = new URLSearchParams(prev) + next.set("search_mode", e.target.value as string) + if (e.target.value !== "phrase") { + next.delete("slop") + } + return next + }) + } + options={searchModeDropdownOptions} + className="search-mode-dropdown" + /> + ) + return (
= ({ {titleLineIcon} - {expandAdminOptions ? ( + {expandAdminOptions && adminParams && !isAdminParamsLoading ? (
Resource Score Staleness Penalty @@ -655,7 +698,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("yearly_decay_percent") ? Number(searchParams.get("yearly_decay_percent")) - : DEFAULT_SEARCH_STALENESS_PENALTY + : adminParams.yearly_decay_percent } setSearchParams={setSearchParams} urlParam="yearly_decay_percent" @@ -677,7 +720,7 @@ const SearchDisplay: React.FC = ({
{(!searchParams.get("search_mode") && - DEFAULT_SEARCH_MODE === "phrase") || + adminParams.search_mode === "phrase") || searchParams.get("search_mode") === "phrase" ? (
Slop @@ -686,7 +729,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("slop") ? Number(searchParams.get("slop")) - : DEFAULT_SEARCH_SLOP + : adminParams.slop } setSearchParams={setSearchParams} urlParam="slop" @@ -706,7 +749,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("min_score") ? Number(searchParams.get("min_score")) - : DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF + : adminParams.min_score } setSearchParams={setSearchParams} urlParam="min_score" @@ -725,7 +768,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("max_incompleteness_penalty") ? Number(searchParams.get("max_incompleteness_penalty")) - : DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY + : adminParams.max_incompleteness_penalty } setSearchParams={setSearchParams} urlParam="max_incompleteness_penalty" @@ -747,7 +790,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("content_file_score_weight") ? Number(searchParams.get("content_file_score_weight")) - : 1 + : adminParams.content_file_score_weight } setSearchParams={setSearchParams} urlParam="content_file_score_weight" @@ -782,7 +825,7 @@ const SearchDisplay: React.FC = ({ facetOptions={data?.metadata.aggregations ?? {}} /> {user?.is_learning_path_editor - ? AdminOptions(expandAdminOptions, setExpandAdminOptions) + ? AdminOptions(expandAdminOptions, setExpandAdminOptions, adminParams) : null} diff --git a/frontends/mit-learn/src/page-components/SearchField/SearchField.tsx b/frontends/mit-learn/src/page-components/SearchField/SearchField.tsx index 5de4ede3d4..42040f8b6f 100644 --- a/frontends/mit-learn/src/page-components/SearchField/SearchField.tsx +++ b/frontends/mit-learn/src/page-components/SearchField/SearchField.tsx @@ -5,7 +5,7 @@ import { usePostHog } from "posthog-js/react" type SearchFieldProps = SearchInputProps & { onSubmit: (event: SearchSubmissionEvent) => void - setPage: (page: number) => void + setPage?: (page: number) => void } const { POSTHOG } = APP_SETTINGS @@ -26,7 +26,7 @@ const SearchField: React.FC = ({ { isEnter } = {}, ) => { onSubmit(event) - setPage(1) + setPage && setPage(1) if (POSTHOG?.api_key) { posthog.capture("search_update", { isEnter: isEnter }) } diff --git a/frontends/mit-learn/src/pages/HomePage/HeroSearch.tsx b/frontends/mit-learn/src/pages/HomePage/HeroSearch.tsx index efe922953b..fc0fa00749 100644 --- a/frontends/mit-learn/src/pages/HomePage/HeroSearch.tsx +++ b/frontends/mit-learn/src/pages/HomePage/HeroSearch.tsx @@ -5,7 +5,6 @@ import { styled, ChipLink, Link, - SearchInput, SearchInputProps, } from "ol-components" import type { ChipLinkProps } from "ol-components" @@ -27,6 +26,7 @@ import { RiVerifiedBadgeLine, } from "@remixicon/react" import _ from "lodash" +import { SearchField } from "@/page-components/SearchField/SearchField" type SearchChip = { label: string @@ -236,7 +236,7 @@ const HeroSearch: React.FC = () => { - { const adminOptions = screen.queryByText("Admin Options") expect(adminOptions).toBeNull() }) + + expect(makeRequest).not.toHaveBeenCalledWith( + "get", + urls.adminSearchParams, + expect.anything(), + ) }) test("non admin users do not see admin options", async () => { @@ -274,12 +280,19 @@ describe("SearchPage", () => { is_authenticated: true, is_learning_path_editor: false, }) + renderWithProviders() await waitFor(() => { const adminOptions = screen.queryByText("Admin Options") expect(adminOptions).toBeNull() }) + + expect(makeRequest).not.toHaveBeenCalledWith( + "get", + urls.adminSearchParams, + expect.anything(), + ) }) test("admin users can set staleness and score cutoff sliders", async () => { @@ -305,11 +318,16 @@ describe("SearchPage", () => { setMockResponse.get(urls.userMe.get(), { is_learning_path_editor: true, }) - APP_SETTINGS.DEFAULT_SEARCH_MODE = "phrase" - APP_SETTINGS.DEFAULT_SEARCH_SLOP = 6 - APP_SETTINGS.DEFAULT_SEARCH_STALENESS_PENALTY = 2.5 - APP_SETTINGS.DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF = 0 - APP_SETTINGS.DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY = 90 + + setMockResponse.get(urls.adminSearchParams.get(), { + search_mode: "phrase", + slop: 6, + yearly_decay_percent: 2.5, + min_score: 0, + max_incompleteness_penalty: 90, + content_file_score_weight: 1, + }) + renderWithProviders() await waitFor(() => { const adminFacetContainer = screen.getByText("Admin Options") @@ -325,11 +343,6 @@ describe("SearchPage", () => { }) test("admin users can set the search mode and slop", async () => { - APP_SETTINGS.DEFAULT_SEARCH_MODE = "phrase" - APP_SETTINGS.DEFAULT_SEARCH_SLOP = 6 - APP_SETTINGS.DEFAULT_SEARCH_STALENESS_PENALTY = 2.5 - APP_SETTINGS.DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF = 0 - APP_SETTINGS.DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY = 90 setMockApiResponses({ search: { count: 700, @@ -352,6 +365,15 @@ test("admin users can set the search mode and slop", async () => { setMockResponse.get(urls.userMe.get(), { is_learning_path_editor: true, }) + setMockResponse.get(urls.adminSearchParams.get(), { + search_mode: "phrase", + slop: 6, + yearly_decay_percent: 2.5, + min_score: 0, + max_incompleteness_penalty: 90, + content_file_score_weight: 1, + }) + const { location } = renderWithProviders() await waitFor(() => { const adminFacetContainer = screen.getByText("Admin Options") diff --git a/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx b/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx index 1b3cfd56a7..d36f5f6848 100644 --- a/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx +++ b/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx @@ -15,7 +15,6 @@ import type { LearningResourceOfferor } from "api" import { useOfferorsList } from "api/hooks/learningResources" import { capitalize } from "ol-utilities" import MetaTags from "@/page-components/MetaTags/MetaTags" -import { usePostHog } from "posthog-js/react" const cssGradient = ` linear-gradient( @@ -171,8 +170,6 @@ const useFacetManifest = (resourceCategory: string | null) => { const SearchPage: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams() const facetManifest = useFacetManifest(searchParams.get("resource_category")) - const posthog = usePostHog() - const { POSTHOG } = APP_SETTINGS const setPage = useCallback( (newPage: number) => { @@ -189,11 +186,8 @@ const SearchPage: React.FC = () => { [setSearchParams], ) const onFacetsChange = useCallback(() => { - if (!(!POSTHOG?.api_key || POSTHOG.api_key.length < 1)) { - posthog.capture("search_update") - } setPage(1) - }, [setPage, posthog, POSTHOG]) + }, [setPage]) const { params, diff --git a/frontends/mit-learn/webpack.config.js b/frontends/mit-learn/webpack.config.js index 5adf71258b..7e49fd3bbb 100644 --- a/frontends/mit-learn/webpack.config.js +++ b/frontends/mit-learn/webpack.config.js @@ -45,11 +45,6 @@ const { CSRF_COOKIE_NAME, APPZI_URL, MITOL_NOINDEX, - DEFAULT_SEARCH_MODE, - DEFAULT_SEARCH_SLOP, - DEFAULT_SEARCH_STALENESS_PENALTY, - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF, - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY, } = cleanEnv(process.env, { NODE_ENV: str({ choices: ["development", "production", "test"], @@ -129,26 +124,6 @@ const { desc: "Whether to include a noindex meta tag", default: true, }), - DEFAULT_SEARCH_SLOP: num({ - desc: "The default search slop", - default: 6, - }), - DEFAULT_SEARCH_STALENESS_PENALTY: num({ - desc: "The default search staleness penalty", - default: 2.5, - }), - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF: num({ - desc: "The default search minimum score cutoff", - default: 5, - }), - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY: num({ - desc: "The default search max incompleteness penalty", - default: 90, - }), - DEFAULT_SEARCH_MODE: str({ - desc: "The default search mode", - default: "phrase", - }), }) const MITOL_FEATURES_PREFIX = "FEATURE_" @@ -290,11 +265,6 @@ module.exports = (env, argv) => { MITOL_SUPPORT_EMAIL: JSON.stringify(MITOL_SUPPORT_EMAIL), PUBLIC_URL: JSON.stringify(PUBLIC_URL), CSRF_COOKIE_NAME: JSON.stringify(CSRF_COOKIE_NAME), - DEFAULT_SEARCH_MODE: JSON.stringify(DEFAULT_SEARCH_MODE), - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY, - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF, - DEFAULT_SEARCH_SLOP, - DEFAULT_SEARCH_STALENESS_PENALTY, }, }), ] diff --git a/frontends/ol-components/src/components/Dialog/Dialog.tsx b/frontends/ol-components/src/components/Dialog/Dialog.tsx index b90fbfde7b..9ee8b4571f 100644 --- a/frontends/ol-components/src/components/Dialog/Dialog.tsx +++ b/frontends/ol-components/src/components/Dialog/Dialog.tsx @@ -23,7 +23,9 @@ const Header = styled.div` ` const Content = styled.div` - margin: 28px 28px 40px; + margin: 28px; + min-height: 0; + overflow: auto; ` const DialogActions = styled(MuiDialogActions)` diff --git a/frontends/ol-components/src/components/FormDialog/FormDialog.tsx b/frontends/ol-components/src/components/FormDialog/FormDialog.tsx index 85d7cbb281..3f82e535d2 100644 --- a/frontends/ol-components/src/components/FormDialog/FormDialog.tsx +++ b/frontends/ol-components/src/components/FormDialog/FormDialog.tsx @@ -8,7 +8,6 @@ const FormContent = styled.div` flex-direction: column; gap: 20px; width: 100%; - margin-bottom: -12px; ` interface FormDialogProps { /** diff --git a/frontends/ol-components/src/components/ThemeProvider/ThemeProvider.tsx b/frontends/ol-components/src/components/ThemeProvider/ThemeProvider.tsx index f2bb97970d..e5783bf93e 100644 --- a/frontends/ol-components/src/components/ThemeProvider/ThemeProvider.tsx +++ b/frontends/ol-components/src/components/ThemeProvider/ThemeProvider.tsx @@ -77,7 +77,13 @@ const themeOptions: ThemeOptions = { styleOverrides: { paper: { borderRadius: "4px" } }, }, MuiAutocomplete: { - styleOverrides: { paper: { borderRadius: "4px" } }, + styleOverrides: { + paper: { borderRadius: "4px" }, + // Mui puts paddingRight: 2px, marginRight: -2px on the popupIndicator, + // which causes the browser to show a horizontal scrollbar on overflow + // containers when a scrollbar isn't really necessary. + popupIndicator: { paddingRight: 0, marginRight: 0 }, + }, }, MuiChip: chips.chipComponent, }, diff --git a/frontends/ol-utilities/src/types/settings.d.ts b/frontends/ol-utilities/src/types/settings.d.ts index 553c75e0c8..da51840dd7 100644 --- a/frontends/ol-utilities/src/types/settings.d.ts +++ b/frontends/ol-utilities/src/types/settings.d.ts @@ -22,10 +22,5 @@ export declare global { SITE_NAME: string MITOL_SUPPORT_EMAIL: string PUBLIC_URL: string - DEFAULT_SEARCH_MODE: string - DEFAULT_SEARCH_SLOP: number - DEFAULT_SEARCH_STALENESS_PENALTY: number - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF: number - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY: number } } diff --git a/learning_resources_search/urls.py b/learning_resources_search/urls.py index 029ae81492..3e5825b619 100644 --- a/learning_resources_search/urls.py +++ b/learning_resources_search/urls.py @@ -5,6 +5,7 @@ from learning_resources_search.views import ( ContentFileSearchView, + LearningResourceSearchDefaultsView, LearningResourcesSearchView, UserSearchSubscriptionViewSet, ) @@ -30,7 +31,16 @@ ), ] +v0_urls = [ + path( + r"learning_resources_search_admin_params/", + LearningResourceSearchDefaultsView.as_view(), + name="learning_resources_search_admin_params", + ), +] + app_name = "lr_search" urlpatterns = [ re_path(r"^api/v1/", include((v1_urls, "v1"))), + re_path(r"^api/v0/", include((v0_urls, "v0"))), ] diff --git a/learning_resources_search/views.py b/learning_resources_search/views.py index b61a2310a9..70ad54ecba 100644 --- a/learning_resources_search/views.py +++ b/learning_resources_search/views.py @@ -4,6 +4,7 @@ from itertools import chain from django.conf import settings +from django.http import JsonResponse from django.utils.decorators import method_decorator from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view from opensearchpy.exceptions import TransportError @@ -265,3 +266,29 @@ def get(self, request): errors[key] = list(set(chain(*errors_obj.values()))) return Response(errors, status=400) + + +@action(methods=["GET"], detail=False, name="Search Defaults") +@extend_schema_view( + get=extend_schema( + parameters=None, + ), +) +class LearningResourceSearchDefaultsView(APIView): + """Learning resource search default admin param values""" + + authentication_classes = [] + permission_classes = [] + serializer_class = None + + def get(self, _): + return JsonResponse( + { + "search_mode": settings.DEFAULT_SEARCH_MODE, + "slop": settings.DEFAULT_SEARCH_SLOP, + "yearly_decay_percent": settings.DEFAULT_SEARCH_STALENESS_PENALTY, + "min_score": settings.DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF, + "max_incompleteness_penalty": settings.DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY, # noqa: E501 + "content_file_score_weight": settings.DEFAULT_SEARCH_CONTENT_FILE_SCORE_WEIGHT, # noqa: E501 + } + ) diff --git a/main/settings.py b/main/settings.py index a36cc1c800..ae31561d85 100644 --- a/main/settings.py +++ b/main/settings.py @@ -33,7 +33,7 @@ from main.settings_pluggy import * # noqa: F403 from openapi.settings_spectacular import open_spectacular_settings -VERSION = "0.21.3" +VERSION = "0.22.0" log = logging.getLogger() @@ -779,3 +779,6 @@ def get_all_config_keys(): DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY = get_float( name="DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY", default=90 ) +DEFAULT_SEARCH_CONTENT_FILE_SCORE_WEIGHT = get_float( + name="DEFAULT_SEARCH_CONTENT_FILE_SCORE_WEIGHT", default=1 +) diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index bea72ed810..b080e4cebf 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -288,6 +288,15 @@ paths: schema: $ref: '#/components/schemas/CKEditorSettings' description: '' + /api/v0/learning_resources_search_admin_params/: + get: + operationId: learning_resources_search_admin_params_retrieve + description: Learning resource search default admin param values + tags: + - learning_resources_search_admin_params + responses: + '200': + description: No response body /api/v0/news_events/: get: operationId: news_events_list