diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c228eea714..a0bc915a41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5 with: - python-version: "3.12.5" + python-version: "3.12.6" cache: "poetry" - name: Validate lockfile diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a30546482..9294da061b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,7 +74,7 @@ repos: - ".*/generated/" additional_dependencies: ["gibberish-detector"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.6.6" + rev: "v0.6.7" hooks: - id: ruff-format - id: ruff diff --git a/Dockerfile b/Dockerfile index 9d3cf6721e..85076f0560 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.5 +FROM python:3.12.6 LABEL maintainer "ODL DevOps " # Add package files, install updated node and pip diff --git a/RELEASE.rst b/RELEASE.rst index 1ae708ba43..296f6daec5 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,56 @@ Release Notes ============= +Version 0.20.3 (Released October 03, 2024) +-------------- + +- add is_incomplete_or_stale (#1627) +- "unfollow" confirmation modal and "Unfollow All" button (#1628) +- raise SystemExit as a RetryError (#1635) + +Version 0.20.2 (Released October 01, 2024) +-------------- + +- remove border and shadow (#1636) +- set card root to auto height (#1633) + +Version 0.20.1 (Released October 01, 2024) +-------------- + +- fix Safari MIT logo bug (#1631) +- Retry recreate_index subtasks on worker exit (#1615) +- Email for saved searches (#1619) + +Version 0.20.0 (Released October 01, 2024) +-------------- + +- updated header (#1622) +- Switch to using full name (#1621) +- Update dependency lxml to v5 (#1554) +- Remove health checks against opensearch (#1620) +- Add additional event capturing for some interactions (#1596) +- Revert "changes for formatting search subscription emails" +- changes for formatting search subscription emails +- Add custom label for percolate query subscriptions (#1610) +- Update Python to v3.12.6 (#1593) +- [pre-commit.ci] pre-commit autoupdate (#1601) + +Version 0.19.6 (Released September 27, 2024) +-------------- + +- Add task_reject_on_worker_lost=True to finish_recreate_index, use return instead of raise for replaced tasks (#1608) + +Version 0.19.5 (Released September 26, 2024) +-------------- + +- Add separate field for ocw topics, use best field to assign related topics (#1600) + +Version 0.19.4 (Released September 25, 2024) +-------------- + +- new -> recently added (#1594) +- Pace and format fields for learning resources (#1588) + Version 0.19.3 (Released September 23, 2024) -------------- diff --git a/data_fixtures/migrations/0015_unit_page_copy_updates.py b/data_fixtures/migrations/0015_unit_page_copy_updates.py new file mode 100644 index 0000000000..769ad6cc9d --- /dev/null +++ b/data_fixtures/migrations/0015_unit_page_copy_updates.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.14 on 2024-07-16 17:30 + +from django.db import migrations + +fixtures = [ + { + "name": "mitpe", + "offeror_configuration": { + "value_prop": ( + "MIT Professional Education is a leader in technology and " + "engineering education for working professionals pursuing " + "career advancement, and organizations seeking to meet modern-day " + "challenges by expanding the knowledge and skills of their employees. " + "Courses are delivered in a range of formats—in-person (on-campus " + "and live online), online, and through hybrid approaches—to " + "meet the needs of today's learners." + ), + }, + "channel_configuration": { + "sub_heading": ( + "MIT Professional Education is a leader in technology and " + "engineering education for working professionals pursuing " + "career advancement, and organizations seeking to meet modern-day " + "challenges by expanding the knowledge and skills of their employees. " + "Courses are delivered in a range of formats—in-person (on-campus " + "and live online), online, and through hybrid approaches—to " + "meet the needs of today's learners." + ), + }, + }, +] + + +def update_copy(apps, schema_editor): + Channel = apps.get_model("channels", "Channel") + LearningResourceOfferor = apps.get_model( + "learning_resources", "LearningResourceOfferor" + ) + for fixture in fixtures: + channel_configuration_updates = fixture["channel_configuration"] + offeror_configuration_updates = fixture["offeror_configuration"] + channel = Channel.objects.get(name=fixture["name"]) + if Channel.objects.filter(name=fixture["name"]).exists(): + for key, val in channel_configuration_updates.items(): + channel.configuration[key] = val + channel.save() + if LearningResourceOfferor.objects.filter(code=fixture["name"]).exists(): + offeror = LearningResourceOfferor.objects.get(code=fixture["name"]) + for key, val in offeror_configuration_updates.items(): + setattr(offeror, key, val) + offeror.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("data_fixtures", "0014_add_department_SP"), + ] + + operations = [ + migrations.RunPython(update_copy, migrations.RunPython.noop), + ] diff --git a/docker-compose.apps.yml b/docker-compose.apps.yml index bacbf2107e..87b55552d2 100644 --- a/docker-compose.apps.yml +++ b/docker-compose.apps.yml @@ -8,9 +8,6 @@ services: build: context: . dockerfile: Dockerfile - extends: - file: docker-compose.opensearch.${OPENSEARCH_CLUSTER_TYPE:-single-node}.apps.yml - service: web mem_limit: 1gb cpus: 2 command: ./scripts/run-django-dev.sh @@ -54,14 +51,10 @@ services: build: context: . dockerfile: Dockerfile - extends: - file: docker-compose.opensearch.${OPENSEARCH_CLUSTER_TYPE:-single-node}.apps.yml - service: web command: > /bin/bash -c ' sleep 3; - celery -A main.celery:app worker -Q default -B -l ${MITOL_LOG_LEVEL:-INFO} & - celery -A main.celery:app worker -Q edx_content,default -l ${MITOL_LOG_LEVEL:-INFO}' + celery -A main.celery:app worker -E -Q default,edx_content -B -l ${MITOL_LOG_LEVEL:-INFO}' depends_on: db: condition: service_healthy diff --git a/docker-compose.opensearch.cluster.apps.yml b/docker-compose.opensearch.cluster.apps.yml deleted file mode 100644 index cd473ffef1..0000000000 --- a/docker-compose.opensearch.cluster.apps.yml +++ /dev/null @@ -1,17 +0,0 @@ -services: - web: - depends_on: - opensearch-node-mitopen-1: - condition: service_healthy - opensearch-node-mitopen-2: - condition: service_healthy - opensearch-node-mitopen-3: - condition: service_healthy - celery: - depends_on: - opensearch-node-mitopen-1: - condition: service_healthy - opensearch-node-mitopen-2: - condition: service_healthy - opensearch-node-mitopen-3: - condition: service_healthy diff --git a/docker-compose.opensearch.single-node.apps.yml b/docker-compose.opensearch.single-node.apps.yml deleted file mode 100644 index 9a66e038f3..0000000000 --- a/docker-compose.opensearch.single-node.apps.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - web: - depends_on: - opensearch-node-mitopen-1: - condition: service_healthy - celery: - depends_on: - opensearch-node-mitopen-1: - condition: service_healthy diff --git a/fixtures/common.py b/fixtures/common.py index 724d6f3569..4f18431505 100644 --- a/fixtures/common.py +++ b/fixtures/common.py @@ -44,7 +44,7 @@ def warnings_as_errors(): # noqa: PT004 # Ignore deprecation warnings in third party libraries warnings.filterwarnings( "ignore", - module=".*(api_jwt|api_jws|rest_framework_jwt|astroid|celery|factory|botocore|posthog).*", + module=".*(api_jwt|api_jws|rest_framework_jwt|astroid|bs4|celery|factory|botocore|posthog).*", category=DeprecationWarning, ) yield diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index a8ecc603ce..694b9b5086 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -1597,12 +1597,6 @@ export interface PatchedChannelWriteRequest { * @interface PatchedProfileRequest */ export interface PatchedProfileRequest { - /** - * - * @type {string} - * @memberof PatchedProfileRequest - */ - name?: string | null /** * * @type {string} @@ -1928,11 +1922,11 @@ export interface PreferencesSearch { */ export interface Profile { /** - * + * Get the user\'s name * @type {string} * @memberof Profile */ - name?: string | null + name: string /** * * @type {string} @@ -2054,12 +2048,6 @@ export interface Profile { * @interface ProfileRequest */ export interface ProfileRequest { - /** - * - * @type {string} - * @memberof ProfileRequest - */ - name?: string | null /** * * @type {string} diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index ff24f6357f..62385b9658 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -758,6 +758,12 @@ export interface CourseResource { * @memberof CourseResource */ url?: string | null + /** + * + * @type {Array} + * @memberof CourseResource + */ + ocw_topics?: Array /** * * @type {boolean} @@ -972,6 +978,12 @@ export interface CourseResourceRequest { * @memberof CourseResourceRequest */ url?: string | null + /** + * + * @type {Array} + * @memberof CourseResourceRequest + */ + ocw_topics?: Array /** * * @type {string} @@ -1516,6 +1528,12 @@ export interface LearningPathResource { * @memberof LearningPathResource */ url?: string | null + /** + * + * @type {Array} + * @memberof LearningPathResource + */ + ocw_topics?: Array /** * * @type {boolean} @@ -1608,6 +1626,12 @@ export interface LearningPathResourceRequest { * @memberof LearningPathResourceRequest */ url?: string | null + /** + * + * @type {Array} + * @memberof LearningPathResourceRequest + */ + ocw_topics?: Array /** * * @type {boolean} @@ -3439,6 +3463,12 @@ export interface PatchedLearningPathResourceRequest { * @memberof PatchedLearningPathResourceRequest */ url?: string | null + /** + * + * @type {Array} + * @memberof PatchedLearningPathResourceRequest + */ + ocw_topics?: Array /** * * @type {boolean} @@ -3608,6 +3638,12 @@ export interface PercolateQuery { * @memberof PercolateQuery */ source_type: SourceTypeEnum + /** + * Friendly display label for the query + * @type {string} + * @memberof PercolateQuery + */ + display_label?: string } /** @@ -3652,6 +3688,12 @@ export interface PercolateQuerySubscriptionRequestRequest { * @memberof PercolateQuerySubscriptionRequestRequest */ topic?: Array + /** + * The ocw topic name. + * @type {Array} + * @memberof PercolateQuerySubscriptionRequestRequest + */ + ocw_topic?: Array /** * If true return raw open search results with score explanations * @type {boolean} @@ -4213,6 +4255,12 @@ export interface PodcastEpisodeResource { * @memberof PodcastEpisodeResource */ url?: string | null + /** + * + * @type {Array} + * @memberof PodcastEpisodeResource + */ + ocw_topics?: Array /** * * @type {boolean} @@ -4311,6 +4359,12 @@ export interface PodcastEpisodeResourceRequest { * @memberof PodcastEpisodeResourceRequest */ url?: string | null + /** + * + * @type {Array} + * @memberof PodcastEpisodeResourceRequest + */ + ocw_topics?: Array /** * * @type {string} @@ -4571,6 +4625,12 @@ export interface PodcastResource { * @memberof PodcastResource */ url?: string | null + /** + * + * @type {Array} + * @memberof PodcastResource + */ + ocw_topics?: Array /** * * @type {boolean} @@ -4669,6 +4729,12 @@ export interface PodcastResourceRequest { * @memberof PodcastResourceRequest */ url?: string | null + /** + * + * @type {Array} + * @memberof PodcastResourceRequest + */ + ocw_topics?: Array /** * * @type {string} @@ -5149,6 +5215,12 @@ export interface ProgramResource { * @memberof ProgramResource */ url?: string | null + /** + * + * @type {Array} + * @memberof ProgramResource + */ + ocw_topics?: Array /** * * @type {boolean} @@ -5247,6 +5319,12 @@ export interface ProgramResourceRequest { * @memberof ProgramResourceRequest */ url?: string | null + /** + * + * @type {Array} + * @memberof ProgramResourceRequest + */ + ocw_topics?: Array /** * * @type {string} @@ -5986,6 +6064,12 @@ export interface VideoPlaylistResource { * @memberof VideoPlaylistResource */ url?: string | null + /** + * + * @type {Array} + * @memberof VideoPlaylistResource + */ + ocw_topics?: Array /** * * @type {boolean} @@ -6084,6 +6168,12 @@ export interface VideoPlaylistResourceRequest { * @memberof VideoPlaylistResourceRequest */ url?: string | null + /** + * + * @type {Array} + * @memberof VideoPlaylistResourceRequest + */ + ocw_topics?: Array /** * * @type {string} @@ -6332,6 +6422,12 @@ export interface VideoResource { * @memberof VideoResource */ url?: string | null + /** + * + * @type {Array} + * @memberof VideoResource + */ + ocw_topics?: Array /** * * @type {boolean} @@ -6430,6 +6526,12 @@ export interface VideoResourceRequest { * @memberof VideoResourceRequest */ url?: string | null + /** + * + * @type {Array} + * @memberof VideoResourceRequest + */ + ocw_topics?: Array /** * * @type {string} @@ -7190,6 +7292,7 @@ export const ContentFileSearchApiAxiosParamCreator = function ( * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {Array} [id] The id value for the content file * @param {number} [limit] Number of results to return per page + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -7207,6 +7310,7 @@ export const ContentFileSearchApiAxiosParamCreator = function ( dev_mode?: boolean | null, id?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -7253,6 +7357,10 @@ export const ContentFileSearchApiAxiosParamCreator = function ( localVarQueryParameter["limit"] = limit } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -7318,6 +7426,7 @@ export const ContentFileSearchApiFp = function (configuration?: Configuration) { * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {Array} [id] The id value for the content file * @param {number} [limit] Number of results to return per page + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -7335,6 +7444,7 @@ export const ContentFileSearchApiFp = function (configuration?: Configuration) { dev_mode?: boolean | null, id?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -7357,6 +7467,7 @@ export const ContentFileSearchApiFp = function (configuration?: Configuration) { dev_mode, id, limit, + ocw_topic, offered_by, offset, platform, @@ -7412,6 +7523,7 @@ export const ContentFileSearchApiFactory = function ( requestParameters.dev_mode, requestParameters.id, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -7468,6 +7580,13 @@ export interface ContentFileSearchApiContentFileSearchRetrieveRequest { */ readonly limit?: number + /** + * The ocw topic name. + * @type {Array} + * @memberof ContentFileSearchApiContentFileSearchRetrieve + */ + readonly ocw_topic?: Array + /** * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see'>} @@ -7551,6 +7670,7 @@ export class ContentFileSearchApi extends BaseAPI { requestParameters.dev_mode, requestParameters.id, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -12619,6 +12739,7 @@ export const LearningResourcesSearchApiAxiosParamCreator = function ( * @param {number} [limit] Number of results to return per page * @param {number | null} [max_incompleteness_penalty] Maximum score penalty for incomplete OCW courses in percent. An OCW course with completeness = 0 will have this score penalty. Partially complete courses have a linear penalty proportional to the degree of incompleteness. Only affects results if there is a search term. * @param {number | null} [min_score] Minimum score value a text query result needs to have to be displayed + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -12648,6 +12769,7 @@ export const LearningResourcesSearchApiAxiosParamCreator = function ( limit?: number, max_incompleteness_penalty?: number | null, min_score?: number | null, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -12731,6 +12853,10 @@ export const LearningResourcesSearchApiAxiosParamCreator = function ( localVarQueryParameter["min_score"] = min_score } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -12822,6 +12948,7 @@ export const LearningResourcesSearchApiFp = function ( * @param {number} [limit] Number of results to return per page * @param {number | null} [max_incompleteness_penalty] Maximum score penalty for incomplete OCW courses in percent. An OCW course with completeness = 0 will have this score penalty. Partially complete courses have a linear penalty proportional to the degree of incompleteness. Only affects results if there is a search term. * @param {number | null} [min_score] Minimum score value a text query result needs to have to be displayed + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -12851,6 +12978,7 @@ export const LearningResourcesSearchApiFp = function ( limit?: number, max_incompleteness_penalty?: number | null, min_score?: number | null, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -12885,6 +13013,7 @@ export const LearningResourcesSearchApiFp = function ( limit, max_incompleteness_penalty, min_score, + ocw_topic, offered_by, offset, platform, @@ -12952,6 +13081,7 @@ export const LearningResourcesSearchApiFactory = function ( requestParameters.limit, requestParameters.max_incompleteness_penalty, requestParameters.min_score, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -13068,6 +13198,13 @@ export interface LearningResourcesSearchApiLearningResourcesSearchRetrieveReques */ readonly min_score?: number | null + /** + * The ocw topic name. + * @type {Array} + * @memberof LearningResourcesSearchApiLearningResourcesSearchRetrieve + */ + readonly ocw_topic?: Array + /** * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see'>} @@ -13187,6 +13324,7 @@ export class LearningResourcesSearchApi extends BaseAPI { requestParameters.limit, requestParameters.max_incompleteness_penalty, requestParameters.min_score, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -13424,6 +13562,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @param {number} [limit] Number of results to return per page * @param {number | null} [max_incompleteness_penalty] Maximum score penalty for incomplete OCW courses in percent. An OCW course with completeness = 0 will have this score penalty. Partially complete courses have a linear penalty proportional to the degree of incompleteness. Only affects results if there is a search term. * @param {number | null} [min_score] Minimum score value a text query result needs to have to be displayed + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -13454,6 +13593,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( limit?: number, max_incompleteness_penalty?: number | null, min_score?: number | null, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -13538,6 +13678,10 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( localVarQueryParameter["min_score"] = min_score } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -13620,6 +13764,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @param {number} [limit] Number of results to return per page * @param {number | null} [max_incompleteness_penalty] Maximum score penalty for incomplete OCW courses in percent. An OCW course with completeness = 0 will have this score penalty. Partially complete courses have a linear penalty proportional to the degree of incompleteness. Only affects results if there is a search term. * @param {number | null} [min_score] Minimum score value a text query result needs to have to be displayed + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -13649,6 +13794,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( limit?: number, max_incompleteness_penalty?: number | null, min_score?: number | null, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -13732,6 +13878,10 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( localVarQueryParameter["min_score"] = min_score } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -13810,6 +13960,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @param {number} [limit] Number of results to return per page * @param {number | null} [max_incompleteness_penalty] Maximum score penalty for incomplete OCW courses in percent. An OCW course with completeness = 0 will have this score penalty. Partially complete courses have a linear penalty proportional to the degree of incompleteness. Only affects results if there is a search term. * @param {number | null} [min_score] Minimum score value a text query result needs to have to be displayed + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -13841,6 +13992,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( limit?: number, max_incompleteness_penalty?: number | null, min_score?: number | null, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -13926,6 +14078,10 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( localVarQueryParameter["min_score"] = min_score } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -14079,6 +14235,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @param {number} [limit] Number of results to return per page * @param {number | null} [max_incompleteness_penalty] Maximum score penalty for incomplete OCW courses in percent. An OCW course with completeness = 0 will have this score penalty. Partially complete courses have a linear penalty proportional to the degree of incompleteness. Only affects results if there is a search term. * @param {number | null} [min_score] Minimum score value a text query result needs to have to be displayed + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -14109,6 +14266,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( limit?: number, max_incompleteness_penalty?: number | null, min_score?: number | null, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -14144,6 +14302,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( limit, max_incompleteness_penalty, min_score, + ocw_topic, offered_by, offset, platform, @@ -14188,6 +14347,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @param {number} [limit] Number of results to return per page * @param {number | null} [max_incompleteness_penalty] Maximum score penalty for incomplete OCW courses in percent. An OCW course with completeness = 0 will have this score penalty. Partially complete courses have a linear penalty proportional to the degree of incompleteness. Only affects results if there is a search term. * @param {number | null} [min_score] Minimum score value a text query result needs to have to be displayed + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -14217,6 +14377,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( limit?: number, max_incompleteness_penalty?: number | null, min_score?: number | null, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -14251,6 +14412,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( limit, max_incompleteness_penalty, min_score, + ocw_topic, offered_by, offset, platform, @@ -14294,6 +14456,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @param {number} [limit] Number of results to return per page * @param {number | null} [max_incompleteness_penalty] Maximum score penalty for incomplete OCW courses in percent. An OCW course with completeness = 0 will have this score penalty. Partially complete courses have a linear penalty proportional to the degree of incompleteness. Only affects results if there is a search term. * @param {number | null} [min_score] Minimum score value a text query result needs to have to be displayed + * @param {Array} [ocw_topic] The ocw topic name. * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube @@ -14325,6 +14488,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( limit?: number, max_incompleteness_penalty?: number | null, min_score?: number | null, + ocw_topic?: Array, offered_by?: Array, offset?: number, platform?: Array, @@ -14358,6 +14522,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( limit, max_incompleteness_penalty, min_score, + ocw_topic, offered_by, offset, platform, @@ -14458,6 +14623,7 @@ export const LearningResourcesUserSubscriptionApiFactory = function ( requestParameters.limit, requestParameters.max_incompleteness_penalty, requestParameters.min_score, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -14501,6 +14667,7 @@ export const LearningResourcesUserSubscriptionApiFactory = function ( requestParameters.limit, requestParameters.max_incompleteness_penalty, requestParameters.min_score, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -14543,6 +14710,7 @@ export const LearningResourcesUserSubscriptionApiFactory = function ( requestParameters.limit, requestParameters.max_incompleteness_penalty, requestParameters.min_score, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -14679,6 +14847,13 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr */ readonly min_score?: number | null + /** + * The ocw topic name. + * @type {Array} + * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionCheckList + */ + readonly ocw_topic?: Array + /** * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see'>} @@ -14868,6 +15043,13 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr */ readonly min_score?: number | null + /** + * The ocw topic name. + * @type {Array} + * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionList + */ + readonly ocw_topic?: Array + /** * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see'>} @@ -15050,6 +15232,13 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr */ readonly min_score?: number | null + /** + * The ocw topic name. + * @type {Array} + * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionSubscribeCreate + */ + readonly ocw_topic?: Array + /** * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see'>} @@ -15197,6 +15386,7 @@ export class LearningResourcesUserSubscriptionApi extends BaseAPI { requestParameters.limit, requestParameters.max_incompleteness_penalty, requestParameters.min_score, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -15242,6 +15432,7 @@ export class LearningResourcesUserSubscriptionApi extends BaseAPI { requestParameters.limit, requestParameters.max_incompleteness_penalty, requestParameters.min_score, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, @@ -15286,6 +15477,7 @@ export class LearningResourcesUserSubscriptionApi extends BaseAPI { requestParameters.limit, requestParameters.max_incompleteness_penalty, requestParameters.min_score, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.offset, requestParameters.platform, diff --git a/frontends/main/public/images/mit-learn-logo.jpg b/frontends/main/public/images/mit-learn-logo.jpg new file mode 100644 index 0000000000..488eae1e1b Binary files /dev/null and b/frontends/main/public/images/mit-learn-logo.jpg differ diff --git a/frontends/main/public/images/mit-learn-logo.svg b/frontends/main/public/images/mit-learn-logo.svg new file mode 100644 index 0000000000..0069f62884 --- /dev/null +++ b/frontends/main/public/images/mit-learn-logo.svg @@ -0,0 +1,5 @@ + + + diff --git a/frontends/main/public/images/mit_logo_std_cmyk_black.svg b/frontends/main/public/images/mit-logo-black.svg similarity index 100% rename from frontends/main/public/images/mit_logo_std_cmyk_black.svg rename to frontends/main/public/images/mit-logo-black.svg diff --git a/frontends/main/public/images/mit-logo-transparent5.svg b/frontends/main/public/images/mit-logo-transparent5.svg deleted file mode 100644 index a85e39a189..0000000000 --- a/frontends/main/public/images/mit-logo-transparent5.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - -logo2 - - - - - - - diff --git a/frontends/main/public/images/mit-logo-white.svg b/frontends/main/public/images/mit-logo-white.svg new file mode 100644 index 0000000000..5b0774426b --- /dev/null +++ b/frontends/main/public/images/mit-logo-white.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/frontends/main/src/app-pages/ChannelPage/ChannelSearch.test.tsx b/frontends/main/src/app-pages/ChannelPage/ChannelSearch.test.tsx index 02ec52246b..e3413496c2 100644 --- a/frontends/main/src/app-pages/ChannelPage/ChannelSearch.test.tsx +++ b/frontends/main/src/app-pages/ChannelPage/ChannelSearch.test.tsx @@ -1,5 +1,11 @@ import React from "react" -import { screen, within, waitFor, renderWithProviders } from "@/test-utils" +import { + screen, + within, + waitFor, + renderWithProviders, + user, +} from "@/test-utils" import { setMockResponse, urls, factories, makeRequest } from "api/test-utils" import type { LearningResourcesSearchResponse } from "api" import invariant from "tiny-invariant" @@ -270,4 +276,45 @@ describe("ChannelSearch", () => { } }, ) + + test("Submitting search text updates URL correctly", async () => { + const resources = factories.learningResources.resources({ + count: 10, + }).results + const { channel } = setMockApiResponses({ + search: { + count: 1000, + metadata: { + aggregations: { + resource_type: [ + { key: "course", doc_count: 100 }, + { key: "podcast", doc_count: 200 }, + { key: "program", doc_count: 300 }, + { key: "irrelevant", doc_count: 400 }, + ], + }, + suggestions: [], + }, + results: resources, + }, + }) + setMockResponse.get(urls.userMe.get(), {}) + + const initialSearch = "?q=meow&page=2" + const finalSearch = "?q=woof" + + const { location } = renderWithProviders(, { + url: `/c/${channel.channel_type}/${channel.name}${initialSearch}`, + }) + + const queryInput = await screen.findByRole("textbox", { + name: "Search for", + }) + expect(queryInput.value).toBe("meow") + await user.clear(queryInput) + await user.paste("woof") + expect(location.current.search).toBe(initialSearch) + await user.click(screen.getByRole("button", { name: "Search" })) + expect(location.current.search).toBe(finalSearch) + }) }) diff --git a/frontends/main/src/app-pages/ChannelPage/ChannelSearch.tsx b/frontends/main/src/app-pages/ChannelPage/ChannelSearch.tsx index 54e770aaa7..e756bb4e8e 100644 --- a/frontends/main/src/app-pages/ChannelPage/ChannelSearch.tsx +++ b/frontends/main/src/app-pages/ChannelPage/ChannelSearch.tsx @@ -14,7 +14,8 @@ import type { } from "@mitodl/course-search-utils" import { useSearchParams } from "@mitodl/course-search-utils/next" import SearchDisplay from "@/page-components/SearchDisplay/SearchDisplay" -import { Container, SearchInput, styled, VisuallyHidden } from "ol-components" +import { Container, styled, VisuallyHidden } from "ol-components" +import { SearchField } from "@/page-components/SearchField/SearchField" import { getFacetManifest } from "@/app-pages/SearchPage/SearchPage" @@ -30,7 +31,7 @@ const SearchInputContainer = styled(Container)(({ theme }) => ({ }, })) -const StyledSearchInput = styled(SearchInput)({ +const StyledSearchField = styled(SearchField)({ width: "624px", }) @@ -172,7 +173,7 @@ const ChannelSearch: React.FC = ({
Search within {channelTitle} - setCurrentText(e.target.value)} @@ -182,6 +183,7 @@ const ChannelSearch: React.FC = ({ onClear={() => { setCurrentTextAndQuery("") }} + setPage={setPage} /> diff --git a/frontends/main/src/app-pages/ChannelPage/ChannelSearchFacetDisplay.tsx b/frontends/main/src/app-pages/ChannelPage/ChannelSearchFacetDisplay.tsx index 2da87b65d5..c5404b191e 100644 --- a/frontends/main/src/app-pages/ChannelPage/ChannelSearchFacetDisplay.tsx +++ b/frontends/main/src/app-pages/ChannelPage/ChannelSearchFacetDisplay.tsx @@ -8,9 +8,8 @@ import type { BooleanFacetKey, } from "@mitodl/course-search-utils" import { BOOLEAN_FACET_NAMES } from "@mitodl/course-search-utils" -import { Skeleton, styled } from "ol-components" +import { Skeleton, styled, SimpleSelect } from "ol-components" import type { SimpleSelectOption } from "ol-components" -import { StyledSelect } from "@/page-components/SearchDisplay/SearchDisplay" const StyledSkeleton = styled(Skeleton)` display: inline-flex; @@ -117,7 +116,7 @@ const AvailableFacetsDropdowns: React.FC< return ( facetItems.length && ( - { setupAPIs() setMockResponse.get(urls.userMe.get(), { [Permissions.Authenticated]: true, - first_name: "User", - last_name: "Info", + first_name: "Joe", + last_name: "Smith", + profile: { + name: "Jane Smith", + }, }) renderWithProviders() await waitFor(() => { /** - * There should be two instances of "User Info" text, + * There should be two instances of "Jane Smith" text, * one in the header and one in the main content */ - const userInfoText = screen.getByText("User Info") + const userInfoText = screen.getByText("Jane Smith") expect(userInfoText).toBeInTheDocument() }) }) diff --git a/frontends/main/src/app-pages/DashboardPage/DashboardPage.tsx b/frontends/main/src/app-pages/DashboardPage/DashboardPage.tsx index 5402943068..838228df00 100644 --- a/frontends/main/src/app-pages/DashboardPage/DashboardPage.tsx +++ b/frontends/main/src/app-pages/DashboardPage/DashboardPage.tsx @@ -336,7 +336,7 @@ const DashboardPage: React.FC = () => { {isLoadingUser ? ( ) : ( - {`${user?.first_name} ${user?.last_name}`} + {`${user?.profile?.name}`} )} diff --git a/frontends/main/src/app-pages/DashboardPage/ProfileEditForm.tsx b/frontends/main/src/app-pages/DashboardPage/ProfileEditForm.tsx index eefdbe2f66..b7e89a58d9 100644 --- a/frontends/main/src/app-pages/DashboardPage/ProfileEditForm.tsx +++ b/frontends/main/src/app-pages/DashboardPage/ProfileEditForm.tsx @@ -103,17 +103,10 @@ const ProfileEditForm: React.FC = ({ profile }) => { - diff --git a/frontends/main/src/app-pages/DashboardPage/SettingsPage.test.tsx b/frontends/main/src/app-pages/DashboardPage/SettingsPage.test.tsx index 38812527f3..16bcb2e1e3 100644 --- a/frontends/main/src/app-pages/DashboardPage/SettingsPage.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/SettingsPage.test.tsx @@ -25,10 +25,15 @@ const setupApis = ({ `${urls.userSubscription.check(subscriptionRequest)}`, subscribeResponse, ) - const unsubscribeUrl = urls.userSubscription.delete(subscribeResponse[0]?.id) - setMockResponse.delete(unsubscribeUrl, subscribeResponse[0]) + const unsubscribeUrls = [] + for (const sub of subscribeResponse) { + const unsubscribeUrl = urls.userSubscription.delete(sub?.id) + unsubscribeUrls.push(unsubscribeUrl) + setMockResponse.delete(unsubscribeUrl, sub) + } + return { - unsubscribeUrl, + unsubscribeUrls, } } @@ -46,7 +51,7 @@ describe("SettingsPage", () => { }) test("Clicking 'Unfollow' removes the subscription", async () => { - const { unsubscribeUrl } = setupApis({ + const { unsubscribeUrls } = setupApis({ isAuthenticated: true, isSubscribed: true, subscriptionRequest: {}, @@ -54,13 +59,42 @@ describe("SettingsPage", () => { renderWithProviders() const followList = await screen.findByTestId("follow-list") - const unsubscribeButton = within(followList).getAllByText("Unfollow")[0] - await user.click(unsubscribeButton) + const unsubscribeLink = within(followList).getAllByText("Unfollow")[0] + await user.click(unsubscribeLink) + const unsubscribeButton = await screen.findByTestId("dialog-unfollow") + await user.click(unsubscribeButton) expect(makeRequest).toHaveBeenCalledWith( "delete", - unsubscribeUrl, + unsubscribeUrls[0], undefined, ) }) + + test("Clicking 'Unfollow All' removes all subscriptions", async () => { + const { unsubscribeUrls } = setupApis({ + isAuthenticated: true, + isSubscribed: true, + subscriptionRequest: {}, + }) + renderWithProviders() + const unsubscribeLink = await screen.findByTestId("unfollow-all") + await user.click(unsubscribeLink) + + const unsubscribeButton = await screen.findByTestId("dialog-unfollow") + await user.click(unsubscribeButton) + for (const unsubUrl of unsubscribeUrls) { + expect(makeRequest).toHaveBeenCalledWith("delete", unsubUrl, undefined) + } + }) + test("Unsubscribe from all is hidden if there are no subscriptions", async () => { + setupApis({ + isAuthenticated: true, + isSubscribed: false, + subscriptionRequest: {}, + }) + renderWithProviders() + const unfollowButton = screen.queryByText("Unfollow All") + expect(unfollowButton).not.toBeInTheDocument() + }) }) diff --git a/frontends/main/src/app-pages/DashboardPage/SettingsPage.tsx b/frontends/main/src/app-pages/DashboardPage/SettingsPage.tsx index 2a135a5178..1e201aaf98 100644 --- a/frontends/main/src/app-pages/DashboardPage/SettingsPage.tsx +++ b/frontends/main/src/app-pages/DashboardPage/SettingsPage.tsx @@ -1,11 +1,19 @@ import React from "react" -import { PlainList, Typography, Link, styled } from "ol-components" +import { + PlainList, + Typography, + Link, + styled, + Button, + Dialog, + DialogActions, +} from "ol-components" import { useUserMe } from "api/hooks/user" import { useSearchSubscriptionDelete, useSearchSubscriptionList, } from "api/hooks/searchSubscription" - +import * as NiceModal from "@ebay/nice-modal-react" const SOURCE_LABEL_DISPLAY = { topic: "Topic", unit: "MIT Unit", @@ -13,6 +21,10 @@ const SOURCE_LABEL_DISPLAY = { saved_search: "Saved Search", } +const Actions = styled(DialogActions)({ + display: "flex", + "> *": { flex: 1 }, +}) const FollowList = styled(PlainList)(({ theme }) => ({ borderRadius: "8px", background: theme.custom.colors.white, @@ -37,6 +49,29 @@ const SubTitleText = styled(Typography)(({ theme }) => ({ ...theme.typography.body2, })) +const SettingsHeader = styled.div(({ theme }) => ({ + display: "flex", + alignItems: "center", + alignSelf: "stretch", + [theme.breakpoints.down("md")]: { + paddingBottom: "8px", + }, +})) + +const SettingsHeaderLeft = styled.div({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + flex: "1 0 0", +}) + +const SettingsHeaderRight = styled.div(({ theme }) => ({ + display: "flex", + [theme.breakpoints.down("md")]: { + display: "none", + }, +})) + const ListItem = styled.li(({ theme }) => [ { padding: "16px 32px", @@ -83,22 +118,100 @@ const ListItemBody: React.FC = ({ ) } +type UnfollowDialogProps = { + subscriptionIds?: number[] + subscriptionName?: string +} +const UnfollowDialog = NiceModal.create( + ({ subscriptionIds, subscriptionName }: UnfollowDialogProps) => { + const modal = NiceModal.useModal() + const subscriptionDelete = useSearchSubscriptionDelete() + const unsubscribe = subscriptionDelete.mutate + return ( + + + + + + } + > + {subscriptionIds?.length === 1 ? ( + <> + Are you sure you want to unfollow {subscriptionName}? + + ) : ( + <> + Are you sure you want to Unfollow All? You will stop getting + emails for all topics, academic departments, and MIT units you are + following. + + )} + + ) + }, +) + const SettingsPage: React.FC = () => { const { data: user } = useUserMe() - const subscriptionDelete = useSearchSubscriptionDelete() + const subscriptionList = useSearchSubscriptionList({ enabled: !!user?.is_authenticated, }) - const unsubscribe = subscriptionDelete.mutate if (!user || subscriptionList.isLoading) return null return ( <> - Following - - All topics, academic departments, and MIT units you are following. - + + + Following + + All topics, academic departments, and MIT units you are following. + + + {subscriptionList?.data && subscriptionList?.data?.length > 1 ? ( + + + + ) : ( + <> + )} + {subscriptionList?.data?.map((subscriptionItem) => ( @@ -111,10 +224,13 @@ const SettingsPage: React.FC = () => { } /> { - event.preventDefault() - unsubscribe(subscriptionItem.id) - }} + onClick={() => + NiceModal.show(UnfollowDialog, { + subscriptionIds: [subscriptionItem.id], + subscriptionName: subscriptionItem.source_description, + id: subscriptionItem.id.toString(), + }) + } > Unfollow diff --git a/frontends/main/src/app-pages/EcommercePages/CartPage/CartPage.test.tsx b/frontends/main/src/app-pages/EcommercePages/CartPage/CartPage.test.tsx new file mode 100644 index 0000000000..5cf414fc23 --- /dev/null +++ b/frontends/main/src/app-pages/EcommercePages/CartPage/CartPage.test.tsx @@ -0,0 +1,54 @@ +import React from "react" +import { renderWithProviders, setMockResponse } from "@/test-utils" +import { urls } from "api/test-utils" +import * as commonUrls from "@/common/urls" +import { ForbiddenError, Permissions } from "@/common/permissions" +import { useFeatureFlagEnabled } from "posthog-js/react" +import CartPage from "./CartPage" +import { allowConsoleErrors } from "ol-test-utilities" + +jest.mock("posthog-js/react") +const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled) + +const oldWindowLocation = window.location + +beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (window as any).location + + window.location = Object.defineProperties({} as Location, { + ...Object.getOwnPropertyDescriptors(oldWindowLocation), + assign: { + configurable: true, + value: jest.fn(), + }, + }) +}) + +afterAll(() => { + window.location = oldWindowLocation +}) + +describe("CartPage", () => { + ;["on", "off"].forEach((testCase: string) => { + test(`Renders when logged in and feature flag is ${testCase}`, async () => { + setMockResponse.get(urls.userMe.get(), { + [Permissions.Authenticated]: true, + }) + mockedUseFeatureFlagEnabled.mockReturnValue(testCase === "on") + + if (testCase === "off") { + allowConsoleErrors() + expect(() => + renderWithProviders(, { + url: commonUrls.ECOMMERCE_CART, + }), + ).toThrow(ForbiddenError) + } else { + renderWithProviders(, { + url: commonUrls.ECOMMERCE_CART, + }) + } + }) + }) +}) diff --git a/frontends/main/src/app-pages/EcommercePages/CartPage/CartPage.tsx b/frontends/main/src/app-pages/EcommercePages/CartPage/CartPage.tsx new file mode 100644 index 0000000000..00f56199e6 --- /dev/null +++ b/frontends/main/src/app-pages/EcommercePages/CartPage/CartPage.tsx @@ -0,0 +1,30 @@ +"use client" +import React from "react" +import { Breadcrumbs, Container, Typography } from "ol-components" +import EcommerceFeature from "@/page-components/EcommerceFeature/EcommerceFeature" +import * as urls from "@/common/urls" + +const CartPage: React.FC = () => { + return ( + + + + + + Shopping Cart + + + + The shopping cart layout should go here, if you're allowed to see + this. + + + + ) +} + +export default CartPage diff --git a/frontends/main/src/app-pages/EcommercePages/CartPage/README.md b/frontends/main/src/app-pages/EcommercePages/CartPage/README.md new file mode 100644 index 0000000000..878d3f0887 --- /dev/null +++ b/frontends/main/src/app-pages/EcommercePages/CartPage/README.md @@ -0,0 +1,9 @@ +# Unified Ecommerce in MIT Learn + +The front end for the Unified Ecommerce system lives in MIT Learn. So, pages that exist here are designed to talk to Unified Ecommerce rather than to the Learn system. + +There's a few functional pieces here: + +- **Cart** - Displays the user's cart, and provides some additional functionality for that (item management, discount application, etc.) +- **Receipts** - Allows the user to display their order history and view receipts from their purchases, including historical ones from other systems. +- **Financial Assistance** - For learning resources that support it, the learner side of the financial assistance request system lives here. (Approvals do not.) diff --git a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx index 16186eebaa..c9be69cef5 100644 --- a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx +++ b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx @@ -201,25 +201,24 @@ describe("SearchPage", () => { await within(facetsContainer).findByText("Resource Type") }) - test.each([{ withPage: false }, { withPage: true }])( - "Submitting text updates URL", - async ({ withPage }) => { - setMockApiResponses({}) - const urlQueryString = withPage ? "?q=meow&page=2" : "?q=meow" - const { location } = renderWithProviders(, { - url: urlQueryString, - }) - const queryInput = await screen.findByRole("textbox", { - name: "Search for", - }) - expect(queryInput.value).toBe("meow") - await user.clear(queryInput) - await user.paste("woof") - expect(location.current.search).toBe(urlQueryString) - await user.click(screen.getByRole("button", { name: "Search" })) - expect(location.current.search).toBe("?q=woof") - }, - ) + test.each([ + { initialQuery: "?q=meow&page=2", finalQuery: "?q=woof" }, + { initialQuery: "?q=meow", finalQuery: "?q=woof" }, + ])("Submitting text updates URL", async ({ initialQuery, finalQuery }) => { + setMockApiResponses({}) + const { location } = renderWithProviders(, { + url: initialQuery, + }) + const queryInput = await screen.findByRole("textbox", { + name: "Search for", + }) + expect(queryInput.value).toBe("meow") + await user.clear(queryInput) + await user.paste("woof") + expect(location.current.search).toBe(initialQuery) + await user.click(screen.getByRole("button", { name: "Search" })) + expect(location.current.search).toBe(finalQuery) + }) test("unathenticated users do not see admin options", async () => { setMockApiResponses({ diff --git a/frontends/main/src/app-pages/SearchPage/SearchPage.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.tsx index 11d9e76778..e22c0bbaef 100644 --- a/frontends/main/src/app-pages/SearchPage/SearchPage.tsx +++ b/frontends/main/src/app-pages/SearchPage/SearchPage.tsx @@ -11,17 +11,13 @@ import { getDepartmentName, } from "@mitodl/course-search-utils" import SearchDisplay from "@/page-components/SearchDisplay/SearchDisplay" -import { - SearchInput, - styled, - Container, - theme, - VisuallyHidden, -} from "ol-components" +import { styled, Container, theme, VisuallyHidden } from "ol-components" +import { SearchField } from "@/page-components/SearchField/SearchField" import type { LearningResourceOfferor } from "api" import { useOfferorsList } from "api/hooks/learningResources" import { capitalize } from "ol-utilities" import LearningResourceDrawer from "@/page-components/LearningResourceDrawer/LearningResourceDrawer" +import { usePostHog } from "posthog-js/react" const cssGradient = ` linear-gradient( @@ -57,7 +53,7 @@ const SearchFieldContainer = styled(Container)({ justifyContent: "center", }) -const SearchField = styled(SearchInput)(({ theme }) => ({ +const StyledSearchField = styled(SearchField)(({ theme }) => ({ [theme.breakpoints.down("sm")]: { width: "100%", }, @@ -177,6 +173,7 @@ const useFacetManifest = (resourceCategory: string | null) => { const SearchPage: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams() const facetManifest = useFacetManifest(searchParams.get("resource_category")) + const posthog = usePostHog() const setPage = useCallback( (newPage: number) => { @@ -193,8 +190,11 @@ const SearchPage: React.FC = () => { [setSearchParams], ) const onFacetsChange = useCallback(() => { + if (process.env.NEXT_PUBLIC_POSTHOG_PROJECT_API_KEY) { + posthog.capture("search_update") + } setPage(1) - }, [setPage]) + }, [setPage, posthog]) const { params, @@ -212,14 +212,6 @@ const SearchPage: React.FC = () => { onFacetsChange, }) - const onSearchTermSubmit = useCallback( - (term: string) => { - setCurrentTextAndQuery(term) - setPage(1) - }, - [setPage, setCurrentTextAndQuery], - ) - const page = +(searchParams.get("page") ?? "1") return ( @@ -230,16 +222,17 @@ const SearchPage: React.FC = () => {
- setCurrentText(e.target.value)} onSubmit={(e) => { - onSearchTermSubmit(e.target.value) + setCurrentTextAndQuery(e.target.value) }} onClear={() => { - onSearchTermSubmit("") + setCurrentTextAndQuery("") }} + setPage={setPage} />
diff --git a/frontends/main/src/app-pages/UnitsListingPage/UnitsListingPage.tsx b/frontends/main/src/app-pages/UnitsListingPage/UnitsListingPage.tsx index c622679783..76df4ef234 100644 --- a/frontends/main/src/app-pages/UnitsListingPage/UnitsListingPage.tsx +++ b/frontends/main/src/app-pages/UnitsListingPage/UnitsListingPage.tsx @@ -111,6 +111,9 @@ const UnitContainer = styled.section(({ theme }) => ({ alignItems: "center", maxWidth: DESKTOP_WIDTH, gap: "32px", + ".MitCard-root": { + height: "auto", + }, [theme.breakpoints.down("md")]: { width: "auto", padding: "0 16px", diff --git a/frontends/main/src/app/cart/page.tsx b/frontends/main/src/app/cart/page.tsx new file mode 100644 index 0000000000..19bc90d93c --- /dev/null +++ b/frontends/main/src/app/cart/page.tsx @@ -0,0 +1,14 @@ +import React from "react" +import { Metadata } from "next" +import { standardizeMetadata } from "@/common/metadata" +import CartPage from "@/app-pages/EcommercePages/CartPage/CartPage" + +export const metadata: Metadata = standardizeMetadata({ + title: "Shopping Cart", +}) + +const Page: React.FC = () => { + return +} + +export default Page diff --git a/frontends/main/src/common/feature_flags.ts b/frontends/main/src/common/feature_flags.ts new file mode 100644 index 0000000000..8d6e479d1e --- /dev/null +++ b/frontends/main/src/common/feature_flags.ts @@ -0,0 +1,6 @@ +// Feature flags for the app. These should correspond to the flag that's set up +// in PostHog. + +export enum FeatureFlags { + EnableEcommerce = "enable-ecommerce", +} diff --git a/frontends/main/src/common/urls.ts b/frontends/main/src/common/urls.ts index c1f6c47222..14d3c5e8fe 100644 --- a/frontends/main/src/common/urls.ts +++ b/frontends/main/src/common/urls.ts @@ -145,3 +145,5 @@ export const SEARCH_PROGRAM = querifiedSearchUrl({ export const SEARCH_LEARNING_MATERIAL = querifiedSearchUrl({ resource_category: "learning_material", }) + +export const ECOMMERCE_CART = "/cart/" as const diff --git a/frontends/main/src/components/MITLogoLink/MITLogoLink.tsx b/frontends/main/src/components/MITLogoLink/MITLogoLink.tsx index 58e256f1d6..0ca1ce6e23 100644 --- a/frontends/main/src/components/MITLogoLink/MITLogoLink.tsx +++ b/frontends/main/src/components/MITLogoLink/MITLogoLink.tsx @@ -1,30 +1,71 @@ import React from "react" import Image from "next/image" import Link from "next/link" -import defaultLogo from "@/public/mit-logo-learn.svg" +import mitLogoBlack from "@/public/images/mit-logo-black.svg" +import whiteLogoWhite from "@/public/images/mit-logo-white.svg" +import learnLogo from "@/public/images/mit-learn-logo.svg" +import { styled } from "ol-components" interface Props { - href?: string + logo: "mit_white" | "mit_black" | "learn" className?: string - logo?: string - alt?: string } -const MITLogoLink: React.FC = ({ - href, - logo, - alt = "MIT Learn Logo", - className, -}) => ( - - {alt} - -) +const StyledImage = styled(Image)({ + /** + * Needs display: block because otherwise the image is inline, which complicates + * parent container height calculations. + * + * See https://stackoverflow.com/a/11126701/2747370 + */ + display: "block", + width: "auto", +}) +const linkProps = { + learn: { + href: "/", + title: "MIT Learn Homepage", + }, + mit_black: { + href: "https://mit.edu/", + title: "MIT Homepage", + target: "_blank", + }, + mit_white: { + href: "https://mit.edu/", + title: "MIT Homepage", + target: "_blank", + }, +} +const imageProps = { + learn: { + src: learnLogo, + }, + mit_black: { + src: mitLogoBlack, + }, + mit_white: { + src: whiteLogoWhite, + }, +} + +/** + * Used for MIT logo variations. + * + * To size the logo, specify at least the `height` of `img` via the parent's className. + */ +const MITLogoLink: React.FC = ({ logo, className }) => { + return ( + + + + ) +} export default MITLogoLink diff --git a/frontends/main/src/page-components/Dialogs/AddToListDialog.tsx b/frontends/main/src/page-components/Dialogs/AddToListDialog.tsx index d13b78761e..0c65b23c38 100644 --- a/frontends/main/src/page-components/Dialogs/AddToListDialog.tsx +++ b/frontends/main/src/page-components/Dialogs/AddToListDialog.tsx @@ -10,6 +10,7 @@ import { } from "ol-components" import { RiAddLine } from "@remixicon/react" +import { usePostHog } from "posthog-js/react" import NiceModal, { muiDialogV5 } from "@ebay/nice-modal-react" @@ -67,6 +68,7 @@ const AddToListDialogInner: React.FC = ({ isLoading: isSavingLearningPathRelationships, mutateAsync: setLearningPathRelationships, } = useLearningResourceSetLearningPathRelationships() + const posthog = usePostHog() const isSaving = isSavingLearningPathRelationships || isSavingUserListRelationships let dialogTitle = "Add to list" @@ -93,6 +95,7 @@ const AddToListDialogInner: React.FC = ({ : null, ) .filter((value) => value !== null) + const formik = useFormik({ enableReinitialize: true, validateOnChange: false, @@ -103,6 +106,15 @@ const AddToListDialogInner: React.FC = ({ }, onSubmit: async (values) => { if (resource) { + if (process.env.NEXT_PUBLIC_POSTHOG_PROJECT_API_KEY) { + posthog.capture("lr_add_to_list", { + listType: listType, + resourceId: resource?.id, + readableId: resource?.readable_id, + platformCode: resource?.platform?.code, + resourceType: resource?.resource_type, + }) + } if (listType === ListType.LearningPath) { const newParents = values.learning_paths.map((id) => parseInt(id)) await setLearningPathRelationships({ diff --git a/frontends/main/src/page-components/EcommerceFeature/EcommerceFeature.tsx b/frontends/main/src/page-components/EcommerceFeature/EcommerceFeature.tsx new file mode 100644 index 0000000000..0cbb0a2edf --- /dev/null +++ b/frontends/main/src/page-components/EcommerceFeature/EcommerceFeature.tsx @@ -0,0 +1,33 @@ +import React from "react" +import { useFeatureFlagEnabled } from "posthog-js/react" +import { ForbiddenError } from "@/common/permissions" +import { FeatureFlags } from "@/common/feature_flags" + +type EcommerceFeatureProps = { + children: React.ReactNode +} + +/** + * Simple wrapper to standardize the feature flag check for ecommerce UI pages. + * If the flag is enabled, display the children; if not, throw a ForbiddenError + * like you'd get for an unauthenticated route. + * + * There's a PostHogFeature component that is provided but went this route + * because it seemed to be inconsistent - sometimes having the flag enabled + * resulted in it tossing to the error page. + * + * Set the feature flag here using the enum, and then make sure it's also + * defined in commmon/feature_flags too. + */ + +const EcommerceFeature: React.FC = ({ children }) => { + const ecommFlag = useFeatureFlagEnabled(FeatureFlags.EnableEcommerce) + + if (ecommFlag === false) { + throw new ForbiddenError("Not enabled.") + } + + return ecommFlag ? children : null +} + +export default EcommerceFeature diff --git a/frontends/main/src/page-components/Footer/Footer.test.tsx b/frontends/main/src/page-components/Footer/Footer.test.tsx index 09f2b25324..82c2391429 100644 --- a/frontends/main/src/page-components/Footer/Footer.test.tsx +++ b/frontends/main/src/page-components/Footer/Footer.test.tsx @@ -6,9 +6,11 @@ import * as urls from "@/common/urls" describe("Footer", () => { test("Renders the appropriate text and links", async () => { - render(