Skip to content

Commit

Permalink
chore: convert all 'search-modal' code to TypeScript (openedx#1129)
Browse files Browse the repository at this point in the history
* chore: convert all 'search-modal' code to TypeScript

* fix: lint should check .ts[x] files too

* fix: remove unused dependency meilisearch-instantsearch
  • Loading branch information
bradenmacdonald authored Jun 27, 2024
1 parent 22ea32c commit a4859d2
Show file tree
Hide file tree
Showing 22 changed files with 261 additions and 297 deletions.
9 changes: 0 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"stylelint": "stylelint \"plugins/**/*.scss\" \"src/**/*.scss\" \"scss/**/*.scss\" --config .stylelintrc.json",
"lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx .",
"lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
"lint:fix": "npm run stylelint -- --fix && fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "TZ=UTC fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
Expand Down Expand Up @@ -54,7 +54,6 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@meilisearch/instant-meilisearch": "^0.17.0",
"@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator",
"@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes",
"@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant",
Expand Down
37 changes: 17 additions & 20 deletions src/content-tags-drawer/ContentTagsCollapsible.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ import type {} from 'react-select/base';
// and add our custom property 'myCustomProp' to it.

export interface TagTreeEntry {
explicit: boolean;
children: Record<string, TagTreeEntry>;
canChangeObjecttag: boolean;
canDeleteObjecttag: boolean;
explicit: boolean;
children: Record<string, TagTreeEntry>;
canChangeObjecttag: boolean;
canDeleteObjecttag: boolean;
}

export interface TaxonomySelectProps {
taxonomyId: number;
searchTerm: string;
appliedContentTagsTree: Record<string, TagTreeEntry>;
stagedContentTagsTree: Record<string, TagTreeEntry>;
checkedTags: string[];
selectCancelRef: Ref,
selectAddRef: Ref,
selectInlineAddRef: Ref,
handleCommitStagedTags: () => void;
handleCancelStagedTags: () => void;
handleSelectableBoxChange: React.ChangeEventHandler;
taxonomyId: number;
searchTerm: string;
appliedContentTagsTree: Record<string, TagTreeEntry>;
stagedContentTagsTree: Record<string, TagTreeEntry>;
checkedTags: string[];
selectCancelRef: Ref,
selectAddRef: Ref,
selectInlineAddRef: Ref,
handleCommitStagedTags: () => void;
handleCancelStagedTags: () => void;
handleSelectableBoxChange: React.ChangeEventHandler;
}

// Unfortunately the only way to specify the custom props we pass into React Select
Expand All @@ -32,11 +32,8 @@ export interface TaxonomySelectProps {
// we should change to using a 'react context' to share this data within <ContentTagsCollapsible>,
// rather than using the custom <Select> Props (selectProps).
declare module 'react-select/base' {
export interface Props<
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>
> extends TaxonomySelectProps {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface Props<Option, IsMulti extends boolean, Group extends GroupBase<Option>> extends TaxonomySelectProps {
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';

/**
* Displays a friendly, localized text name for the given XBlock/component type
* e.g. `vertical` becomes `"Unit"`
* @type {React.FC<{type: string}>}
*/
const BlockTypeLabel = ({ type }) => {
const BlockTypeLabel: React.FC<{ type: string }> = ({ type }) => {
// TODO: Load the localized list of Component names from Studio REST API?
const msg = messages[`blockType.${type}`];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
Expand All @@ -8,9 +6,8 @@ import { useSearchContext } from './manager/SearchManager';

/**
* A button that appears when at least one filter is active, and will clear the filters when clicked.
* @type {React.FC<Record<never, never>>}
*/
const ClearFiltersButton = () => {
const ClearFiltersButton: React.FC<Record<never, never>> = () => {
const { canClearFilters, clearFilters } = useSearchContext();
if (canClearFilters) {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import type { MessageDescriptor } from 'react-intl';
import { Alert, Stack } from '@openedx/paragon';

import { useSearchContext } from './manager/SearchManager';
import EmptySearchImage from './images/empty-search.svg';
import NoResultImage from './images/no-results.svg';
import messages from './messages';

const InfoMessage = ({ title, subtitle, image }) => (
interface InfoMessageProps {
title: MessageDescriptor;
subtitle: MessageDescriptor;
image: string;
}

const InfoMessage = (props: InfoMessageProps) => (
<Stack className="d-flex mt-6 align-items-center">
<p className="lead"> <FormattedMessage {...title} /> </p>
<p className="small text-muted"> <FormattedMessage {...subtitle} /> </p>
<img src={image} alt="" />
<p className="lead"> <FormattedMessage {...props.title} /> </p>
<p className="small text-muted"> <FormattedMessage {...props.subtitle} /> </p>
<img src={props.image} alt="" />
</Stack>
);

/**
* If the user hasn't put any keywords/filters yet, display an "empty state".
* Likewise, if the results are empty (0 results), display a special message.
* Otherwise, display the results, which are assumed to be the children prop.
* @type {React.FC<{children: React.ReactElement}>}
*/
const EmptyStates = ({ children }) => {
const EmptyStates: React.FC<{ children: React.ReactElement }> = ({ children }) => {
const {
canClearFilters: hasFiltersApplied,
totalHits,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Expand All @@ -17,9 +15,8 @@ import { useSearchContext } from './manager/SearchManager';
* A button with a dropdown that allows filtering the current search by component type (XBlock type)
* e.g. Limit results to "Text" (html) and "Problem" (problem) components.
* The button displays the first type selected, and a count of how many other types are selected, if more than one.
* @type {React.FC<Record<never, never>>}
*/
const FilterByBlockType = () => {
const FilterByBlockType: React.FC<Record<never, never>> = () => {
const {
blockTypes,
blockTypesFilter,
Expand All @@ -35,17 +32,17 @@ const FilterByBlockType = () => {
};

// If both blocktypes are in the order dictionary, sort them based on the order defined
if (order[a] && order[b]) {
if (a in order && b in order) {
return order[a] - order[b];
}

// If only blocktype 'a' is in the order dictionary, place it before 'b'
if (order[a]) {
if (a in order) {
return -1;
}

// If only blocktype 'b' is in the order dictionary, place it before 'a'
if (order[b]) {
if (b in order) {
return 1;
}

Expand All @@ -54,7 +51,7 @@ const FilterByBlockType = () => {
});

// Rebuild sorted blocktypes dictionary
const sortedBlockTypes = {};
const sortedBlockTypes: Record<string, number> = {};
sortedBlockTypeKeys.forEach(key => {
sortedBlockTypes[key] = blockTypes[key];
});
Expand Down Expand Up @@ -95,7 +92,7 @@ const FilterByBlockType = () => {
}
{
// Show a message if there are no options at all to avoid the impression that the dropdown isn't working
sortedBlockTypes.length === 0 ? (
Object.keys(sortedBlockTypes).length === 0 ? (
<MenuItem disabled><FormattedMessage {...messages['blockTypeFilter.empty']} /></MenuItem>
) : null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable react/prop-types */
// @ts-check
/* eslint-disable react/require-default-props */
import React from 'react';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import {
Expand All @@ -21,18 +20,17 @@ import { TAG_SEP } from './data/api';

/**
* A menu item with a checkbox and an optional ▼ button (to show/hide children)
* @type {React.FC<{
* label: string;
* tagPath: string;
* isChecked: boolean;
* onClickCheckbox: () => void;
* tagCount: number;
* hasChildren?: boolean;
* isExpanded?: boolean;
* onToggleChildren?: (tagPath: string) => void;
* }>}
*/
const TagMenuItem = ({
const TagMenuItem: React.FC<{
label: string;
tagPath: string;
isChecked: boolean;
onClickCheckbox: () => void;
tagCount: number;
hasChildren?: boolean;
isExpanded?: boolean;
onToggleChildren?: (tagPath: string) => void;
}> = ({
label,
tagPath,
tagCount,
Expand Down Expand Up @@ -83,14 +81,13 @@ const TagMenuItem = ({

/**
* A list of menu items with all of the options for tags at one level of the hierarchy.
* @type {React.FC<{
* tagSearchKeywords: string;
* parentTagPath?: string;
* toggleTagChildren?: (tagPath: string) => void;
* expandedTags: string[],
* }>}
*/
const TagOptions = ({
const TagOptions: React.FC<{
tagSearchKeywords: string;
parentTagPath?: string;
toggleTagChildren?: (tagPath: string) => void;
expandedTags: string[],
}> = ({
parentTagPath = '',
tagSearchKeywords,
expandedTags,
Expand Down Expand Up @@ -164,15 +161,14 @@ const TagOptions = ({
);
};

/** @type {React.FC} */
const FilterByTags = () => {
const FilterByTags: React.FC<Record<never, never>> = () => {
const intl = useIntl();
const { tagsFilter } = useSearchContext();
const [tagSearchKeywords, setTagSearchKeywords] = React.useState('');

// e.g. {"Location", "Location > North America"} if those two paths of the tag tree are expanded
const [expandedTags, setExpandedTags] = React.useState(/** @type {string[]} */([]));
const toggleTagChildren = React.useCallback(tagWithLineage => {
const [expandedTags, setExpandedTags] = React.useState<string[]>([]);
const toggleTagChildren = React.useCallback((tagWithLineage: string) => {
setExpandedTags(currentList => {
if (currentList.includes(tagWithLineage)) {
return currentList.filter(x => x !== tagWithLineage);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
/* eslint-disable react/no-array-index-key */
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';

import { highlightPostTag, highlightPreTag } from './data/api';

/**
* Render some text that contains matching words which should be highlighted
* @type {React.FC<{text: string}>}
*/
const Highlight = ({ text }) => {
const Highlight: React.FC<{ text: string }> = ({ text }) => {
const parts = text.split(highlightPreTag);
return (
<span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable react/prop-types */
// @ts-check
import React from 'react';
import { ArrowDropDown } from '@openedx/paragon/icons';
import {
Expand All @@ -19,10 +17,12 @@ import {
*
* When clicked, the button will display a dropdown menu containing this
* element's `children`. So use this to wrap a <RefinementList> etc.
*
* @type {React.FC<{appliedFilters: {label: React.ReactNode}[], label: React.ReactNode, children: React.ReactNode}>}
*/
const SearchFilterWidget = ({ appliedFilters, ...props }) => {
const SearchFilterWidget: React.FC<{
appliedFilters: { label: React.ReactNode }[];
label: React.ReactNode;
children: React.ReactNode;
}> = ({ appliedFilters, ...props }) => {
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = React.useState(null);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable react/prop-types */
// @ts-check
/* eslint-disable react/require-default-props */
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { SearchField } from '@openedx/paragon';
Expand All @@ -8,9 +7,8 @@ import { useSearchContext } from './manager/SearchManager';

/**
* The "main" input field where users type in search keywords. The search happens as they type (no need to press enter).
* @type {React.FC<{className?: string}>}
*/
const SearchKeywordsField = (props) => {
const SearchKeywordsField: React.FC<{ className?: string }> = (props) => {
const intl = useIntl();
const { searchKeywords, setSearchKeywords } = useSearchContext();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
import type { Store } from 'redux';
import MockAdapter from 'axios-mock-adapter';

import initializeStore from '../store';
import SearchModal from './SearchModal';
import { getContentSearchConfigUrl } from './data/api';

let store;
let axiosMock;
let store: Store;
let axiosMock: MockAdapter;

const queryClient = new QueryClient({
defaultOptions: {
Expand All @@ -32,7 +33,7 @@ const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<QueryClientProvider client={queryClient}>
<SearchModal isOpen onClose={() => undefined} />
<SearchModal courseId="" isOpen onClose={() => undefined} />
</QueryClientProvider>
</IntlProvider>
</AppProvider>
Expand Down
Loading

0 comments on commit a4859d2

Please sign in to comment.