From 148f24289285b3fbba964e1a829eb6db25bb0a8e Mon Sep 17 00:00:00 2001 From: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com> Date: Wed, 30 Mar 2022 10:51:40 -0500 Subject: [PATCH] Replace listings tag component (#16855) * wip * feat: :sparkles: Use MultiSelectAutocomplete for Tags * feat: :sparkles: Add additional tags for listings * refactor: :recycle: Use preact/hooks and refactor propTypes * fix: :bug: Fix addiional tags support depending on the selected category - Add slug to returned array in select_options_for_categories helper - Differentiate the fields categoryId and categorySlug * fix: :bug: Adjust propTypes * test: :rotating_light: Change test for select_options_for_categories helper * refactor: :recycle: Apply requested changes * refactor: :recycle: Remove border prop - default is true * test: :rotating_light: Add test suite for ListingTagsField component * refactor: :recycle: Apply requested changes * refactor: :recycle: Remove unnecessary forEach + Improve code legibility * docs: :books: Remove topTags from hook documentation * refactor: :recycle: Use the alias for crayons folder + Use loclaCompare in sort * test: :rotating_light: Clean up mock response * fix: :bug: Reorder styles * style: :art: Add comment to TagAutocompleteOption/Selection components --- .../stylesheets/components/autocomplete.scss | 69 +++ .../stylesheets/views/article-form.scss | 66 --- app/helpers/listing_helper.rb | 4 +- .../article-form/components/TagsField.jsx | 50 +- .../TagAutocompleteOption.stories.jsx | 2 +- .../TagAutocompleteOption.jsx | 8 +- .../TagAutocompleteSelection.jsx | 4 +- app/javascript/hooks/useTagsField.js | 67 +++ .../__tests__/ListingTagsField.test.jsx | 118 ++++ .../listings/components/Categories.jsx | 16 +- .../listings/components/ListingTagsField.jsx | 112 ++++ app/javascript/listings/listingForm.jsx | 33 +- .../shared/components/__tests__/tags.test.jsx | 53 -- app/javascript/shared/components/tags.jsx | 502 ------------------ app/views/listings/_form.html.erb | 2 +- spec/helpers/listing_helper_spec.rb | 4 +- 16 files changed, 413 insertions(+), 697 deletions(-) rename app/javascript/{article-form/components => crayons/MultiSelectAutocomplete}/TagAutocompleteOption.jsx (80%) rename app/javascript/{article-form/components => crayons/MultiSelectAutocomplete}/TagAutocompleteSelection.jsx (91%) create mode 100644 app/javascript/hooks/useTagsField.js create mode 100644 app/javascript/listings/__tests__/ListingTagsField.test.jsx create mode 100644 app/javascript/listings/components/ListingTagsField.jsx delete mode 100644 app/javascript/shared/components/__tests__/tags.test.jsx delete mode 100644 app/javascript/shared/components/tags.jsx diff --git a/app/assets/stylesheets/components/autocomplete.scss b/app/assets/stylesheets/components/autocomplete.scss index b4792bfb3..091e5173b 100644 --- a/app/assets/stylesheets/components/autocomplete.scss +++ b/app/assets/stylesheets/components/autocomplete.scss @@ -122,3 +122,72 @@ } } } + +/*********************************************** +** TagAutocompleteOption/Selection styles *** +***********************************************/ +.c-autocomplete--multi { + &__top-tags-heading { + padding: var(--su-3); + font-size: var(--fs-l); + border-bottom: 1px solid var(--base-20); + } + + &__tag-selection { + .c-btn { + color: var(--base-80); + background-color: var(--tag-bg); + } + + .c-btn:hover:enabled, + .c-btn:focus-visible:enabled { + color: var(--base-80); + background-color: var(--tag-bg-hover); + } + } + + &__tag-prefix { + color: var(--tag-prefix); + } + + &__tag-option { + background: var(--card-bg); + color: var(--card-color); + padding: var(--su-3); + border-radius: var(--radius); + } + + &__tag-option-title { + font-weight: var(--fw-medium); + } + + &__tag-option-name { + text-overflow: ellipsis; + } + + &__tag-option:hover { + // We try to avoid !important, but since the --tag-prefix color needs to be set inline, !important is required to temporarily change it + --tag-prefix: var(--accent-brand-darker) !important; + background: var(--body-bg); + + .c-autocomplete--multi__tag-option-title { + color: var(--accent-brand-darker); + } + } + + [aria-selected='true'] { + .c-autocomplete--multi__tag-option { + --tag-prefix: var(--accent-brand-darker) !important; + background: var(--body-bg); + + .c-autocomplete--multi__tag-option-title { + color: var(--accent-brand-darker); + } + } + } + + &__tag-option-image { + height: 1rem; + margin-left: var(--su-1); + } +} diff --git a/app/assets/stylesheets/views/article-form.scss b/app/assets/stylesheets/views/article-form.scss index 251e374db..c1d030619 100644 --- a/app/assets/stylesheets/views/article-form.scss +++ b/app/assets/stylesheets/views/article-form.scss @@ -237,72 +237,6 @@ } } -.crayons-article-form { - &__top-tags-heading { - padding: var(--su-3); - font-size: var(--fs-l); - border-bottom: 1px solid var(--base-20); - } - - &__tag-selection { - .c-btn { - color: var(--base-80); - background-color: var(--tag-bg); - } - - .c-btn:hover:enabled, - .c-btn:focus-visible:enabled { - color: var(--base-80); - background-color: var(--tag-bg-hover); - } - } - - &__tag-prefix { - color: var(--tag-prefix); - } - - &__tag-option { - background: var(--card-bg); - color: var(--card-color); - padding: var(--su-3); - border-radius: var(--radius); - } - - &__tag-option-title { - font-weight: var(--fw-medium); - } - - &__tag-option-name { - text-overflow: ellipsis; - } - - &__tag-option:hover { - // We try to avoid !important, but since the --tag-prefix color needs to be set inline, !important is required to temporarily change it - --tag-prefix: var(--accent-brand-darker) !important; - background: var(--body-bg); - - .crayons-article-form__tag-option-title { - color: var(--accent-brand-darker); - } - } - - [aria-selected='true'] { - .crayons-article-form__tag-option { - --tag-prefix: var(--accent-brand-darker) !important; - background: var(--body-bg); - - .crayons-article-form__tag-option-title { - color: var(--accent-brand-darker); - } - } - } - - &__tag-option-image { - height: 1rem; - margin-left: var(--su-1); - } -} - .js-focus-visible .crayons-article-form__tag-selection .c-btn.focus-visible:focus { diff --git a/app/helpers/listing_helper.rb b/app/helpers/listing_helper.rb index 920bb5872..762dff13e 100644 --- a/app/helpers/listing_helper.rb +++ b/app/helpers/listing_helper.rb @@ -1,7 +1,7 @@ module ListingHelper def select_options_for_categories - ListingCategory.select(:id, :name, :cost).map do |cl| - [I18n.t("helpers.listing_helper.option", cl_name: cl.name, count: cl.cost), cl.id] + ListingCategory.select(:id, :slug, :name, :cost).map do |cl| + [I18n.t("helpers.listing_helper.option", cl_name: cl.name, count: cl.cost), cl.slug, cl.id] end end diff --git a/app/javascript/article-form/components/TagsField.jsx b/app/javascript/article-form/components/TagsField.jsx index 9de038775..43f83c0ee 100644 --- a/app/javascript/article-form/components/TagsField.jsx +++ b/app/javascript/article-form/components/TagsField.jsx @@ -1,10 +1,10 @@ import { h } from 'preact'; import { useEffect, useState } from 'preact/hooks'; import PropTypes from 'prop-types'; -import { TagAutocompleteOption } from './TagAutocompleteOption'; -import { TagAutocompleteSelection } from './TagAutocompleteSelection'; +import { useTagsField } from '../../hooks/useTagsField'; +import { TagAutocompleteOption } from '@crayons/MultiSelectAutocomplete/TagAutocompleteOption'; +import { TagAutocompleteSelection } from '@crayons/MultiSelectAutocomplete/TagAutocompleteSelection'; import { MultiSelectAutocomplete } from '@crayons'; -import { fetchSearch } from '@utilities/search'; /** * TagsField for the article form. Allows users to search and select up to 4 tags. @@ -14,9 +14,11 @@ import { fetchSearch } from '@utilities/search'; * @param {Function} switchHelpContext Callback to switch the help context when the field is focused */ export const TagsField = ({ onInput, defaultValue, switchHelpContext }) => { - const [defaultSelections, setDefaultSelections] = useState([]); - const [defaultsLoaded, setDefaultsLoaded] = useState(false); const [topTags, setTopTags] = useState([]); + const { defaultSelections, fetchSuggestions, syncSelections } = useTagsField({ + defaultValue, + onInput, + }); useEffect(() => { fetch('/tags/suggest') @@ -24,49 +26,13 @@ export const TagsField = ({ onInput, defaultValue, switchHelpContext }) => { .then((results) => setTopTags(results)); }, []); - useEffect(() => { - // Previously selected tags are passed as a plain comma separated string - // Fetching further tag data allows us to display a richer UI - // This fetch only happens once on first component load - if (defaultValue && defaultValue !== '' && !defaultsLoaded) { - const tagNames = defaultValue.split(', '); - - const tagRequests = tagNames.map((tagName) => - fetchSearch('tags', { name: tagName }).then(({ result = [] }) => { - const [potentialMatch = {}] = result; - return potentialMatch.name === tagName - ? potentialMatch - : { name: tagName }; - }), - ); - - Promise.all(tagRequests).then((data) => { - setDefaultSelections(data); - }); - } - setDefaultsLoaded(true); - }, [defaultValue, defaultsLoaded]); - - // Converts the array of selected items into a plain string to be saved in the article form - const syncSelections = (selections = []) => { - const selectionsString = selections - .map((selection) => selection.name) - .join(', '); - onInput(selectionsString); - }; - - const fetchSuggestions = (searchTerm) => - fetchSearch('tags', { name: searchTerm }).then( - (response) => response.result, - ); - return ( Top tags +

Top tags

} labelText="Add up to 4 tags" showLabel={false} diff --git a/app/javascript/article-form/components/__stories__/TagAutocompleteOption.stories.jsx b/app/javascript/article-form/components/__stories__/TagAutocompleteOption.stories.jsx index 650f34e36..a897018d4 100644 --- a/app/javascript/article-form/components/__stories__/TagAutocompleteOption.stories.jsx +++ b/app/javascript/article-form/components/__stories__/TagAutocompleteOption.stories.jsx @@ -1,5 +1,5 @@ import { h } from 'preact'; -import { TagAutocompleteOption } from '../TagAutocompleteOption'; +import { TagAutocompleteOption } from '@crayons/MultiSelectAutocomplete/TagAutocompleteOption'; export default { component: TagAutocompleteOption, diff --git a/app/javascript/article-form/components/TagAutocompleteOption.jsx b/app/javascript/crayons/MultiSelectAutocomplete/TagAutocompleteOption.jsx similarity index 80% rename from app/javascript/article-form/components/TagAutocompleteOption.jsx rename to app/javascript/crayons/MultiSelectAutocomplete/TagAutocompleteOption.jsx index 1518672c3..3d03d018d 100644 --- a/app/javascript/article-form/components/TagAutocompleteOption.jsx +++ b/app/javascript/crayons/MultiSelectAutocomplete/TagAutocompleteOption.jsx @@ -19,17 +19,17 @@ export const TagAutocompleteOption = ({ return (
-
+
# - + {name} {badgeUrl ? ( diff --git a/app/javascript/article-form/components/TagAutocompleteSelection.jsx b/app/javascript/crayons/MultiSelectAutocomplete/TagAutocompleteSelection.jsx similarity index 91% rename from app/javascript/article-form/components/TagAutocompleteSelection.jsx rename to app/javascript/crayons/MultiSelectAutocomplete/TagAutocompleteSelection.jsx index 0bc2f6d47..2e4f556d9 100644 --- a/app/javascript/article-form/components/TagAutocompleteSelection.jsx +++ b/app/javascript/crayons/MultiSelectAutocomplete/TagAutocompleteSelection.jsx @@ -28,7 +28,7 @@ export const TagAutocompleteSelection = ({
); diff --git a/app/javascript/shared/components/__tests__/tags.test.jsx b/app/javascript/shared/components/__tests__/tags.test.jsx deleted file mode 100644 index 0929fbebc..000000000 --- a/app/javascript/shared/components/__tests__/tags.test.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import fetch from 'jest-fetch-mock'; -import { h } from 'preact'; -import { render, fireEvent } from '@testing-library/preact'; -import { axe } from 'jest-axe'; -import { Tags } from '../tags'; - -fetch.enableMocks(); - -describe('', () => { - beforeAll(() => { - const environment = document.createElement('meta'); - environment.setAttribute('name', 'environment'); - document.body.appendChild(environment); - fetch.resetMocks(); - window.fetch = fetch; - }); - - it('should have no a11y violations', async () => { - const { container } = render(); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); - - describe('handleKeyDown', () => { - it('does not call preventDefault on used keyCode', () => { - // Only one call is being made to /tags/suggest when the comma key is pressed - // It didn't seem worth it to inspect the whole mock object to ensure the right URL etc. - fetch.mockResponseOnce('[]'); - - const { getByTestId } = render( - , - ); - - Event.prototype.preventDefault = jest.fn(); - - const tests = [ - { key: 'a', code: '65' }, - { key: '1', code: '49' }, - { key: ',', code: '188' }, - { key: 'Enter', code: '13' }, - ]; - - const input = getByTestId('tag-input'); - - tests.forEach((eventPayload) => { - fireEvent.keyDown(input, eventPayload); - }); - - expect(Event.prototype.preventDefault).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/app/javascript/shared/components/tags.jsx b/app/javascript/shared/components/tags.jsx deleted file mode 100644 index 5038dbe3b..000000000 --- a/app/javascript/shared/components/tags.jsx +++ /dev/null @@ -1,502 +0,0 @@ -import { h, Component } from 'preact'; -import PropTypes from 'prop-types'; -import { fetchSearch } from '../../utilities/search'; - -const KEYS = { - UP: 'ArrowUp', - DOWN: 'ArrowDown', - LEFT: 'ArrowLeft', - RIGHT: 'ArrowRight', - TAB: 'Tab', - RETURN: 'Enter', - COMMA: ',', - DELETE: 'Backspace', -}; - -const NAVIGATION_KEYS = [ - KEYS.COMMA, - KEYS.DELETE, - KEYS.LEFT, - KEYS.RIGHT, - KEYS.TAB, -]; - -const LETTERS_NUMBERS = /[a-z0-9]/i; -export const DEFAULT_TAG_FORMAT = '[0-9A-Za-z, ]+'; - -/* TODO: Remove all instances of this.props.listing - and refactor this component to be more generic */ - -export class Tags extends Component { - constructor(props) { - super(props); - const { listing } = props; - - const listingState = listing - ? { - additionalTags: { - jobs: [ - 'remote', - 'remoteoptional', - 'lgbtbenefits', - 'greencard', - 'senior', - 'junior', - 'intermediate', - '401k', - 'fulltime', - 'contract', - 'temp', - ], - forhire: [ - 'remote', - 'remoteoptional', - 'lgbtbenefits', - 'greencard', - 'senior', - 'junior', - 'intermediate', - '401k', - 'fulltime', - 'contract', - 'temp', - ], - forsale: ['laptop', 'desktopcomputer', 'new', 'used'], - events: ['conference', 'meetup'], - collabs: ['paid', 'temp'], - }, - } - : null; - - this.state = { - selectedIndex: -1, - searchResults: [], - additionalTags: [], - cursorIdx: 0, - prevLen: 0, - showingRulesForTag: null, - showingTopTags: false, - ...listingState, - }; - } - - componentDidUpdate() { - // stop cursor jumping if the user goes back to edit previous tags - const { cursorIdx, prevLen } = this.state; - if ( - cursorIdx < this.textArea.value.length && - this.textArea.value.length < prevLen + 1 - ) { - this.textArea.selectionEnd = cursorIdx; - this.textArea.selectionStart = this.textArea.selectionEnd; - } - } - - get selected() { - const { defaultValue } = this.props; - return defaultValue - .split(',') - .map((item) => item !== undefined && item.trim()) - .filter((item) => item.length > 0); - } - - get isTopOfSearchResults() { - const { selectedIndex } = this.state; - return selectedIndex < 0; - } - - get isBottomOfSearchResults() { - const { selectedIndex, searchResults } = this.state; - return selectedIndex >= searchResults.length - 1; - } - - get isSearchResultSelected() { - const { selectedIndex } = this.state; - return selectedIndex > -1; - } - - getCurrentTagAtSelectionIndex = (value, index) => { - let tagIndex = 0; - const tagByCharacterIndex = {}; - - value.split('').forEach((letter, letterIndex) => { - if (letter === ',') { - tagIndex += 1; - } else { - tagByCharacterIndex[letterIndex] = tagIndex; - } - }); - - const tag = value.split(',')[tagByCharacterIndex[index]]; - - if (tag === undefined) { - return ''; - } - return tag.trim(); - }; - - // Given an index of the String value, finds the range between commas. - // This is useful when we want to insert a new tag anywhere in the - // comma separated list of tags. - getRangeBetweenCommas = (value, index) => { - let start = 0; - let end = value.length; - - const toPreviousComma = value - .slice(0, index) - .split('') - .reverse() - .indexOf(','); - const toNextComma = value.slice(index).indexOf(','); - - if (toPreviousComma !== -1) { - start = index - toPreviousComma + 1; - } - - if (toNextComma !== -1) { - end = index + toNextComma; - } - - return [start, end]; - }; - - handleKeyDown = (e) => { - const component = this; - const { searchResults } = this.state; - const { maxTags } = this.props; - if (component.selected.length === maxTags && e.key === KEYS.COMMA) { - e.preventDefault(); - return; - } - - if ( - (e.key === KEYS.DOWN || e.key === KEYS.TAB) && - !this.isBottomOfSearchResults && - searchResults.length > 0 - ) { - e.preventDefault(); - this.moveDownInSearchResults(); - } else if (e.key === KEYS.UP && !this.isTopOfSearchResults) { - e.preventDefault(); - this.moveUpInSearchResults(); - } else if (e.key === KEYS.RETURN && this.isSearchResultSelected) { - e.preventDefault(); - this.insertTag( - component.state.searchResults[component.state.selectedIndex].name, - ); - - setTimeout(() => { - document.getElementById('tag-input').focus(); - }, 10); - } else if (e.key === KEYS.COMMA && !this.isSearchResultSelected) { - this.fetchTopTagSuggestions(); - this.clearSelectedSearchResult(); - } else if (e.key === KEYS.DELETE) { - if ( - component.props.defaultValue[ - component.props.defaultValue.length - 1 - ] === ',' - ) { - this.clearSelectedSearchResult(); - } - } else if ( - !LETTERS_NUMBERS.test(e.key) && - !NAVIGATION_KEYS.includes(e.key) - ) { - e.preventDefault(); - } - }; - - handleRulesClick = (e) => { - e.preventDefault(); - const { showingRulesForTag } = this.state; - if (showingRulesForTag === e.target.dataset.content) { - this.setState({ showingRulesForTag: null }); - } else { - this.setState({ showingRulesForTag: e.target.dataset.content }); - } - }; - - handleTagClick = (e) => { - if (e.target.className === 'articleform__tagsoptionrulesbutton') { - return; - } - - const input = document.getElementById('tag-input'); - input.focus(); - - // the rules container (__tagoptionrow) is the real target of the event, - // by using currentTarget we let the event propagation work - // from the inner rules box as well (__tagrules) - this.insertTag(e.currentTarget.dataset.content); - }; - - handleInput = (e) => { - let { value } = e.target; - // If we start typing immediately after a comma, add a space - // before what we typed. - // e.g. If value = "javascript," and we type a "p", - // the result should be "javascript, p". - if (e.inputType === 'insertText') { - const isTypingAfterComma = - e.target.value[e.target.selectionStart - 2] === ','; - if (isTypingAfterComma) { - value = this.insertSpace(value, e.target.selectionStart - 1); - } - } - - if (e.data === ',') { - value += ' '; - } - - /* eslint-disable-next-line react/destructuring-assignment */ - this.props.onInput(value); - - const query = this.getCurrentTagAtSelectionIndex( - e.target.value, - e.target.selectionStart - 1, - ); - - this.setState({ - selectedIndex: 0, - cursorIdx: e.target.selectionStart, - prevLen: this.textArea.value.length, - }); - return this.search(query); - }; - - handleFocusChange = () => { - const component = this; - setTimeout(() => { - if (document.activeElement.id === 'tag-input') { - return; - } - component.forceUpdate(); - }, 250); - }; - - insertSpace = (value, position) => { - return `${value.slice(0, position)} ${value.slice(position, value.length)}`; - }; - - handleTagEnter = (e) => { - if (e.key === KEYS.RETURN) { - this.handleTagClick(); - } - }; - - insertTag(tag) { - const input = document.getElementById('tag-input'); - const { maxTags } = this.props; - const range = this.getRangeBetweenCommas(input.value, input.selectionStart); - const insertingAtEnd = range[1] === input.value.length; - const maxTagsWillBeReached = this.selected.length === maxTags; - let tagValue = tag; - if (insertingAtEnd && !maxTagsWillBeReached) { - tagValue = `${tagValue}, `; - } - - // Insert new tag between commas if there are any. - const newInput = - input.value.slice(0, range[0]) + - tagValue + - input.value.slice(range[1], input.value.length); - /* eslint-disable-next-line react/destructuring-assignment */ - this.props.onInput(newInput); - this.fetchTopTagSuggestions(); - this.clearSelectedSearchResult(); - } - - fetchTopTagSuggestions() { - this.setState({ showingTopTags: true }); - const { topTags = [] } = this.state; - if (topTags.length > 0) { - return Promise.resolve().then(() => - this.setState({ - searchResults: topTags.filter((t) => !this.selected.includes(t.name)), - }), - ); - } - return window - .fetch('/tags/suggest') - .then((res) => res.json()) - .then((tags) => { - this.setState({ - topTags: tags, - searchResults: tags.filter((t) => !this.selected.includes(t.name)), - }); - }); - } - - search(query) { - if (query === '') { - return this.fetchTopTagSuggestions(); - } - this.setState({ showingTopTags: false }); - - const { listing } = this.props; - - const dataHash = { name: query }; - const responsePromise = fetchSearch('tags', dataHash); - - return responsePromise.then((response) => { - if (listing === true) { - const { additionalTags } = this.state; - const { category } = this.props; - const additionalItems = (additionalTags[category] || []).filter((t) => - t.includes(query), - ); - const resultsArray = response.result; - additionalItems.forEach((t) => { - if (!resultsArray.includes(t)) { - resultsArray.push({ name: t }); - } - }); - } - // updates searchResults array according to what is being typed by user - // allows user to choose a tag when they've typed the partial or whole word - this.setState({ - searchResults: response.result.filter( - (t) => t.name === query || !this.selected.includes(t.name), - ), - }); - }); - } - - moveUpInSearchResults() { - this.setState((prevState) => ({ - selectedIndex: prevState.selectedIndex - 1, - })); - } - - moveDownInSearchResults() { - this.setState((prevState) => ({ - selectedIndex: prevState.selectedIndex + 1, - })); - } - - clearSelectedSearchResult() { - this.setState({ - selectedIndex: -1, - }); - } - - render() { - let searchResultsHTML = ''; - const { searchResults, selectedIndex, showingRulesForTag, showingTopTags } = - this.state; - const { - classPrefix, - defaultValue, - maxTags, - listing, - fieldClassName, - onFocus, - pattern, - } = this.props; - const { activeElement } = document; - const searchResultsRows = searchResults.map((tag, index) => ( -
- {tag.name} - {tag.rules_html && tag.rules_html.length > 0 ? ( - - ) : ( - '' - )} - {!showingTopTags && ( -
- )} -
- )); - if ( - searchResults.length > 0 && - (activeElement.id === 'tag-input' || - activeElement.classList.contains( - 'articleform__tagsoptionrulesbutton', - ) || - activeElement.classList.contains('articleform__tagoptionrow')) - ) { - searchResultsHTML = ( -
- {showingTopTags ? ( -

Top tags

- ) : null} - {searchResultsRows} -
- Some tags have rules and guidelines determined by community - moderators -
-
- ); - } - - return ( -
- {listing && ( - - )} - { - this.textArea = t; - return this.textArea; - }} - className={`${`${fieldClassName} ${classPrefix}`}__tags`} - name="listing[tag_list]" - placeholder={`Add up to ${maxTags} tags...`} - autoComplete="off" - value={defaultValue} - onInput={this.handleInput} - onKeyDown={this.handleKeyDown} - onBlur={this.handleFocusChange} - onFocus={(e) => { - this.fetchTopTagSuggestions(); - onFocus(e); - }} - pattern={pattern} - /> - {searchResultsHTML} -
- ); - } -} - -Tags.propTypes = { - defaultValue: PropTypes.string.isRequired, - onInput: PropTypes.func.isRequired, - maxTags: PropTypes.number.isRequired, - classPrefix: PropTypes.string.isRequired, - fieldClassName: PropTypes.string.isRequired, - listing: PropTypes.string, - category: PropTypes.string, - onFocus: PropTypes.func.isRequired, - pattern: PropTypes.string.isRequired, -}; diff --git a/app/views/listings/_form.html.erb b/app/views/listings/_form.html.erb index 37acf324f..d21a57e16 100644 --- a/app/views/listings/_form.html.erb +++ b/app/views/listings/_form.html.erb @@ -3,7 +3,7 @@ <%= render partial: "form_errors", locals: { listing: listing } %>
diff --git a/spec/helpers/listing_helper_spec.rb b/spec/helpers/listing_helper_spec.rb index 5b3ddd7de..eb4a81581 100644 --- a/spec/helpers/listing_helper_spec.rb +++ b/spec/helpers/listing_helper_spec.rb @@ -8,8 +8,8 @@ RSpec.describe ListingHelper, type: :helper do it "returns the correct options array" do expect(helper.select_options_for_categories).to match_array( [ - ["#{cat1.name} (1 Credit)", cat1.id], - ["#{cat2.name} (5 Credits)", cat2.id], + ["#{cat1.name} (1 Credit)", cat1.slug, cat1.id], + ["#{cat2.name} (5 Credits)", cat2.slug, cat2.id], ], ) end