Skip to content

Commit 9bf9837

Browse files
authored
history: adding multi-select (#1487)
* history: multi select * only reset if delete actually happened * only grey out the delete button when empty * fix some layout things * remove title_menu support * more unused * read-only signals * cleanup hooks * results * more cleanup * 1 less provider * latency fix * support deleting a domain * perform search on submit * clearer logs
1 parent 993d679 commit 9bf9837

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2920
-1206
lines changed

special-pages/pages/history/app/Settings.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ export class Settings {
22
/**
33
* @param {object} params
44
* @param {{name: 'macos' | 'windows'}} [params.platform]
5-
* @param {number} [params.typingDebounce=500] how long to debounce typing in the search field
5+
* @param {number} [params.typingDebounce=100] how long to debounce typing in the search field - default: 100ms
6+
* @param {number} [params.urlDebounce=500] how long to debounce reflecting to the URL? - default: 500ms
67
*/
7-
constructor({ platform = { name: 'macos' }, typingDebounce = 100 }) {
8+
constructor({ platform = { name: 'macos' }, typingDebounce = 100, urlDebounce = 500 }) {
89
this.platform = platform;
910
this.typingDebounce = typingDebounce;
11+
this.urlDebounce = urlDebounce;
1012
}
1113

1214
withPlatformName(name) {
@@ -35,4 +37,19 @@ export class Settings {
3537
}
3638
return this;
3739
}
40+
41+
/**
42+
* @param {null|undefined|number|string} value
43+
*/
44+
withUrlDebounce(value) {
45+
if (!value) return this;
46+
const input = String(value).trim();
47+
if (input.match(/^\d+$/)) {
48+
return new Settings({
49+
...this,
50+
urlDebounce: parseInt(input, 10),
51+
});
52+
}
53+
return this;
54+
}
3855
}

special-pages/pages/history/app/components/App.jsx

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,72 @@
11
import { h } from 'preact';
2+
import cn from 'classnames';
23
import styles from './App.module.css';
34
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
45
import { Header } from './Header.js';
5-
import { Results } from './Results.js';
6-
import { useRef } from 'preact/hooks';
6+
import { ResultsContainer } from './Results.js';
7+
import { useEffect, useRef } from 'preact/hooks';
78
import { Sidebar } from './Sidebar.js';
8-
import { useGlobalState } from '../global-state/GlobalStateProvider.js';
9-
import { useSelected } from '../global-state/SelectionProvider.js';
10-
import { useGlobalHandlers } from '../global-state/HistoryServiceProvider.js';
9+
import { useRowInteractions } from '../global/Providers/SelectionProvider.js';
10+
import { useQueryContext } from '../global/Providers/QueryProvider.js';
11+
import { useContextMenuForEntries } from '../global/hooks/useContextMenuForEntries.js';
12+
import { useAuxClickHandler } from '../global/hooks/useAuxClickHandler.js';
13+
import { useButtonClickHandler } from '../global/hooks/useButtonClickHandler.js';
14+
import { useLinkClickHandler } from '../global/hooks/useLinkClickHandler.js';
15+
import { useResetSelectionsOnQueryChange } from '../global/hooks/useResetSelectionsOnQueryChange.js';
16+
import { useSearchCommitForRange } from '../global/hooks/useSearchCommitForRange.js';
17+
import { useURLReflection } from '../global/hooks/useURLReflection.js';
18+
import { useSearchCommit } from '../global/hooks/useSearchCommit.js';
19+
import { useRangesData } from '../global/Providers/HistoryServiceProvider.js';
1120

1221
export function App() {
22+
const mainRef = useRef(/** @type {HTMLElement|null} */ (null));
1323
const { isDarkMode } = useEnv();
14-
const containerRef = useRef(/** @type {HTMLElement|null} */ (null));
15-
const { ranges, term, results } = useGlobalState();
16-
const selected = useSelected();
24+
const ranges = useRangesData();
25+
const query = useQueryContext();
1726

18-
useGlobalHandlers();
27+
/**
28+
* Handlers that are global in nature
29+
*/
30+
useResetSelectionsOnQueryChange();
31+
useLinkClickHandler();
32+
useButtonClickHandler();
33+
useContextMenuForEntries();
34+
useAuxClickHandler();
35+
useURLReflection();
36+
useSearchCommit();
37+
useSearchCommitForRange();
38+
39+
/**
40+
* onClick can be passed directly to the main container,
41+
* onKeyDown will be observed at the document level.
42+
* todo: can this be resolved if the `main` element is given focus/tab-index?
43+
*/
44+
const { onClick, onKeyDown } = useRowInteractions(mainRef);
45+
46+
useEffect(() => {
47+
// whenever the query changes, scroll the main container back to the top
48+
const unsubscribe = query.subscribe(() => {
49+
mainRef.current?.scrollTo(0, 0);
50+
});
51+
52+
document.addEventListener('keydown', onKeyDown);
53+
54+
return () => {
55+
document.removeEventListener('keydown', onKeyDown);
56+
unsubscribe();
57+
};
58+
}, [onKeyDown, query]);
1959

2060
return (
2161
<div class={styles.layout} data-theme={isDarkMode ? 'dark' : 'light'}>
22-
<header class={styles.header}>
23-
<Header />
24-
</header>
2562
<aside class={styles.aside}>
2663
<Sidebar ranges={ranges} />
2764
</aside>
28-
<main class={styles.main} ref={containerRef} data-main-scroller data-term={term}>
29-
<Results results={results} selected={selected} />
65+
<header class={styles.header}>
66+
<Header />
67+
</header>
68+
<main class={cn(styles.main, styles.customScroller)} ref={mainRef} onClick={onClick}>
69+
<ResultsContainer />
3070
</main>
3171
</div>
3272
);

special-pages/pages/history/app/components/App.module.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ body {
66
font-size: var(--body-font-size);
77
font-weight: var(--body-font-weight);
88
line-height: var(--body-line-height);
9+
background-color: var(--history-background-color);
910
}
1011

1112
.layout {
@@ -39,3 +40,20 @@ body {
3940
padding-right: 76px;
4041
padding-top: 24px;
4142
}
43+
44+
.customScroller {
45+
overflow-y: scroll;
46+
&::-webkit-scrollbar {
47+
width: 12px;
48+
}
49+
50+
&::-webkit-scrollbar-track {
51+
border-radius: 6px;
52+
}
53+
54+
&::-webkit-scrollbar-thumb {
55+
background: rgb(108, 108, 108);
56+
border: 4px solid var(--history-background-color);
57+
border-radius: 6px;
58+
}
59+
}

special-pages/pages/history/app/components/Empty.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ export function Empty() {
1010
const { t } = useTypedTranslation();
1111
return (
1212
<div class={cn(styles.emptyState, styles.emptyStateOffset)}>
13-
<img src="icons/clock.svg" width={128} height={96} alt="" class={styles.emptyStateImage} />
13+
<div class={styles.icons}>
14+
<img src="icons/backdrop.svg" width={128} height={96} alt="" />
15+
<img src="icons/clock.svg" width={60} height={60} alt="" class={styles.forground} />
16+
</div>
1417
<h2 class={styles.emptyTitle}>{t('empty_title')}</h2>
18+
<p class={styles.emptyText}>{t('empty_text')}</p>
1519
</div>
1620
);
1721
}

special-pages/pages/history/app/components/Header.js

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,90 @@ import { useComputed } from '@preact/signals';
44
import { SearchForm } from './SearchForm.js';
55
import { Trash } from '../icons/Trash.js';
66
import { useTypedTranslation } from '../types.js';
7-
import { useQueryContext } from '../global-state/QueryProvider.js';
8-
import { BTN_ACTION_DELETE_ALL } from '../constants.js';
7+
import { useQueryContext } from '../global/Providers/QueryProvider.js';
8+
import { useSelected } from '../global/Providers/SelectionProvider.js';
9+
import { useHistoryServiceDispatch, useResultsData } from '../global/Providers/HistoryServiceProvider.js';
910

1011
/**
1112
*/
1213
export function Header() {
13-
const { t } = useTypedTranslation();
1414
const search = useQueryContext();
1515
const term = useComputed(() => search.value.term);
16+
const range = useComputed(() => search.value.range);
17+
const domain = useComputed(() => search.value.domain);
1618
return (
1719
<div class={styles.root}>
18-
<div class={styles.controls}>
19-
<button class={styles.largeButton} data-action={BTN_ACTION_DELETE_ALL}>
20-
<span>{t('delete_all')}</span>
21-
<Trash />
22-
</button>
23-
</div>
20+
<Controls term={term} range={range} domain={domain} />
2421
<div class={styles.search}>
25-
<SearchForm term={term} />
22+
<SearchForm term={term} domain={domain} />
2623
</div>
2724
</div>
2825
);
2926
}
27+
28+
/**
29+
* Renders the Controls component that displays a button for deletion functionality.
30+
*
31+
* @param {Object} props - Properties passed to the component.
32+
* @param {import("@preact/signals").Signal<string|null>} props.term
33+
* @param {import("@preact/signals").Signal<string|null>} props.range
34+
* @param {import("@preact/signals").Signal<string|null>} props.domain
35+
*/
36+
function Controls({ term, range, domain }) {
37+
const { t } = useTypedTranslation();
38+
const results = useResultsData();
39+
const selected = useSelected();
40+
const dispatch = useHistoryServiceDispatch();
41+
42+
/**
43+
* Aria labels + title text is derived from the current result set.
44+
*/
45+
const ariaDisabled = useComputed(() => results.value.items.length === 0);
46+
const title = useComputed(() => (results.value.items.length === 0 ? t('delete_none') : ''));
47+
48+
/**
49+
* The button text should be 'delete all', unless there are row selections, then it's just 'delete'
50+
*/
51+
const buttonTxt = useComputed(() => {
52+
const hasSelections = selected.value.size > 0;
53+
if (hasSelections) return t('delete_some');
54+
return t('delete_all');
55+
});
56+
57+
/**
58+
* Which action should the delete button take?
59+
*
60+
* - if there are selections, they should be deleted by indexes
61+
* - if there's a range selected, that should be deleted
62+
* - if there's a search term, that should be deleted
63+
* - or fallback to deleting all
64+
*/
65+
function onClick() {
66+
if (ariaDisabled.value === true) return;
67+
if (selected.value.size > 0) {
68+
return dispatch({ kind: 'delete-entries-by-index', value: [...selected.value] });
69+
}
70+
if (range.value !== null) {
71+
return dispatch({ kind: 'delete-range', value: range.value });
72+
}
73+
if (term.value !== null && term.value !== '') {
74+
return dispatch({ kind: 'delete-term', term: term.value });
75+
}
76+
if (domain.value !== null) {
77+
return dispatch({ kind: 'delete-domain', domain: domain.value });
78+
}
79+
if (term.value !== null && term.value !== '') {
80+
return dispatch({ kind: 'delete-term', term: term.value });
81+
}
82+
dispatch({ kind: 'delete-all' });
83+
}
84+
85+
return (
86+
<div class={styles.controls}>
87+
<button class={styles.largeButton} onClick={onClick} aria-disabled={ariaDisabled} title={title}>
88+
<Trash />
89+
<span>{buttonTxt}</span>
90+
</button>
91+
</div>
92+
);
93+
}

special-pages/pages/history/app/components/Header.module.css

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,65 @@
4242
background-color: var(--color-white-at-12)
4343
}
4444
}
45+
46+
&[aria-disabled="true"] {
47+
opacity: .3;
48+
}
4549
}
4650

4751
.search {
4852
margin-left: auto;
4953
}
54+
.label {
55+
color: inherit;
56+
display: block;
57+
position: relative;
58+
}
59+
.searchIcon {
60+
position: absolute;
61+
display: block;
62+
width: 16px;
63+
height: 16px;
64+
top: 50%;
65+
left: 9px;
66+
transform: translateY(-50%);
67+
color: black;
68+
[data-theme="dark"] & {
69+
color: white;
70+
}
71+
}
5072
.searchInput {
5173
width: 238px;
5274
height: 28px;
5375
border-radius: 6px;
54-
border: 1px solid var(--history-surface-border-color);
55-
padding-left: 9px;
76+
border: 0.5px solid var(--history-surface-border-color);
77+
/* these precise numbers help it match figma when overriding default UI */
78+
padding-left: 31px;
5679
padding-right: 9px;
5780
background: inherit;
5881
color: inherit;
82+
83+
[data-theme="dark"] & {
84+
background: var(--color-white-at-6);
85+
border-color: var(--color-white-at-9);
86+
}
87+
5988
&:focus {
6089
outline: none;
90+
box-shadow: 0px 0px 0px 2.5px rgba(87, 151, 237, 0.64), 0px 0px 0px 1px rgba(87, 151, 237, 0.64) inset, 0px 0.5px 0px -0.5px rgba(0, 0, 0, 0.10), 0px 1px 0px -0.5px rgba(0, 0, 0, 0.10);
91+
}
92+
93+
&::-webkit-search-cancel-button {
94+
-webkit-appearance: none;
95+
height: 13px;
96+
width: 13px;
97+
background-image: url("../../public/icons/clear.svg");
98+
background-repeat: no-repeat;
99+
background-position: center center;
100+
cursor: pointer;
101+
}
102+
103+
[data-theme="dark"] &::-webkit-search-cancel-button {
104+
background-image: url("../../public/icons/clear-dark.svg");
61105
}
62106
}

0 commit comments

Comments
 (0)