Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions frontend/src/lib/forms/UserTypeahead.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import { FormField, PlainInput, randomFormId } from '$lib/forms';
import { _userTypeaheadSearch, _usersTypeaheadSearch, type SingleUserTypeaheadResult, type SingleUserICanSeeTypeaheadResult } from '$lib/gql/typeahead-queries';
import { overlay } from '$lib/overlay';
import { deriveAsync } from '$lib/util/time';
import { writable } from 'svelte/store';
import {resource} from 'runed';

type UserTypeaheadResult = SingleUserTypeaheadResult | SingleUserICanSeeTypeaheadResult;
let inputComponent: PlainInput | undefined = $state();
Expand Down Expand Up @@ -40,15 +39,14 @@

// making this explicit allows us to only react to input events,
// rather than programmatic changes like selecting a user
let trigger = writable('');
const _typeaheadResults = deriveAsync(trigger, (value) => typeaheadSearch(value), [], debounceMs);
let trigger = $state('');
const _typeaheadResults = resource(() => trigger, (value) => typeaheadSearch(value), {initialValue: [], debounce: debounceMs});

// TODO: Turn this into state instead of a store at some point
let selectedUser = writable<UserTypeaheadResult | null>(null);
let selectedUser = $state<UserTypeaheadResult | null>(null);

function selectUser(user: UserTypeaheadResult): void {
$selectedUser = user;
onSelectedUserChange?.($selectedUser);
selectedUser = user;
onSelectedUserChange?.(selectedUser);
selectedValue = getInputValue(user);
value = selectedValue;
}
Expand Down Expand Up @@ -99,14 +97,14 @@
typeaheadResults = []; // prevent old results showing when opening next time
}
}
let typeaheadResults = $derived($_typeaheadResults);
let typeaheadResults = $derived(_typeaheadResults.current);
let filteredResults = $derived(typeaheadResults.filter((user) => !exclude.includes(user.id)));
// TODO: Can this be simplified by making the "value !== selectedValue" part into a $derived?
// Then we'd be able to just do `$effect(() => { if (changedSelection) dispatch(...)})`
// And, of course, change the dispatch into calling a prop function
$effect(() => {
if ($selectedUser && value !== selectedValue) {
$selectedUser = null;
if (selectedUser && value !== selectedValue) {
selectedUser = null;
selectedValue = undefined;
onSelectedUserChange?.(null);
}
Expand All @@ -131,7 +129,7 @@
autocomplete="off"
{autofocus}
{keydownHandler}
onInput={(value) => trigger.set(value ?? '')}
onInput={(value) => trigger = value ?? ''}
Comment thread
hahn-kev marked this conversation as resolved.
/>
<div class="overlay-content">
<ul class="menu p-0">
Expand Down
111 changes: 0 additions & 111 deletions frontend/src/lib/util/time.test.ts

This file was deleted.

95 changes: 0 additions & 95 deletions frontend/src/lib/util/time.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { writable, type Readable, derived } from 'svelte/store';

export const enum Duration {
Persistent = 0,
Default = 5000,
Expand All @@ -12,96 +10,3 @@ export async function delay<T>(ms: Duration | number = Duration.Default): Promis
}

export const DEFAULT_DEBOUNCE_TIME = 400;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface Debouncer<P extends any[]> {
debounce: (...args: P) => void;
debouncing: Readable<boolean>;
clear: () => void;
}

function pickDebounceTime(debounce: number | boolean): number {
return typeof debounce === 'number' ? debounce : DEFAULT_DEBOUNCE_TIME;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function makeDebouncer<P extends any[]>(fn: Debouncer<P>['debounce'], debounce: number | boolean = DEFAULT_DEBOUNCE_TIME): Debouncer<P> {
const debouncing = writable(false);

if (!debounce) {
return { debounce: fn, debouncing, clear: () => { } };
} else {
const debounceTime = pickDebounceTime(debounce);
let timeout: ReturnType<typeof setTimeout>;
return {
debounce: (...args: P) => {
debouncing.set(true);
clearTimeout(timeout);
timeout = setTimeout(() => {
try {
fn(...args);
} finally {
debouncing.set(false);
}
}, debounceTime);
},
debouncing,
clear: () => {
clearTimeout(timeout);
debouncing.set(false);
},
};
}
}

/**
* @param fn A function that maps the store value to an async result
* @returns A store that contains the result of the async function, optionally debounced
*/
export function deriveAsync<T, D>(
store: Readable<T>,
fn: (value: T) => Promise<D>,
initialValue?: D,
debounce: number | boolean = false): Readable<D> {

const debounceTime = pickDebounceTime(debounce);
let timeout: ReturnType<typeof setTimeout> | undefined;

return derived(store, (value, set) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
const myTimeout = timeout;
void fn(value).then((result) => {
if (myTimeout !== timeout) return; // discard outdated results
set(result);
});
}, debounceTime);
}, initialValue);
}

/**
* @param fn A function that maps the store value to an async result, filtering out undefined values
* @returns A store that contains the result of the async function
*/
export function deriveAsyncIfDefined<T, D>(
store: Readable<T | undefined>,
fn: (value: T) => Promise<D>,
initialValue?: D,
debounce: number | boolean = false): Readable<D> {

const debounceTime = pickDebounceTime(debounce);
let timeout: ReturnType<typeof setTimeout> | undefined;

return derived(store, (value, set) => {
if (value) {
clearTimeout(timeout);
timeout = setTimeout(() => {
const myTimeout = timeout;
void fn(value).then((result) => {
if (myTimeout !== timeout) return; // discard outdated results
set(result);
});
}, debounceTime);
}
}, initialValue);
}
Loading
Loading