Skip to content

Commit aacf81d

Browse files
authored
history: documenting the data model (#1468)
1 parent 82a9ec9 commit aacf81d

Some content is hidden

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

49 files changed

+2169
-270
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { h, createContext } from 'preact';
2+
import { useContext } from 'preact/hooks';
3+
import { useSignalEffect } from '@preact/signals';
4+
import { paramsToQuery } from './history.service.js';
5+
import { OVERSCAN_AMOUNT } from './constants.js';
6+
import { usePlatformName } from './types.js';
7+
import { eventToTarget } from '../../../shared/handlers.js';
8+
9+
// Create the context
10+
const HistoryServiceContext = createContext({
11+
service: /** @type {import("./history.service").HistoryService} */ ({}),
12+
initial: /** @type {import("./history.service").ServiceData} */ ({}),
13+
});
14+
15+
// Provider component
16+
/**
17+
* Provides a context for the history service, allowing dependent components to access it.
18+
*
19+
* @param {Object} props - The properties object for the HistoryServiceProvider component.
20+
* @param {import("./history.service").HistoryService} props.service - The history service instance to be provided through the context.
21+
* @param {import("./history.service").ServiceData} props.initial - The history service instance to be provided through the context.
22+
* @param {import("preact").ComponentChild} props.children - The child components that will consume the history service context.
23+
*/
24+
export function HistoryServiceProvider({ service, initial, children }) {
25+
const platFormName = usePlatformName();
26+
useSignalEffect(() => {
27+
// Add a listener for the 'search-commit' event
28+
window.addEventListener('search-commit', (/** @type {CustomEvent<{params: URLSearchParams}>} */ event) => {
29+
const detail = event.detail;
30+
if (detail && detail.params instanceof URLSearchParams) {
31+
const asQuery = paramsToQuery(detail.params);
32+
service.trigger(asQuery);
33+
} else {
34+
console.error('missing detail.params from search-commit event');
35+
}
36+
});
37+
38+
// Cleanup the event listener on unmount
39+
return () => {
40+
window.removeEventListener('search-commit', this);
41+
};
42+
});
43+
44+
useSignalEffect(() => {
45+
function handler(/** @type {CustomEvent<{start: number, end: number}>} */ event) {
46+
if (!service.query.data) throw new Error('unreachable');
47+
const { end } = event.detail;
48+
const memory = service.query.data.results;
49+
if (memory.length - end < OVERSCAN_AMOUNT) {
50+
service.requestMore();
51+
}
52+
}
53+
window.addEventListener('range-change', handler);
54+
return () => {
55+
window.removeEventListener('range-change', handler);
56+
};
57+
});
58+
59+
useSignalEffect(() => {
60+
function handler(/** @type {MouseEvent} */ event) {
61+
if (!(event.target instanceof Element)) return;
62+
const btn = /** @type {HTMLButtonElement|null} */ (event.target.closest('button'));
63+
const anchor = /** @type {HTMLButtonElement|null} */ (event.target.closest('a[href][data-url]'));
64+
if (btn) {
65+
if (btn?.dataset.titleMenu) {
66+
event.stopImmediatePropagation();
67+
event.preventDefault();
68+
return confirm(`todo: title menu for ${btn.dataset.titleMenu}`);
69+
}
70+
if (btn?.dataset.rowMenu) {
71+
event.stopImmediatePropagation();
72+
event.preventDefault();
73+
return confirm(`todo: row menu for ${btn.dataset.rowMenu}`);
74+
}
75+
if (btn?.dataset.deleteRange) {
76+
event.stopImmediatePropagation();
77+
event.preventDefault();
78+
return confirm(`todo: delete range for ${btn.dataset.deleteRange}`);
79+
}
80+
if (btn?.dataset.deleteAll) {
81+
event.stopImmediatePropagation();
82+
event.preventDefault();
83+
return confirm(`todo: delete all`);
84+
}
85+
} else if (anchor) {
86+
const url = anchor.dataset.url;
87+
if (!url) return;
88+
event.preventDefault();
89+
event.stopImmediatePropagation();
90+
const target = eventToTarget(event, platFormName);
91+
service.openUrl(url, target);
92+
return;
93+
}
94+
return null;
95+
}
96+
document.addEventListener('click', handler);
97+
98+
const handleAuxClick = (event) => {
99+
const anchor = /** @type {HTMLButtonElement|null} */ (event.target.closest('a[href][data-url]'));
100+
const url = anchor?.dataset.url;
101+
if (anchor && url && event.button === 1) {
102+
event.preventDefault();
103+
event.stopImmediatePropagation();
104+
const target = eventToTarget(event, platFormName);
105+
service.openUrl(url, target);
106+
}
107+
};
108+
document.addEventListener('auxclick', handleAuxClick);
109+
110+
return () => {
111+
document.removeEventListener('auxclick', handleAuxClick);
112+
document.removeEventListener('click', handler);
113+
};
114+
});
115+
return <HistoryServiceContext.Provider value={{ service, initial }}>{children}</HistoryServiceContext.Provider>;
116+
}
117+
118+
// Hook for consuming the context
119+
export function useHistory() {
120+
const context = useContext(HistoryServiceContext);
121+
if (!context) {
122+
throw new Error('useHistoryService must be used within a HistoryServiceProvider');
123+
}
124+
return context;
125+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export class Settings {
2+
/**
3+
* @param {object} params
4+
* @param {{name: 'macos' | 'windows'}} [params.platform]
5+
* @param {number} [params.typingDebounce=500] how long to debounce typing in the search field
6+
*/
7+
constructor({ platform = { name: 'macos' }, typingDebounce = 100 }) {
8+
this.platform = platform;
9+
this.typingDebounce = typingDebounce;
10+
}
11+
12+
withPlatformName(name) {
13+
/** @type {ImportMeta['platform'][]} */
14+
const valid = ['windows', 'macos'];
15+
if (valid.includes(/** @type {any} */ (name))) {
16+
return new Settings({
17+
...this,
18+
platform: { name },
19+
});
20+
}
21+
return this;
22+
}
23+
24+
/**
25+
* @param {null|undefined|number|string} value
26+
*/
27+
withDebounce(value) {
28+
if (!value) return this;
29+
const input = String(value).trim();
30+
if (input.match(/^\d+$/)) {
31+
return new Settings({
32+
...this,
33+
typingDebounce: parseInt(input, 10),
34+
});
35+
}
36+
return this;
37+
}
38+
}

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

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,64 @@ import { h } from 'preact';
22
import styles from './App.module.css';
33
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
44
import { Header } from './Header.js';
5-
import { useSignal } from '@preact/signals';
5+
import { batch, useSignal, useSignalEffect } from '@preact/signals';
66
import { Results } from './Results.js';
7+
import { useRef } from 'preact/hooks';
8+
import { useHistory } from '../HistoryProvider.js';
9+
import { generateHeights } from '../utils.js';
10+
import { Sidebar } from './Sidebar.js';
11+
12+
/**
13+
* @typedef {object} Results
14+
* @property {import('../../types/history').HistoryItem[]} items
15+
* @property {number[]} heights
16+
*/
717

818
export function App() {
919
const { isDarkMode } = useEnv();
20+
const containerRef = useRef(/** @type {HTMLElement|null} */ (null));
21+
const { initial, service } = useHistory();
22+
1023
const results = useSignal({
11-
info: {
12-
finished: true,
13-
term: '',
14-
},
15-
value: [],
24+
items: initial.query.results,
25+
heights: generateHeights(initial.query.results),
26+
});
27+
28+
const term = useSignal('term' in initial.query.info.query ? initial.query.info.query.term : '');
29+
30+
useSignalEffect(() => {
31+
const unsub = service.onResults((data) => {
32+
batch(() => {
33+
if ('term' in data.info.query && data.info.query.term !== null) {
34+
term.value = data.info.query.term;
35+
}
36+
results.value = {
37+
items: data.results,
38+
heights: generateHeights(data.results),
39+
};
40+
});
41+
});
42+
return () => {
43+
unsub();
44+
};
45+
});
46+
47+
useSignalEffect(() => {
48+
term.subscribe((t) => {
49+
containerRef.current?.scrollTo(0, 0);
50+
});
1651
});
52+
1753
return (
1854
<div class={styles.layout} data-theme={isDarkMode ? 'dark' : 'light'}>
1955
<header class={styles.header}>
20-
<Header setResults={(next) => (results.value = next)} />
56+
<Header />
2157
</header>
2258
<aside class={styles.aside}>
23-
<h1 class={styles.pageTitle}>History</h1>
59+
<Sidebar ranges={initial.ranges.ranges} />
2460
</aside>
25-
<main class={styles.main}>
26-
<Results />
61+
<main class={styles.main} ref={containerRef} data-main-scroller data-term={term}>
62+
<Results results={results} />
2763
</main>
2864
</div>
2965
);
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@import url("../../../../shared/styles/variables.css");
22
@import url("../../styles/base.css");
3-
@import url("../../styles/theme.css");
3+
@import url("../../styles/history-theme.css");
44

55
body {
66
font-size: var(--body-font-size);
@@ -11,19 +11,18 @@ body {
1111
.layout {
1212
display: grid;
1313
grid-template-columns: 250px 1fr;
14-
grid-template-rows: 64px auto;
14+
grid-template-rows: max-content 1fr;
1515
grid-template-areas:
1616
'aside header'
1717
'aside main';
1818
overflow: hidden;
1919
height: 100vh;
20+
background-color: var(--history-background-color);
2021
}
2122
.header {
2223
grid-area: header;
2324
padding-left: 48px;
2425
padding-right: 76px;
25-
padding-top: 16px;
26-
padding-bottom: 16px;
2726
}
2827
.search {
2928
justify-self: flex-end;
@@ -33,15 +32,10 @@ body {
3332
padding: 10px 16px;
3433
border-right: 1px solid var(--history-surface-border-color);
3534
}
36-
.pageTitle {
37-
font-size: var(--title-font-size);
38-
font-weight: var(--title-font-weight);
39-
line-height: var(--title-line-height);
40-
padding: 10px 6px 10px 10px;
41-
}
4235
.main {
4336
grid-area: main;
4437
overflow: auto;
4538
padding-left: 48px;
4639
padding-right: 76px;
40+
padding-top: 24px;
4741
}

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

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,24 @@
11
import styles from './Header.module.css';
2-
import { Fire } from '../icons/Fire.js';
32
import { h } from 'preact';
4-
import { useMessaging, useTypedTranslation } from '../types.js';
5-
import { Cross } from '../icons/Cross.js';
6-
import { useEffect } from 'preact/hooks';
3+
import { useComputed } from '@preact/signals';
4+
import { SearchForm, useSearchContext } from './SearchForm.js';
5+
import { Trash } from '../icons/Trash.js';
76

8-
export function Header({ setResults }) {
9-
const { t } = useTypedTranslation();
10-
const historyPage = useMessaging();
11-
useEffect(() => {
12-
historyPage
13-
.query({ term: '', limit: 150, offset: 0 })
14-
// eslint-disable-next-line promise/prefer-await-to-then
15-
.then(setResults)
16-
// eslint-disable-next-line promise/prefer-await-to-then
17-
.catch((e) => {
18-
console.log('did catch...', e);
19-
});
20-
}, []);
7+
/**
8+
*/
9+
export function Header() {
10+
const search = useSearchContext();
11+
const term = useComputed(() => search.value.term);
2112
return (
2213
<div class={styles.root}>
2314
<div class={styles.controls}>
24-
<button class={styles.largeButton}>
25-
<Fire />
26-
<span>Clear History and Data...</span>
27-
</button>
28-
<button class={styles.largeButton}>
29-
<Cross />
30-
<span>Remove History...</span>
15+
<button class={styles.largeButton} data-delete-all>
16+
<span>Delete All</span>
17+
<Trash />
3118
</button>
3219
</div>
3320
<div class={styles.search}>
34-
<form
35-
action=""
36-
onSubmit={(e) => {
37-
e.preventDefault();
38-
const data = new FormData(/** @type {HTMLFormElement} */ (e.target));
39-
historyPage
40-
.query({ term: data.get('term')?.toString() || '', limit: 150, offset: 0 })
41-
// eslint-disable-next-line promise/prefer-await-to-then
42-
.then(setResults)
43-
// eslint-disable-next-line promise/prefer-await-to-then
44-
.catch((e) => {
45-
console.log('did catch...', e);
46-
});
47-
}}
48-
>
49-
<input type="search" placeholder={t('search')} class={styles.searchInput} name="term" />
50-
</form>
21+
<SearchForm term={term} />
5122
</div>
5223
</div>
5324
);

0 commit comments

Comments
 (0)