Skip to content

Commit

Permalink
history: documenting the data model (#1468)
Browse files Browse the repository at this point in the history
  • Loading branch information
shakyShane authored Feb 7, 2025
1 parent 82a9ec9 commit aacf81d
Show file tree
Hide file tree
Showing 49 changed files with 2,169 additions and 270 deletions.
125 changes: 125 additions & 0 deletions special-pages/pages/history/app/HistoryProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { h, createContext } from 'preact';
import { useContext } from 'preact/hooks';
import { useSignalEffect } from '@preact/signals';
import { paramsToQuery } from './history.service.js';
import { OVERSCAN_AMOUNT } from './constants.js';
import { usePlatformName } from './types.js';
import { eventToTarget } from '../../../shared/handlers.js';

// Create the context
const HistoryServiceContext = createContext({
service: /** @type {import("./history.service").HistoryService} */ ({}),
initial: /** @type {import("./history.service").ServiceData} */ ({}),
});

// Provider component
/**
* Provides a context for the history service, allowing dependent components to access it.
*
* @param {Object} props - The properties object for the HistoryServiceProvider component.
* @param {import("./history.service").HistoryService} props.service - The history service instance to be provided through the context.
* @param {import("./history.service").ServiceData} props.initial - The history service instance to be provided through the context.
* @param {import("preact").ComponentChild} props.children - The child components that will consume the history service context.
*/
export function HistoryServiceProvider({ service, initial, children }) {
const platFormName = usePlatformName();
useSignalEffect(() => {
// Add a listener for the 'search-commit' event
window.addEventListener('search-commit', (/** @type {CustomEvent<{params: URLSearchParams}>} */ event) => {
const detail = event.detail;
if (detail && detail.params instanceof URLSearchParams) {
const asQuery = paramsToQuery(detail.params);
service.trigger(asQuery);
} else {
console.error('missing detail.params from search-commit event');
}
});

// Cleanup the event listener on unmount
return () => {
window.removeEventListener('search-commit', this);
};
});

useSignalEffect(() => {
function handler(/** @type {CustomEvent<{start: number, end: number}>} */ event) {
if (!service.query.data) throw new Error('unreachable');
const { end } = event.detail;
const memory = service.query.data.results;
if (memory.length - end < OVERSCAN_AMOUNT) {
service.requestMore();
}
}
window.addEventListener('range-change', handler);
return () => {
window.removeEventListener('range-change', handler);
};
});

useSignalEffect(() => {
function handler(/** @type {MouseEvent} */ event) {
if (!(event.target instanceof Element)) return;
const btn = /** @type {HTMLButtonElement|null} */ (event.target.closest('button'));
const anchor = /** @type {HTMLButtonElement|null} */ (event.target.closest('a[href][data-url]'));
if (btn) {
if (btn?.dataset.titleMenu) {
event.stopImmediatePropagation();
event.preventDefault();
return confirm(`todo: title menu for ${btn.dataset.titleMenu}`);
}
if (btn?.dataset.rowMenu) {
event.stopImmediatePropagation();
event.preventDefault();
return confirm(`todo: row menu for ${btn.dataset.rowMenu}`);
}
if (btn?.dataset.deleteRange) {
event.stopImmediatePropagation();
event.preventDefault();
return confirm(`todo: delete range for ${btn.dataset.deleteRange}`);
}
if (btn?.dataset.deleteAll) {
event.stopImmediatePropagation();
event.preventDefault();
return confirm(`todo: delete all`);
}
} else if (anchor) {
const url = anchor.dataset.url;
if (!url) return;
event.preventDefault();
event.stopImmediatePropagation();
const target = eventToTarget(event, platFormName);
service.openUrl(url, target);
return;
}
return null;
}
document.addEventListener('click', handler);

const handleAuxClick = (event) => {
const anchor = /** @type {HTMLButtonElement|null} */ (event.target.closest('a[href][data-url]'));
const url = anchor?.dataset.url;
if (anchor && url && event.button === 1) {
event.preventDefault();
event.stopImmediatePropagation();
const target = eventToTarget(event, platFormName);
service.openUrl(url, target);
}
};
document.addEventListener('auxclick', handleAuxClick);

return () => {
document.removeEventListener('auxclick', handleAuxClick);
document.removeEventListener('click', handler);
};
});
return <HistoryServiceContext.Provider value={{ service, initial }}>{children}</HistoryServiceContext.Provider>;
}

// Hook for consuming the context
export function useHistory() {
const context = useContext(HistoryServiceContext);
if (!context) {
throw new Error('useHistoryService must be used within a HistoryServiceProvider');
}
return context;
}
38 changes: 38 additions & 0 deletions special-pages/pages/history/app/Settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export class Settings {
/**
* @param {object} params
* @param {{name: 'macos' | 'windows'}} [params.platform]
* @param {number} [params.typingDebounce=500] how long to debounce typing in the search field
*/
constructor({ platform = { name: 'macos' }, typingDebounce = 100 }) {
this.platform = platform;
this.typingDebounce = typingDebounce;
}

withPlatformName(name) {
/** @type {ImportMeta['platform'][]} */
const valid = ['windows', 'macos'];
if (valid.includes(/** @type {any} */ (name))) {
return new Settings({
...this,
platform: { name },
});
}
return this;
}

/**
* @param {null|undefined|number|string} value
*/
withDebounce(value) {
if (!value) return this;
const input = String(value).trim();
if (input.match(/^\d+$/)) {
return new Settings({
...this,
typingDebounce: parseInt(input, 10),
});
}
return this;
}
}
56 changes: 46 additions & 10 deletions special-pages/pages/history/app/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,64 @@ import { h } from 'preact';
import styles from './App.module.css';
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
import { Header } from './Header.js';
import { useSignal } from '@preact/signals';
import { batch, useSignal, useSignalEffect } from '@preact/signals';
import { Results } from './Results.js';
import { useRef } from 'preact/hooks';
import { useHistory } from '../HistoryProvider.js';
import { generateHeights } from '../utils.js';
import { Sidebar } from './Sidebar.js';

/**
* @typedef {object} Results
* @property {import('../../types/history').HistoryItem[]} items
* @property {number[]} heights
*/

export function App() {
const { isDarkMode } = useEnv();
const containerRef = useRef(/** @type {HTMLElement|null} */ (null));
const { initial, service } = useHistory();

const results = useSignal({
info: {
finished: true,
term: '',
},
value: [],
items: initial.query.results,
heights: generateHeights(initial.query.results),
});

const term = useSignal('term' in initial.query.info.query ? initial.query.info.query.term : '');

useSignalEffect(() => {
const unsub = service.onResults((data) => {
batch(() => {
if ('term' in data.info.query && data.info.query.term !== null) {
term.value = data.info.query.term;
}
results.value = {
items: data.results,
heights: generateHeights(data.results),
};
});
});
return () => {
unsub();
};
});

useSignalEffect(() => {
term.subscribe((t) => {
containerRef.current?.scrollTo(0, 0);
});
});

return (
<div class={styles.layout} data-theme={isDarkMode ? 'dark' : 'light'}>
<header class={styles.header}>
<Header setResults={(next) => (results.value = next)} />
<Header />
</header>
<aside class={styles.aside}>
<h1 class={styles.pageTitle}>History</h1>
<Sidebar ranges={initial.ranges.ranges} />
</aside>
<main class={styles.main}>
<Results />
<main class={styles.main} ref={containerRef} data-main-scroller data-term={term}>
<Results results={results} />
</main>
</div>
);
Expand Down
14 changes: 4 additions & 10 deletions special-pages/pages/history/app/components/App.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@import url("../../../../shared/styles/variables.css");
@import url("../../styles/base.css");
@import url("../../styles/theme.css");
@import url("../../styles/history-theme.css");

body {
font-size: var(--body-font-size);
Expand All @@ -11,19 +11,18 @@ body {
.layout {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 64px auto;
grid-template-rows: max-content 1fr;
grid-template-areas:
'aside header'
'aside main';
overflow: hidden;
height: 100vh;
background-color: var(--history-background-color);
}
.header {
grid-area: header;
padding-left: 48px;
padding-right: 76px;
padding-top: 16px;
padding-bottom: 16px;
}
.search {
justify-self: flex-end;
Expand All @@ -33,15 +32,10 @@ body {
padding: 10px 16px;
border-right: 1px solid var(--history-surface-border-color);
}
.pageTitle {
font-size: var(--title-font-size);
font-weight: var(--title-font-weight);
line-height: var(--title-line-height);
padding: 10px 6px 10px 10px;
}
.main {
grid-area: main;
overflow: auto;
padding-left: 48px;
padding-right: 76px;
padding-top: 24px;
}
53 changes: 12 additions & 41 deletions special-pages/pages/history/app/components/Header.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,24 @@
import styles from './Header.module.css';
import { Fire } from '../icons/Fire.js';
import { h } from 'preact';
import { useMessaging, useTypedTranslation } from '../types.js';
import { Cross } from '../icons/Cross.js';
import { useEffect } from 'preact/hooks';
import { useComputed } from '@preact/signals';
import { SearchForm, useSearchContext } from './SearchForm.js';
import { Trash } from '../icons/Trash.js';

export function Header({ setResults }) {
const { t } = useTypedTranslation();
const historyPage = useMessaging();
useEffect(() => {
historyPage
.query({ term: '', limit: 150, offset: 0 })
// eslint-disable-next-line promise/prefer-await-to-then
.then(setResults)
// eslint-disable-next-line promise/prefer-await-to-then
.catch((e) => {
console.log('did catch...', e);
});
}, []);
/**
*/
export function Header() {
const search = useSearchContext();
const term = useComputed(() => search.value.term);
return (
<div class={styles.root}>
<div class={styles.controls}>
<button class={styles.largeButton}>
<Fire />
<span>Clear History and Data...</span>
</button>
<button class={styles.largeButton}>
<Cross />
<span>Remove History...</span>
<button class={styles.largeButton} data-delete-all>
<span>Delete All</span>
<Trash />
</button>
</div>
<div class={styles.search}>
<form
action=""
onSubmit={(e) => {
e.preventDefault();
const data = new FormData(/** @type {HTMLFormElement} */ (e.target));
historyPage
.query({ term: data.get('term')?.toString() || '', limit: 150, offset: 0 })
// eslint-disable-next-line promise/prefer-await-to-then
.then(setResults)
// eslint-disable-next-line promise/prefer-await-to-then
.catch((e) => {
console.log('did catch...', e);
});
}}
>
<input type="search" placeholder={t('search')} class={styles.searchInput} name="term" />
</form>
<SearchForm term={term} />
</div>
</div>
);
Expand Down
Loading

0 comments on commit aacf81d

Please sign in to comment.