Second pass - mostly everything user-facing is done

Still needs:
- Tests
- Potentially negating other filters
- Potentially some code cleanup
This commit is contained in:
Vignesh Joglekar 2021-05-21 03:23:16 -05:00
parent cb7b5b9fbd
commit 007d44df38
2 changed files with 100 additions and 58 deletions

View File

@ -102,6 +102,9 @@ class Filters extends React.Component {
};
filterText(key, value, query) {
const negated = value[0] == '!' && ['page', 'entry_page', 'exit_page'].includes(key)
value = negated ? value.slice(1) : value
if (key === "goal") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Completed goal <b>{value}</b></span>
}
@ -148,13 +151,13 @@ class Filters extends React.Component {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Country: <b>{selectedCountry.properties.name}</b></span>
}
if (key === "page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Page: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Page{negated ? ' (not)' : ''}: <b>{value}</b></span>
}
if (key === "entry_page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Entry Page: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Entry Page{negated ? ' (not)' : ''}: <b>{value}</b></span>
}
if (key === "exit_page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Exit Page: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Exit Page{negated ? ' (not)' : ''}: <b>{value}</b></span>
}
}
@ -174,7 +177,13 @@ class Filters extends React.Component {
return (
<div className="px-4 sm:py-2 py-3 md:text-sm leading-tight flex items-center justify-between" key={key + value}>
{this.filterText(key, value, query)}
<b className="ml-1 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}></b>
<span className="ml-2 flex items-center">
{!['goal', 'props'].includes(key) &&
<Link to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}/filter/${key}`, search: window.location.search }}>
<svg className="cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</Link>}
<b className="ml-2 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}></b>
</span>
</div>
)
}
@ -182,7 +191,14 @@ class Filters extends React.Component {
renderListFilter(history, [key, value], query) {
return (
<span key={key} title={value} className="flex bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 shadow text-sm rounded py-2 px-3 mr-2">
{this.filterText(key, value, query)} <b className="ml-1 cursor-pointer hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}></b>
{this.filterText(key, value, query)}
<span className="ml-2 flex items-center">
{!['goal', 'props'].includes(key) &&
<Link to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}/filter/${key}`, search: window.location.search }}>
<svg className="cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</Link>}
<b className="ml-2 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}></b>
</span>
</span>
)
}
@ -203,8 +219,8 @@ class Filters extends React.Component {
return (
<div className="absolute mt-2 rounded shadow-md z-10" style={{ width: viewport <= 768 ? '320px' : '350px', right: '-5px' }} ref={node => this.dropDownNode = node}>
<div className="rounded bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 font-medium text-gray-800 dark:text-gray-200 flex flex-col">
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className="border-b flex border-gray-200 dark:border-gray-500 px-4 sm:py-2 py-3 md:text-sm leading-tight hover:text-indigo-700 dark:hover:text-indigo-500 hover:cursor-pointer">
<svg className="mr-2 h-4 w-4 text-gray-500 dark:text-gray-200" fill="none" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className="group border-b flex border-gray-200 dark:border-gray-500 px-4 sm:py-2 py-3 md:text-sm leading-tight hover:text-indigo-700 dark:hover:text-indigo-500 hover:cursor-pointer">
<svg className="mr-2 h-4 w-4 text-gray-500 dark:text-gray-200 group-hover:text-indigo-700 dark:group-hover:text-indigo-500 hover:cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
Add Filter
</Link>
{this.appliedFilters.map((filter) => this.renderDropdownFilter(history, filter, query))}
@ -247,10 +263,10 @@ class Filters extends React.Component {
const {viewport} = this.state;
return (
<div id="filters" className="flex flex-grow pl-3 flex-wrap">
<div id="filters" className="flex flex-grow pl-2 flex-wrap">
{(this.appliedFilters.map((filter) => this.renderListFilter(history, filter, query)))}
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className={`button ${viewport <= 768 ? "px-2 mr-1" : "px-3 mr-2"} py-2 cursor-pointer ml-auto`}>
<svg className={`${viewport <= 768 ? "mr-1" : "mr-2"} h-4 w-4 text-gray-500 dark:text-gray-200 group-hover:text-gray-600 dark:group-hover:text-gray-400 group-focus:text-gray-500 dark:group-focus:text-gray-200`} fill="none" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
<svg className={`${viewport <= 768 ? "mr-1" : "mr-2"} h-4 w-4`} fill="none" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
{viewport <= 768 ? "Filter" : "Add Filter"}
</Link>
</div>

View File

@ -1,91 +1,117 @@
import React from "react";
import { withRouter, Link } from 'react-router-dom'
import { withRouter, Redirect } from 'react-router-dom'
import Modal from './modal'
import { parseQuery, formattedFilters } from '../../query'
import { parseQuery, formattedFilters, navigateToQuery } from '../../query'
class FilterModal extends React.Component {
constructor(props) {
super(props)
this.state = {
query: parseQuery(props.location.search, props.site),
selectedFilter: this.props.match.params.field || "",
selectedFilter: "",
negated: false,
updatedValue: "",
negated: false
filterSaved: false
}
this.editableGoals = Object.keys(this.state.query.filters).filter(filter => !['goal', 'props'].includes(filter))
}
componentDidMount() {
this.setState({ selectedFilter: this.props.match.params.field })
}
componentDidUpdate(prevProps, prevState) {
const { query, selectedFilter } = this.state
if (prevState.selectedFilter !== selectedFilter) {
const negated = query.filters[selectedFilter] && query.filters[selectedFilter][0] == '!' && this.negationSupported(selectedFilter)
const updatedValue = negated ? query.filters[selectedFilter].slice(1) : (query.filters[selectedFilter] || "")
this.setState({ updatedValue, negated })
}
}
negationSupported(filter) {
return ['page', 'entry_page', 'exit_page'].includes(filter)
}
renderBody() {
const { selectedFilter, negated, updatedValue, query } = this.state;
const updatedQuery = new URLSearchParams(window.location.search)
const finalFilterValue = (['page', 'entry_page', 'exit_page'].includes(selectedFilter) && negated ? '!' : '') + updatedValue
const validQuery = this.editableGoals.includes(selectedFilter) && updatedValue
updatedQuery.set(selectedFilter, finalFilterValue)
const finalFilterValue = (this.negationSupported(selectedFilter) && negated ? '!' : '') + updatedValue
const finalizedQuery = new URLSearchParams(window.location.search)
const validFilter = this.editableGoals.includes(selectedFilter) && updatedValue
finalizedQuery.set(selectedFilter, finalFilterValue)
return (
<React.Fragment>
<h1 className="text-xl font-bold dark:text-gray-100">{query.filters[selectedFilter] ? 'Edit' : 'Add'} a Filter</h1>
<h1 className="text-xl font-bold dark:text-gray-100">{query.filters[selectedFilter] ? 'Edit' : 'Add'} Filter</h1>
<div className="my-4 border-b border-gray-300"></div>
<main className="modal__content flex flex-col">
<select
value={selectedFilter}
className="block w-full py-2 pl-3 pr-10 mt-1 text-base border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300"
placeholder="Select a Filter"
onChange={(e) => {
const negated = query.filters[e.target.value][0] == '!' && ['page', 'entry_page', 'exit_page'].includes(e.target.value)
const updatedValue = negated ? query.filters[e.target.value].slice(1) : (query.filters[e.target.value] || "")
this.setState({ selectedFilter: e.target.value, updatedValue, negated })
}}
>
<option disabled value="" className="hidden">Select a Filter</option>
{this.editableGoals.map(filter => <option key={filter} value={filter}>{formattedFilters[filter]}</option>)}
</select>
<main className="modal__content">
<form className="flex flex-col" onSubmit={() => {
if (validFilter) {
this.setState({ finalizedQuery })
}
}}>
<select
value={selectedFilter}
className="my-2 block w-full py-2 pl-3 pr-10 text-base border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300"
placeholder="Select a Filter"
onChange={(e) => this.setState({ selectedFilter: e.target.value })}
>
<option disabled value="" className="hidden">Select a Filter</option>
{this.editableGoals.map(filter => <option key={filter} value={filter}>{formattedFilters[filter]}</option>)}
</select>
{['page', 'entry_page', 'exit_page'].includes(selectedFilter) &&
<div className="my-4 flex items-center">
<label className="text-gray-700 dark:text-gray-300 text-sm cursor-pointer">
<input
type="checkbox"
className={`${negated ? 'bg-indigo-600' : 'bg-gray-200 dark:bg-gray-700'} mr-2 relative inline-flex flex-shrink-0 h-6 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring`}
checked={negated}
name="exclude"
onClick={(e) => this.setState({ negated: e.target.checked })}
/>
{this.negationSupported(selectedFilter) &&
<div className="mt-4 flex items-center">
<label className="text-gray-700 dark:text-gray-300 text-sm cursor-pointer">
<input
type="checkbox"
className={"text-indigo-600 bg-gray-100 dark:bg-gray-700 mr-2 relative inline-flex flex-shrink-0 h-6 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none"}
checked={negated}
name="exclude"
onChange={(e) => this.setState({ negated: e.target.checked })}
/>
Exclude pages matching this filter
</label>
</div>
}
</div>
}
{selectedFilter &&
<input
type="text"
className="bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500"
value={updatedValue}
placeholder="Filter value"
onChange={(e) => { this.setState({ updatedValue: e.target.value }) }}
/>
}
{selectedFilter &&
<input
type="text"
className="mt-4 bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500"
value={updatedValue}
placeholder="Filter value"
onChange={(e) => { this.setState({ updatedValue: e.target.value }) }}
/>
}
<Link to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}`, search: updatedQuery.toString() }}
className={"relative button my-4 w-2/3"}
>
{query.filters[selectedFilter] ? 'Update' : 'Add'} Filter
</Link>
<button
type="submit"
disabled={!validFilter}
className={"button mt-4 w-2/3 mx-auto"}
>
{query.filters[selectedFilter] ? 'Update' : 'Add'} Filter
</button>
</form>
</main>
</React.Fragment>
)
}
render() {
const { finalizedQuery } = this.state
if (finalizedQuery) {
return <Redirect to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}`, search: finalizedQuery.toString() }} />
}
return (
<Modal site={this.props.site} maxWidth="460px">
{ this.renderBody()}