Improve comparisons navigation (#2736)

This pull request enhances the navigation for comparisons by implementing the following changes:

* Hides the comparison input behind the period picker, making the top bar less busy.
* Adds a key binding for the comparison input (assigned to x) alongside arrow navigation.
* Persists the selected comparison mode in Local Storage by domain, resulting in its persistence between refreshes.
This commit is contained in:
Vini Brasil 2023-03-15 07:30:05 -03:00 committed by GitHub
parent b17710a706
commit 49e4c2fb64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 46 deletions

View File

@ -4,42 +4,85 @@ import { navigateToQuery } from './query'
import { Menu, Transition } from '@headlessui/react' import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid' import { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames' import classNames from 'classnames'
import * as storage from './util/storage'
const COMPARISON_MODES = { const COMPARISON_MODES = {
'off': 'Disable comparison',
'previous_period': 'Previous period', 'previous_period': 'Previous period',
'year_over_year': 'Year over year', 'year_over_year': 'Year over year',
} }
const DEFAULT_COMPARISON_MODE = 'previous_period'
export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all'] export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all']
export const getStoredComparisonMode = function(domain) {
const mode = storage.getItem(`comparison_mode__${domain}`)
if (Object.keys(COMPARISON_MODES).includes(mode)) {
return mode
} else {
return null
}
}
const storeComparisonMode = function(domain, mode) {
storage.setItem(`comparison_mode__${domain}`, mode)
}
export const isComparisonEnabled = function(mode) {
return mode && mode !== "off"
}
export const toggleComparisons = function(history, query, site) {
if (!site.flags.comparisons) return
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return
if (isComparisonEnabled(query.comparison)) {
storeComparisonMode(site.domain, "off")
navigateToQuery(history, query, { comparison: "off" })
} else {
const storedMode = getStoredComparisonMode(site.domain)
const newMode = isComparisonEnabled(storedMode) ? storedMode : DEFAULT_COMPARISON_MODE
storeComparisonMode(site.domain, newMode)
navigateToQuery(history, query, { comparison: newMode })
}
}
function DropdownItem({ label, value, isCurrentlySelected, updateMode }) {
return (
<Menu.Item
key={value}
onClick={() => updateMode(value)}
disabled={isCurrentlySelected}>
{({ active }) => (
<button className={classNames("px-4 py-2 w-full text-left font-medium text-sm dark:text-white cursor-pointer", { "bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100": active, "font-bold": isCurrentlySelected })}>
{ label }
</button>
)}
</Menu.Item>
)
}
const ComparisonInput = function({ site, query, history }) { const ComparisonInput = function({ site, query, history }) {
if (!site.flags.comparisons) return null if (!site.flags.comparisons) return null
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
if (!isComparisonEnabled(query.comparison)) return null
function update(key) { const updateMode = (key) => {
storeComparisonMode(site.domain, key)
navigateToQuery(history, query, { comparison: key }) navigateToQuery(history, query, { comparison: key })
} }
function renderItem({ label, value, isCurrentlySelected }) {
const labelClass = classNames("font-medium text-sm", { "font-bold disabled": isCurrentlySelected })
return ( return (
<Menu.Item <>
key={value} <span className="pl-2 text-sm font-medium text-gray-800 dark:text-gray-200">vs.</span>
onClick={() => update(value)} <div className="flex">
className="px-4 py-2 leading-tight hover:bg-gray-100 dark:text-white hover:text-gray-900 dark:hover:bg-gray-900 dark:hover:text-gray-100 flex hover:cursor-pointer"> <div className="min-w-32 md:w-52 md:relative">
<span className={labelClass}>{ label }</span>
</Menu.Item>
)
}
return (
<div className="flex ml-auto pl-2">
<div className="w-20 sm:w-36 md:w-48 md:relative">
<Menu as="div" className="relative inline-block pl-2 w-full"> <Menu as="div" className="relative inline-block pl-2 w-full">
<Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow truncate cursor-pointer w-full"> <Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow cursor-pointer w-full truncate">
<span>{ COMPARISON_MODES[query.comparison] || 'Compare to' }</span> <span className="truncate">{ COMPARISON_MODES[query.comparison] || 'Compare to' }</span>
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-5" /> <ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-2" aria-hidden="true" />
</Menu.Button> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
@ -50,13 +93,13 @@ const ComparisonInput = function({ site, query, history }) {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"> leaveTo="transform opacity-0 scale-95">
<Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10" static> <Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10" static>
{ renderItem({ label: "Disabled", value: false, isCurrentlySelected: !query.comparison }) } { Object.keys(COMPARISON_MODES).map((key) => DropdownItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode })) }
{ Object.keys(COMPARISON_MODES).map((key) => renderItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison})) }
</Menu.Items> </Menu.Items>
</Transition> </Transition>
</Menu> </Menu>
</div> </div>
</div> </div>
</>
) )
} }

View File

