Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

X topic search #218

Merged
merged 71 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
e5d3803
first commit
fenglish Nov 12, 2018
f1cba1c
apply existing functions from ft.com topic search bar
fenglish Nov 12, 2018
3678a13
change currentlyFollowingTopics = => followedTopics
fenglish Nov 16, 2018
85ba407
make JSX being its own JSX component
fenglish Nov 16, 2018
ddbdb06
reflect reviews
fenglish Nov 16, 2018
69753c0
use x-follow-button
fenglish Nov 16, 2018
c5702ac
add snapshot tests
fenglish Nov 19, 2018
05cb353
set empty array as followedTopic's default
fenglish Nov 20, 2018
5f074d4
display correct sentence when matchingFollowedTopic is only one
fenglish Nov 20, 2018
2cc7644
use onInput instead of onChange
fenglish Nov 21, 2018
47c745b
Do not lint x-docs build output
fenglish Jan 14, 2019
f941991
modify styling
fenglish Jan 15, 2019
2ed3703
delete unnecessary classes
fenglish Jan 15, 2019
69ec345
update x-follow-button to the latest version
fenglish Jan 15, 2019
b81fcd3
update x-follow-button to v0.0.8
fenglish Jan 16, 2019
8f201df
update x-follow-button
fenglish Jan 16, 2019
37c839f
update snapshots
fenglish Jan 16, 2019
3051a7a
pass csrfToken for x-follow-button
fenglish Jan 16, 2019
33aa09a
Add topicFollowed/topicUnfollowed externally triggered actions (#232)
fenglish Jan 22, 2019
bfc262e
Remove followedTopic ids from the api query
fenglish Jan 22, 2019
b3ac018
Separate api response into followed topics and not followed topics
fenglish Jan 22, 2019
d228268
Change topic.name => topic.prefLabel to display topic's name
fenglish Jan 22, 2019
2916c3b
remove unused variable
fenglish Jan 22, 2019
3c9839f
Move the logic of deciding which message is displayed into TopicSearc…
fenglish Jan 22, 2019
b5f9b04
Treat no followed topics are passed
fenglish Jan 22, 2019
d6c62c2
Simplify the logic of separating suggestions into followedTopics and …
fenglish Jan 23, 2019
528fed3
Move the process of applying suggestion url
fenglish Jan 23, 2019
26cc7b1
Change from followedTopics to followedTopicIds to make interaction si…
fenglish Jan 23, 2019
0230e90
set more specific key for all-followed topic list
fenglish Jan 23, 2019
d97fbba
tidy up
fenglish Jan 23, 2019
2e88e97
modify test from two all followed topics to three all followed topics
fenglish Jan 23, 2019
a6f59b4
modify followedTopicIds explanation in Readme
fenglish Jan 23, 2019
4e4d2ff
tidy up
fenglish Jan 23, 2019
d0df75b
Remove onBlur which hides result
fenglish Jan 23, 2019
b35fed9
Disable input box's autocomplete (#237)
fenglish Jan 24, 2019
1f771ef
Pass followedTopicIds into SuggestionList to update FollowButton
fenglish Jan 23, 2019
7619cbe
Apply suggestion mapping directly
fenglish Jan 24, 2019
5bb4eed
Move the decision of displaying result to one place
fenglish Jan 24, 2019
7ff55ad
Make the condition to display result more readable
fenglish Jan 24, 2019
43ae068
update x-follow-button to 0.0.11
fenglish Jan 24, 2019
1617c23
Remove unnecessary data-component="topic-search"
fenglish Jan 29, 2019
4f18169
Apply custom styles to search cancel button (#242)
fenglish Jan 29, 2019
a3e8cab
Change testing style from snapshots tests to unit tests (#228)
fenglish Jan 30, 2019
7e5df9f
Extend Component class instead of using x-interaction
dan-searle Jan 24, 2019
07e0467
followedTopicIds prop is expected to be updated when followed topics …
dan-searle Jan 28, 2019
ba56f39
Create story knob for followedTopicIds prop.
dan-searle Jan 30, 2019
9e1f5a6
Minor improvement to arrayToSentence function.
dan-searle Jan 30, 2019
31126f5
Ensure results are only displayed if they are for the current search …
dan-searle Jan 30, 2019
014d87b
Simplify get-suggestions response processing.
dan-searle Jan 31, 2019
447dea8
Slightly more descriptive tests.
dan-searle Jan 31, 2019
97be6f1
Remove redundant throw in catch.
dan-searle Jan 31, 2019
b5996bb
Bug fix: csrfToken is a prop not state...
dan-searle Jan 31, 2019
45181e5
Extract arrayToSentence function to be a component.
dan-searle Feb 1, 2019
85d16af
Don't show any search results if the searchTerm length is less than t…
dan-searle Feb 1, 2019
9adec50
Remove "you already follow..." messaging and just display any suggest…
dan-searle Feb 1, 2019
1c3edc0
Overwrite top/bottom padding 0 to keep input height the same across b…
fenglish Feb 1, 2019
cc8ddb4
Remove o-forms
fenglish Feb 4, 2019
396d0db
Select the text when input box is changed from blur to focus
fenglish Feb 5, 2019
55f0a05
Delete outdated explanation
fenglish Feb 6, 2019
2a4a01d
Delete spaces
fenglish Feb 6, 2019
93d1606
Set more specific key instead of map index
fenglish Feb 6, 2019
0ba9879
Update README
fenglish Feb 6, 2019
9a1297a
Correct left alignment of no-suggestions bullet list.
dan-searle Feb 8, 2019
4f40d01
Unlock x-follow-button version to include compatible versions
rowanbeentje Mar 7, 2019
13dcebc
Bump x-follow-button dependency of x-topic-search to ^0.0.12
rowanbeentje Mar 7, 2019
b0149e1
AT-2015: Use a render prop for the follow button to make it work with…
Mar 11, 2019
a0f64a5
AT-2015: Added prop docs
Mar 12, 2019
0d6ba09
Miggrate x-topic-search Storybook setup to v6 format
rowanbeentje Nov 24, 2021
0fb6614
Update x-topic-search bower dependencies to majoor versions used acro…
rowanbeentje Nov 24, 2021
9b96d24
Update x-topic-search to use the merged component version of x-follow…
rowanbeentje Nov 24, 2021
8c7f58c
Update x-topic-search test setup so that fetchMock is set up in the b…
rowanbeentje Nov 24, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
**/public-prod/**
**/blueprints/**
web/static/**
/e2e/**
/e2e/**
8 changes: 8 additions & 0 deletions components/x-topic-search/.bowerrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"registry": {
"search": [
"https://origami-bower-registry.ft.com",
"https://registry.bower.io"
]
}
}
3 changes: 3 additions & 0 deletions components/x-topic-search/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
src/
stories/
rollup.js
128 changes: 128 additions & 0 deletions components/x-topic-search/__tests__/x-topic-search.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const fetchMock = require('fetch-mock')
const { h } = require('@financial-times/x-engine')
const { mount } = require('@financial-times/x-test-utils/enzyme')
const { TopicSearch } = require('../')

const minSearchLength = 3
const maxSuggestions = 3
const apiUrl = 'api-url'
const FOLLOWED_TOPIC_ID1 = 'Cat-House-id'
const FOLLOWED_TOPIC_ID2 = 'Cat-Food-id'
const UNFOLLOWED_TOPIC_ID1 = 'Cat-Toys-id'

describe('x-topic-search', () => {
const buildSearchUrl = (term) => `${apiUrl}?count=${maxSuggestions}&partial=${term}`
const enterSearchTerm = (searchTerm) => {
target.find('input').simulate('input', { target: { value: searchTerm } })

return new Promise((resolve) => {
setTimeout(resolve, 400)
})
}
let target

beforeEach(() => {
const props = {
minSearchLength,
maxSuggestions,
apiUrl,
followedTopicIds: [FOLLOWED_TOPIC_ID1, FOLLOWED_TOPIC_ID2]
}
target = mount(<TopicSearch {...props} />)
})

afterEach(() => {
fetchMock.reset()
})

describe('initial rendering', () => {
it('should render with input box', () => {
expect(target.find('input').exists()).toBe(true)
})

it('should not display result container', () => {
expect(target.render().children('div')).toHaveLength(1)
})
})

describe('when input receives focus', () => {
it('selects the text in the input', () => {
const selectMock = jest.fn()
const inputBox = target.find('input')

inputBox.simulate('blur')
inputBox.simulate('focus', { target: { select: selectMock } })

expect(selectMock).toHaveBeenCalledTimes(1)
})
})

describe('given inputted text is shorter than minSearchLength', () => {
const apiUrlWithResults = buildSearchUrl('a')

beforeEach(() => {
fetchMock.get(apiUrlWithResults, [])
return enterSearchTerm('a')
})

it('does not make a request to the api or render any result', () => {
expect(fetchMock.called(apiUrlWithResults)).toBe(false)
expect(target.render().children('div')).toHaveLength(1)
})
})

describe('given searchTerm which has some topic suggestions to follow', () => {
const apiUrlWithResults = buildSearchUrl('Cat')
const results = [
{ id: FOLLOWED_TOPIC_ID1, prefLabel: 'Cat House', url: 'Cat-House-url' },
{ id: FOLLOWED_TOPIC_ID2, prefLabel: 'Cat Food', url: 'Cat-Food-url' },
{ id: UNFOLLOWED_TOPIC_ID1, prefLabel: 'Cat Toys', url: 'Cat-Toys-url' }
]

beforeEach(() => {
fetchMock.get(apiUrlWithResults, results)
return enterSearchTerm('Cat')
})

it('requests the topic suggestions with count set to maxSuggestions', () => {
expect(fetchMock.called(apiUrlWithResults)).toBe(true)
})

it('renders no more than the max number of suggestions', () => {
expect(target.render().children('div')).toHaveLength(2)
expect(target.render().find('li')).toHaveLength(maxSuggestions)
})

it('renders links and follow buttons for each suggestion', () => {
const suggestionsList = target.render().find('li')

results.forEach((topic, index) => {
const suggestion = suggestionsList.eq(index)

expect(suggestion.find('a').text()).toEqual(topic.prefLabel)
expect(suggestion.find('a').attr('href')).toEqual(topic.url)
expect(suggestion.find('button').text()).toEqual(
topic.id === UNFOLLOWED_TOPIC_ID1 ? 'Add to myFT' : 'Added'
)
})
})
})

describe('given searchTerm which has no topic suggestions to follow', () => {
const apiUrlNoResults = buildSearchUrl('Dog')

beforeEach(() => {
fetchMock.get(apiUrlNoResults, [])
return enterSearchTerm('Dog')
})

it('requests from the api and renders the no matching topics message', () => {
expect(fetchMock.called(apiUrlNoResults)).toBe(true)

const resultContainer = target.render().children('div').eq(1)

expect(resultContainer).toHaveLength(1)
expect(resultContainer.find('h2').text()).toMatch('No topics matching')
})
})
})
11 changes: 11 additions & 0 deletions components/x-topic-search/bower.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "x-topic-search",
"main": "dist/TopicSearch.es5.js",
"private": true,
"dependencies": {
"o-icons": "^6.3.0",
"o-typography": "^6.4.6",
"o-editorial-typography": "^1.2.1",
"o-colors": "^5.4.1"
}
}
42 changes: 42 additions & 0 deletions components/x-topic-search/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@financial-times/x-topic-search",
"version": "0.0.0",
"description": "",
"main": "dist/TopicSearch.cjs.js",
"module": "dist/TopicSearch.esm.js",
"browser": "dist/TopicSearch.es5.js",
"style": "dist/TopicSearch.css",
"scripts": {
"prepare": "bower install && npm run build",
"build": "node rollup.js",
"start": "node rollup.js --watch"
},
"keywords": [
"x-dash"
],
"author": "",
"license": "ISC",
"dependencies": {
"@financial-times/x-engine": "file:../../packages/x-engine",
"@financial-times/x-follow-button": "file:../x-follow-button",
"classnames": "^2.2.6",
"debounce-promise": "^3.1.0"
},
"devDependencies": {
"@financial-times/x-rollup": "file:../../packages/x-rollup",
"@financial-times/x-test-utils": "file:../../packages/x-test-utils",
"bower": "^1.7.9",
"node-sass": "^4.9.2"
},
"repository": {
"type": "git",
"url": "https://github.com/Financial-Times/x-dash.git"
},
"homepage": "https://github.com/Financial-Times/x-dash/tree/master/components/x-topic-search",
"engines": {
"node": ">= 6.0.0"
},
"publishConfig": {
"access": "public"
}
}
54 changes: 54 additions & 0 deletions components/x-topic-search/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# x-topic-search

This module allows a user to search for topics by name, and follow them. If an already-followed topic is returned in the search results, then those topics are indicated as such.

The search results are fetched from the api whose url is passed as a property.
[next-myft-page](https://github.com/Financial-Times/next-myft-page/blob/master/client/components/topic-search/TopicSearchContainer.jsx#L9)
uses [next-tag-facets-api](https://github.com/Financial-Times/next-tag-facets-api).


## Installation

This module is compatible with Node 6+ and is distributed on npm.

```bash
npm install --save @financial-times/x-topic-search
```

The [`x-engine`][engine] module is used to inject your chosen runtime into the component. Please read the `x-engine` documentation first if you are consuming `x-` components for the first time in your application.

[engine]: https://github.com/Financial-Times/x-dash/tree/master/packages/x-engine


## Usage

The components provided by this module are all functions that expect a map of [properties](#properties). They can be used with vanilla JavaScript or JSX (If you are not familiar check out [WTF is JSX][jsx-wtf] first). For example if you were writing your application using React you could use the component like this:

```jsx
import React from 'react';
import { TopicSearch } from '@financial-times/x-topic-search';

// A == B == C
const a = TopicSearch(props);
const b = <TopicSearch {...props} />;
const c = React.createElement(TopicSearch, props);
```

All `x-` components are designed to be compatible with a variety of runtimes, not just React. Check out the [`x-engine`][engine] documentation for a list of recommended libraries and frameworks.

[jsx-wtf]: https://jasonformat.com/wtf-is-jsx/


The consumer of this component needs to update `followedTopicIds` every time when users follow or unfollow topics.


### Properties

Property | Type | Required | Note
---------------------|----------|----------|------------------
`minSearchLength` | Number | No | Minimum chars to start search. Default is 2
`maxSuggestions` | Number | No | Maximum number to display suggestions. Default is 5
`apiUrl` | String | Yes | The url to use when making requests to get topics
`followedTopicIds` | Array | Yes | Array of followed topic `id`s.
`csrfToken` | String | Yes | Value included in a hidden form field for x-follow-button
`renderFollowButton` | Function | No | Optional render prop for the follow button
4 changes: 4 additions & 0 deletions components/x-topic-search/rollup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const xRollup = require('@financial-times/x-rollup');
const pkg = require('./package.json');

xRollup({ input: './src/TopicSearch.jsx', pkg });
22 changes: 22 additions & 0 deletions components/x-topic-search/src/NoSuggestions.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { h } from '@financial-times/x-engine';
import styles from './TopicSearch.scss';
import classNames from 'classnames';

export default ({ searchTerm }) => (
<div className={classNames(styles["no-suggestions"])} aria-live="polite">

<h2 className={classNames(styles["no-suggestions__title"])}>
No topics matching <b>{searchTerm}</b>
</h2>

<p>Suggestions:</p>

<ul className={classNames(styles["no-suggestions__message"])}>
<li>Make sure that all words are spelled correctly.</li>
<li>Try different keywords.</li>
<li>Try more general keywords.</li>
<li>Try fewer keywords.</li>
</ul>

</div>
);
40 changes: 40 additions & 0 deletions components/x-topic-search/src/SuggestionList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { h } from '@financial-times/x-engine';
import { FollowButton } from '@financial-times/x-follow-button';
import styles from './TopicSearch.scss';
import classNames from 'classnames';

const defaultFollowButtonRender = (concept, csrfToken, followedTopicIds) => (
<FollowButton
conceptId={concept.id}
conceptName={concept.prefLabel}
csrfToken={csrfToken}
isFollowed={followedTopicIds.includes(concept.id)}
/>
);

export default ({ suggestions, renderFollowButton, searchTerm, csrfToken, followedTopicIds = [] }) => {
renderFollowButton = typeof renderFollowButton === 'function' ? renderFollowButton : defaultFollowButtonRender;

return (
<ul className={classNames(styles["suggestions"])} aria-live="polite">

{suggestions.map(suggestion => (
<li className={classNames(styles["suggestion"])}
key={suggestion.id}
data-trackable="myft-topic"
data-concept-id={suggestion.id}
data-trackable-meta={'{"search-term":"' + searchTerm + '"}'}>

<a data-trackable="topic-link"
className={classNames(styles["suggestion__name"])}
href={suggestion.url || `/stream/${suggestion.id}`}>
{suggestion.prefLabel}
</a>

{renderFollowButton(suggestion, csrfToken, followedTopicIds)}
</li>
))}

</ul>
);
};
Loading