@ -23,6 +23,7 @@ import {
} from "./util/date"; } from "./util/date";
import { navigateToQuery, QueryLink, QueryButton } from "./query"; import { navigateToQuery, QueryLink, QueryButton } from "./query";
import { shouldIgnoreKeypress } from "./keybinding.js" import { shouldIgnoreKeypress } from "./keybinding.js"
import { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input.js"
function renderArrow(query, site, period, prevDate, nextDate) { function renderArrow(query, site, period, prevDate, nextDate) {
const insertionDate = parseUTCDate(site.statsBegin); const insertionDate = parseUTCDate(site.statsBegin);
@ -194,11 +195,24 @@ function DatePicker({query, site, history}) {
setOpen(false); setOpen(false);
const keys = ['d', 'e', 'r', 'w', 'm', 'y', 't', 's', 'l', 'a']; const keybindings = {
const redirects = [{date: false, period: 'day'}, {date: formatISO(shiftDays(nowForSite(site), -1)), period: 'day'}, {period: 'realtime'}, {date: false, period: '7d'}, {date: false, period: 'month'}, {date: false, period: 'year'}, {date: false, period: '30d'}, {date: false, period: '6mo'}, {date: false, period: '12mo'}, {date: false, period: 'all'}]; d: {date: false, period: 'day'},
e: {date: formatISO(shiftDays(nowForSite(site), -1)), period: 'day'},
r: {period: 'realtime'},
w: {date: false, period: '7d'},
m: {date: false, period: 'month'},
y: {date: false, period: 'year'},
t: {date: false, period: '30d'},
s: {date: false, period: '6mo'},
l: {date: false, period: '12mo'},
a: {date: false, period: 'all'},
}
if (keys.includes(e.key.toLowerCase())) { const redirect = keybindings[e.key.toLowerCase()]
navigateToQuery(history, query, {...newSearch, ...(redirects[keys.indexOf(e.key.toLowerCase())])}); if (redirect) {
navigateToQuery(history, query, {...newSearch, ...redirect})
} else if (e.key.toLowerCase() === 'x') {
toggleComparisons(history, query, site)
} else if (e.key.toLowerCase() === 'c') { } else if (e.key.toLowerCase() === 'c') {
setOpen(true) setOpen(true)
setMode('calendar') setMode('calendar')
@ -352,6 +366,18 @@ function DatePicker({query, site, history}) {
<span className='font-normal'>C</span> <span className='font-normal'>C</span>
</span> </span>
</div> </div>
{ !COMPARISON_DISABLED_PERIODS.includes(query.period) && site.flags.comparisons &&
<div className="py-1 date-option-group border-t border-gray-200 dark:border-gray-500">
<span
onClick={() => {
toggleComparisons(history, query, site)
setOpen(false)
}}
className="px-4 py-2 text-sm leading-tight hover:bg-gray-100 dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer flex items-center justify-between">
{ isComparisonEnabled(query.comparison) ? 'Disable comparison' : 'Compare' }
<span className='font-normal'>X</span>
</span>
</div> }
</div> </div>
</div> </div>
); );
@ -381,7 +407,7 @@ function DatePicker({query, site, history}) {
function renderPicker() { function renderPicker() {
return ( return (
<div <div
className="w-20 sm:w-36 md:w-48 md:relative" className="min-w-32 md:w-48 md:relative"
ref={dropDownNode} ref={dropDownNode}
> >
<div <div

View File

@ -2,7 +2,7 @@ import React from 'react'
import { Link, withRouter } from 'react-router-dom' import { Link, withRouter } from 'react-router-dom'
import {formatDay, formatMonthYYYY, nowForSite, parseUTCDate} from './util/date' import {formatDay, formatMonthYYYY, nowForSite, parseUTCDate} from './util/date'
import * as storage from './util/storage' import * as storage from './util/storage'
import { COMPARISON_DISABLED_PERIODS } from './comparison-input' import { COMPARISON_DISABLED_PERIODS, getStoredComparisonMode, isComparisonEnabled } from './comparison-input'
const PERIODS = ['realtime', 'day', 'month', '7d', '30d', '6mo', '12mo', 'year', 'all', 'custom'] const PERIODS = ['realtime', 'day', 'month', '7d', '30d', '6mo', '12mo', 'year', 'all', 'custom']
@ -19,16 +19,16 @@ export function parseQuery(querystring, site) {
period = '30d' period = '30d'
} }
let comparison = q.get('comparison') let comparison = q.get('comparison') || getStoredComparisonMode(site.domain)
if (COMPARISON_DISABLED_PERIODS.includes(period)) comparison = null if (COMPARISON_DISABLED_PERIODS.includes(period) || !isComparisonEnabled(comparison)) comparison = null
return { return {
period, period,
comparison,
date: q.get('date') ? parseUTCDate(q.get('date')) : nowForSite(site), date: q.get('date') ? parseUTCDate(q.get('date')) : nowForSite(site),
from: q.get('from') ? parseUTCDate(q.get('from')) : undefined, from: q.get('from') ? parseUTCDate(q.get('from')) : undefined,
to: q.get('to') ? parseUTCDate(q.get('to')) : undefined, to: q.get('to') ? parseUTCDate(q.get('to')) : undefined,
with_imported: q.get('with_imported') ? q.get('with_imported') === 'true' : true, with_imported: q.get('with_imported') ? q.get('with_imported') === 'true' : true,
comparison: comparison,
filters: { filters: {
'goal': q.get('goal'), 'goal': q.get('goal'),
'props': JSON.parse(q.get('props')), 'props': JSON.parse(q.get('props')),