Add URL-synced listing search (#1217)

* Add URL-synced listing search

Closes #1174

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Aaron Powell
2026-03-30 12:26:37 +11:00
committed by GitHub
parent 1edf5bc29d
commit aa86725613
9 changed files with 401 additions and 17 deletions

View File

@@ -12,6 +12,16 @@ export function getChoicesValues(choices: Choices): string[] {
return Array.isArray(val) ? val : (val ? [val] : []); return Array.isArray(val) ? val : (val ? [val] : []);
} }
/**
* Restore selected values on a Choices instance.
*/
export function setChoicesValues(choices: Choices, values: string[]): void {
// Clear any existing active items so that the final selection matches `values`
choices.removeActiveItems();
// Set all provided values as the current selection
choices.setChoiceByValue(values);
}
/** /**
* Create a new Choices instance with sensible defaults * Create a new Choices instance with sensible defaults
*/ */

View File

@@ -1,9 +1,23 @@
/** /**
* Agents page functionality * Agents page functionality
*/ */
import { createChoices, getChoicesValues, type Choices } from '../choices'; import {
createChoices,
getChoicesValues,
setChoicesValues,
type Choices,
} from '../choices';
import { FuzzySearch, type SearchItem } from '../search'; import { FuzzySearch, type SearchItem } from '../search';
import { fetchData, debounce, setupDropdownCloseHandlers, setupActionHandlers } from '../utils'; import {
fetchData,
debounce,
getQueryParam,
getQueryParamFlag,
getQueryParamValues,
setupDropdownCloseHandlers,
setupActionHandlers,
updateQueryParams,
} from '../utils';
import { setupModal, openFileModal } from '../modal'; import { setupModal, openFileModal } from '../modal';
import { renderAgentsHtml, sortAgents, type AgentSortOption, type RenderableAgent } from './agents-render'; import { renderAgentsHtml, sortAgents, type AgentSortOption, type RenderableAgent } from './agents-render';
@@ -111,6 +125,16 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
resourceListHandlersReady = true; resourceListHandlersReady = true;
} }
function syncUrlState(searchInput: HTMLInputElement | null): void {
updateQueryParams({
q: searchInput?.value ?? '',
model: currentFilters.models,
tool: currentFilters.tools,
handoffs: currentFilters.hasHandoffs,
sort: currentSort === 'title' ? '' : currentSort,
});
}
export async function initAgentsPage(): Promise<void> { export async function initAgentsPage(): Promise<void> {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
const searchInput = document.getElementById('search-input') as HTMLInputElement; const searchInput = document.getElementById('search-input') as HTMLInputElement;
@@ -132,23 +156,46 @@ export async function initAgentsPage(): Promise<void> {
// Initialize Choices.js for model filter // Initialize Choices.js for model filter
modelSelect = createChoices('#filter-model', { placeholderValue: 'All Models' }); modelSelect = createChoices('#filter-model', { placeholderValue: 'All Models' });
modelSelect.setChoices(data.filters.models.map(m => ({ value: m, label: m })), 'value', 'label', true); modelSelect.setChoices(data.filters.models.map(m => ({ value: m, label: m })), 'value', 'label', true);
const initialQuery = getQueryParam('q');
const initialModels = getQueryParamValues('model').filter(model => data.filters.models.includes(model));
const initialTools = getQueryParamValues('tool').filter(tool => data.filters.tools.includes(tool));
const initialSort = getQueryParam('sort');
if (searchInput) searchInput.value = initialQuery;
if (initialModels.length > 0) {
currentFilters.models = initialModels;
setChoicesValues(modelSelect, initialModels);
}
document.getElementById('filter-model')?.addEventListener('change', () => { document.getElementById('filter-model')?.addEventListener('change', () => {
currentFilters.models = getChoicesValues(modelSelect); currentFilters.models = getChoicesValues(modelSelect);
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
// Initialize Choices.js for tool filter // Initialize Choices.js for tool filter
toolSelect = createChoices('#filter-tool', { placeholderValue: 'All Tools' }); toolSelect = createChoices('#filter-tool', { placeholderValue: 'All Tools' });
toolSelect.setChoices(data.filters.tools.map(t => ({ value: t, label: t })), 'value', 'label', true); toolSelect.setChoices(data.filters.tools.map(t => ({ value: t, label: t })), 'value', 'label', true);
if (initialTools.length > 0) {
currentFilters.tools = initialTools;
setChoicesValues(toolSelect, initialTools);
}
document.getElementById('filter-tool')?.addEventListener('change', () => { document.getElementById('filter-tool')?.addEventListener('change', () => {
currentFilters.tools = getChoicesValues(toolSelect); currentFilters.tools = getChoicesValues(toolSelect);
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
// Initialize sort select // Initialize sort select
if (initialSort === 'lastUpdated') {
currentSort = initialSort;
if (sortSelect) sortSelect.value = initialSort;
}
sortSelect?.addEventListener('change', () => { sortSelect?.addEventListener('change', () => {
currentSort = sortSelect.value as AgentSortOption; currentSort = sortSelect.value as AgentSortOption;
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
const countEl = document.getElementById('results-count'); const countEl = document.getElementById('results-count');
@@ -156,11 +203,20 @@ export async function initAgentsPage(): Promise<void> {
countEl.textContent = `${allItems.length} of ${allItems.length} agents`; countEl.textContent = `${allItems.length} of ${allItems.length} agents`;
} }
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200)); searchInput?.addEventListener('input', debounce(() => {
applyFiltersAndRender();
syncUrlState(searchInput);
}, 200));
if (getQueryParamFlag('handoffs')) {
currentFilters.hasHandoffs = true;
if (handoffsCheckbox) handoffsCheckbox.checked = true;
}
handoffsCheckbox?.addEventListener('change', () => { handoffsCheckbox?.addEventListener('change', () => {
currentFilters.hasHandoffs = handoffsCheckbox.checked; currentFilters.hasHandoffs = handoffsCheckbox.checked;
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
clearFiltersBtn?.addEventListener('click', () => { clearFiltersBtn?.addEventListener('click', () => {
@@ -172,8 +228,10 @@ export async function initAgentsPage(): Promise<void> {
if (searchInput) searchInput.value = ''; if (searchInput) searchInput.value = '';
if (sortSelect) sortSelect.value = 'title'; if (sortSelect) sortSelect.value = 'title';
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
applyFiltersAndRender();
setupModal(); setupModal();
setupDropdownCloseHandlers(); setupDropdownCloseHandlers();
setupActionHandlers(); setupActionHandlers();

View File

@@ -1,13 +1,21 @@
/** /**
* Hooks page functionality * Hooks page functionality
*/ */
import { createChoices, getChoicesValues, type Choices } from "../choices"; import {
createChoices,
getChoicesValues,
setChoicesValues,
type Choices,
} from "../choices";
import { FuzzySearch, type SearchItem } from "../search"; import { FuzzySearch, type SearchItem } from "../search";
import { import {
fetchData, fetchData,
debounce, debounce,
getQueryParam,
getQueryParamValues,
showToast, showToast,
downloadZipBundle, downloadZipBundle,
updateQueryParams,
} from "../utils"; } from "../utils";
import { setupModal, openFileModal } from "../modal"; import { setupModal, openFileModal } from "../modal";
import { import {
@@ -126,6 +134,15 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
resourceListHandlersReady = true; resourceListHandlersReady = true;
} }
function syncUrlState(searchInput: HTMLInputElement | null): void {
updateQueryParams({
q: searchInput?.value ?? "",
hook: currentFilters.hooks,
tag: currentFilters.tags,
sort: currentSort === "title" ? "" : currentSort,
});
}
async function downloadHook( async function downloadHook(
hookId: string, hookId: string,
btn: HTMLButtonElement btn: HTMLButtonElement
@@ -210,9 +227,26 @@ export async function initHooksPage(): Promise<void> {
"label", "label",
true true
); );
const initialQuery = getQueryParam("q");
const initialHooks = getQueryParamValues("hook").filter((hook) =>
data.filters.hooks.includes(hook)
);
const initialTags = getQueryParamValues("tag").filter((tag) =>
data.filters.tags.includes(tag)
);
const initialSort = getQueryParam("sort");
if (searchInput) searchInput.value = initialQuery;
if (initialHooks.length > 0) {
currentFilters.hooks = initialHooks;
setChoicesValues(hookSelect, initialHooks);
}
document.getElementById("filter-hook")?.addEventListener("change", () => { document.getElementById("filter-hook")?.addEventListener("change", () => {
currentFilters.hooks = getChoicesValues(hookSelect); currentFilters.hooks = getChoicesValues(hookSelect);
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
// Setup tag filter // Setup tag filter
@@ -225,20 +259,33 @@ export async function initHooksPage(): Promise<void> {
"label", "label",
true true
); );
if (initialTags.length > 0) {
currentFilters.tags = initialTags;
setChoicesValues(tagSelect, initialTags);
}
document.getElementById("filter-tag")?.addEventListener("change", () => { document.getElementById("filter-tag")?.addEventListener("change", () => {
currentFilters.tags = getChoicesValues(tagSelect); currentFilters.tags = getChoicesValues(tagSelect);
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
if (initialSort === "lastUpdated") {
currentSort = initialSort;
if (sortSelect) sortSelect.value = initialSort;
}
sortSelect?.addEventListener("change", () => { sortSelect?.addEventListener("change", () => {
currentSort = sortSelect.value as HookSortOption; currentSort = sortSelect.value as HookSortOption;
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
applyFiltersAndRender(); applyFiltersAndRender();
searchInput?.addEventListener( searchInput?.addEventListener(
"input", "input",
debounce(() => applyFiltersAndRender(), 200) debounce(() => {
applyFiltersAndRender();
syncUrlState(searchInput);
}, 200)
); );
clearFiltersBtn?.addEventListener("click", () => { clearFiltersBtn?.addEventListener("click", () => {
@@ -249,6 +296,7 @@ export async function initHooksPage(): Promise<void> {
if (searchInput) searchInput.value = ""; if (searchInput) searchInput.value = "";
if (sortSelect) sortSelect.value = "title"; if (sortSelect) sortSelect.value = "title";
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
setupModal(); setupModal();

View File

@@ -1,9 +1,22 @@
/** /**
* Instructions page functionality * Instructions page functionality
*/ */
import { createChoices, getChoicesValues, type Choices } from '../choices'; import {
createChoices,
getChoicesValues,
setChoicesValues,
type Choices,
} from '../choices';
import { FuzzySearch, type SearchItem } from '../search'; import { FuzzySearch, type SearchItem } from '../search';
import { fetchData, debounce, setupDropdownCloseHandlers, setupActionHandlers } from '../utils'; import {
fetchData,
debounce,
getQueryParam,
getQueryParamValues,
setupDropdownCloseHandlers,
setupActionHandlers,
updateQueryParams,
} from '../utils';
import { setupModal, openFileModal } from '../modal'; import { setupModal, openFileModal } from '../modal';
import { import {
renderInstructionsHtml, renderInstructionsHtml,
@@ -93,6 +106,14 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
resourceListHandlersReady = true; resourceListHandlersReady = true;
} }
function syncUrlState(searchInput: HTMLInputElement | null): void {
updateQueryParams({
q: searchInput?.value ?? '',
extension: currentFilters.extensions,
sort: currentSort === 'title' ? '' : currentSort,
});
}
export async function initInstructionsPage(): Promise<void> { export async function initInstructionsPage(): Promise<void> {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
const searchInput = document.getElementById('search-input') as HTMLInputElement; const searchInput = document.getElementById('search-input') as HTMLInputElement;
@@ -112,14 +133,31 @@ export async function initInstructionsPage(): Promise<void> {
extensionSelect = createChoices('#filter-extension', { placeholderValue: 'All Extensions' }); extensionSelect = createChoices('#filter-extension', { placeholderValue: 'All Extensions' });
extensionSelect.setChoices(data.filters.extensions.map(e => ({ value: e, label: e })), 'value', 'label', true); extensionSelect.setChoices(data.filters.extensions.map(e => ({ value: e, label: e })), 'value', 'label', true);
const initialQuery = getQueryParam('q');
const initialExtensions = getQueryParamValues('extension').filter(extension => data.filters.extensions.includes(extension));
const initialSort = getQueryParam('sort');
if (searchInput) searchInput.value = initialQuery;
if (initialExtensions.length > 0) {
currentFilters.extensions = initialExtensions;
setChoicesValues(extensionSelect, initialExtensions);
}
if (initialSort === 'lastUpdated') {
currentSort = initialSort;
if (sortSelect) sortSelect.value = initialSort;
}
document.getElementById('filter-extension')?.addEventListener('change', () => { document.getElementById('filter-extension')?.addEventListener('change', () => {
currentFilters.extensions = getChoicesValues(extensionSelect); currentFilters.extensions = getChoicesValues(extensionSelect);
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
sortSelect?.addEventListener('change', () => { sortSelect?.addEventListener('change', () => {
currentSort = sortSelect.value as InstructionSortOption; currentSort = sortSelect.value as InstructionSortOption;
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
const countEl = document.getElementById('results-count'); const countEl = document.getElementById('results-count');
@@ -127,7 +165,10 @@ export async function initInstructionsPage(): Promise<void> {
countEl.textContent = `${allItems.length} of ${allItems.length} instructions`; countEl.textContent = `${allItems.length} of ${allItems.length} instructions`;
} }
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200)); searchInput?.addEventListener('input', debounce(() => {
applyFiltersAndRender();
syncUrlState(searchInput);
}, 200));
clearFiltersBtn?.addEventListener('click', () => { clearFiltersBtn?.addEventListener('click', () => {
currentFilters = { extensions: [] }; currentFilters = { extensions: [] };
@@ -136,8 +177,10 @@ export async function initInstructionsPage(): Promise<void> {
if (searchInput) searchInput.value = ''; if (searchInput) searchInput.value = '';
if (sortSelect) sortSelect.value = 'title'; if (sortSelect) sortSelect.value = 'title';
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
applyFiltersAndRender();
setupModal(); setupModal();
setupDropdownCloseHandlers(); setupDropdownCloseHandlers();
setupActionHandlers(); setupActionHandlers();

View File

@@ -1,9 +1,20 @@
/** /**
* Plugins page functionality * Plugins page functionality
*/ */
import { createChoices, getChoicesValues, type Choices } from '../choices'; import {
createChoices,
getChoicesValues,
setChoicesValues,
type Choices,
} from '../choices';
import { FuzzySearch, type SearchItem } from '../search'; import { FuzzySearch, type SearchItem } from '../search';
import { fetchData, debounce } from '../utils'; import {
fetchData,
debounce,
getQueryParam,
getQueryParamValues,
updateQueryParams,
} from '../utils';
import { setupModal, openFileModal } from '../modal'; import { setupModal, openFileModal } from '../modal';
import { renderPluginsHtml, type RenderablePlugin } from './plugins-render'; import { renderPluginsHtml, type RenderablePlugin } from './plugins-render';
@@ -98,6 +109,13 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
resourceListHandlersReady = true; resourceListHandlersReady = true;
} }
function syncUrlState(searchInput: HTMLInputElement | null): void {
updateQueryParams({
q: searchInput?.value ?? '',
tag: currentFilters.tags,
});
}
export async function initPluginsPage(): Promise<void> { export async function initPluginsPage(): Promise<void> {
const list = document.getElementById('resource-list'); const list = document.getElementById('resource-list');
const searchInput = document.getElementById('search-input') as HTMLInputElement; const searchInput = document.getElementById('search-input') as HTMLInputElement;
@@ -123,9 +141,20 @@ export async function initPluginsPage(): Promise<void> {
tagSelect = createChoices('#filter-tag', { placeholderValue: 'All Tags' }); tagSelect = createChoices('#filter-tag', { placeholderValue: 'All Tags' });
tagSelect.setChoices(data.filters.tags.map(t => ({ value: t, label: t })), 'value', 'label', true); tagSelect.setChoices(data.filters.tags.map(t => ({ value: t, label: t })), 'value', 'label', true);
const initialQuery = getQueryParam('q');
const initialTags = getQueryParamValues('tag').filter(tag => data.filters.tags.includes(tag));
if (searchInput) searchInput.value = initialQuery;
if (initialTags.length > 0) {
currentFilters.tags = initialTags;
setChoicesValues(tagSelect, initialTags);
}
document.getElementById('filter-tag')?.addEventListener('change', () => { document.getElementById('filter-tag')?.addEventListener('change', () => {
currentFilters.tags = getChoicesValues(tagSelect); currentFilters.tags = getChoicesValues(tagSelect);
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
const countEl = document.getElementById('results-count'); const countEl = document.getElementById('results-count');
@@ -133,15 +162,20 @@ export async function initPluginsPage(): Promise<void> {
countEl.textContent = `${allItems.length} of ${allItems.length} plugins`; countEl.textContent = `${allItems.length} of ${allItems.length} plugins`;
} }
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200)); searchInput?.addEventListener('input', debounce(() => {
applyFiltersAndRender();
syncUrlState(searchInput);
}, 200));
clearFiltersBtn?.addEventListener('click', () => { clearFiltersBtn?.addEventListener('click', () => {
currentFilters = { tags: [] }; currentFilters = { tags: [] };
tagSelect.removeActiveItems(); tagSelect.removeActiveItems();
if (searchInput) searchInput.value = ''; if (searchInput) searchInput.value = '';
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
applyFiltersAndRender();
setupModal(); setupModal();
} }

View File

@@ -1,13 +1,22 @@
/** /**
* Skills page functionality * Skills page functionality
*/ */
import { createChoices, getChoicesValues, type Choices } from "../choices"; import {
createChoices,
getChoicesValues,
setChoicesValues,
type Choices,
} from "../choices";
import { FuzzySearch, type SearchItem } from "../search"; import { FuzzySearch, type SearchItem } from "../search";
import { import {
fetchData, fetchData,
debounce, debounce,
getQueryParam,
getQueryParamFlag,
getQueryParamValues,
showToast, showToast,
downloadZipBundle, downloadZipBundle,
updateQueryParams,
} from "../utils"; } from "../utils";
import { setupModal, openFileModal } from "../modal"; import { setupModal, openFileModal } from "../modal";
import { import {
@@ -120,6 +129,15 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
resourceListHandlersReady = true; resourceListHandlersReady = true;
} }
function syncUrlState(searchInput: HTMLInputElement | null): void {
updateQueryParams({
q: searchInput?.value ?? "",
category: currentFilters.categories,
hasAssets: currentFilters.hasAssets,
sort: currentSort === "title" ? "" : currentSort,
});
}
async function downloadSkill( async function downloadSkill(
skillId: string, skillId: string,
btn: HTMLButtonElement btn: HTMLButtonElement
@@ -189,25 +207,52 @@ export async function initSkillsPage(): Promise<void> {
"label", "label",
true true
); );
const initialQuery = getQueryParam("q");
const initialCategories = getQueryParamValues("category").filter((category) =>
data.filters.categories.includes(category)
);
const initialSort = getQueryParam("sort");
if (searchInput) searchInput.value = initialQuery;
if (initialCategories.length > 0) {
currentFilters.categories = initialCategories;
setChoicesValues(categorySelect, initialCategories);
}
if (getQueryParamFlag("hasAssets")) {
currentFilters.hasAssets = true;
if (hasAssetsCheckbox) hasAssetsCheckbox.checked = true;
}
if (initialSort === "lastUpdated") {
currentSort = initialSort;
if (sortSelect) sortSelect.value = initialSort;
}
document.getElementById("filter-category")?.addEventListener("change", () => { document.getElementById("filter-category")?.addEventListener("change", () => {
currentFilters.categories = getChoicesValues(categorySelect); currentFilters.categories = getChoicesValues(categorySelect);
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
sortSelect?.addEventListener("change", () => { sortSelect?.addEventListener("change", () => {
currentSort = sortSelect.value as SkillSortOption; currentSort = sortSelect.value as SkillSortOption;
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
applyFiltersAndRender(); applyFiltersAndRender();
searchInput?.addEventListener( searchInput?.addEventListener(
"input", "input",
debounce(() => applyFiltersAndRender(), 200) debounce(() => {
applyFiltersAndRender();
syncUrlState(searchInput);
}, 200)
); );
hasAssetsCheckbox?.addEventListener("change", () => { hasAssetsCheckbox?.addEventListener("change", () => {
currentFilters.hasAssets = hasAssetsCheckbox.checked; currentFilters.hasAssets = hasAssetsCheckbox.checked;
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
clearFiltersBtn?.addEventListener("click", () => { clearFiltersBtn?.addEventListener("click", () => {
@@ -218,6 +263,7 @@ export async function initSkillsPage(): Promise<void> {
if (searchInput) searchInput.value = ""; if (searchInput) searchInput.value = "";
if (sortSelect) sortSelect.value = "title"; if (sortSelect) sortSelect.value = "title";
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
setupModal(); setupModal();

View File

@@ -2,7 +2,12 @@
* Tools page functionality * Tools page functionality
*/ */
import { FuzzySearch, type SearchableItem } from "../search"; import { FuzzySearch, type SearchableItem } from "../search";
import { fetchData, debounce } from "../utils"; import {
fetchData,
debounce,
getQueryParam,
updateQueryParams,
} from "../utils";
import { renderToolsHtml } from "./tools-render"; import { renderToolsHtml } from "./tools-render";
export interface Tool extends SearchableItem { export interface Tool extends SearchableItem {
@@ -84,6 +89,13 @@ function renderTools(tools: Tool[], query = ""): void {
}); });
} }
function syncUrlState(searchInput: HTMLInputElement | null): void {
updateQueryParams({
q: searchInput?.value ?? "",
category: currentFilters.categories,
});
}
function setupCopyConfigHandlers(): void { function setupCopyConfigHandlers(): void {
if (copyHandlersReady) return; if (copyHandlersReady) return;
@@ -157,18 +169,33 @@ export async function initToolsPage(): Promise<void> {
) )
.join(""); .join("");
const initialCategory = getQueryParam("category");
if (initialCategory && data.filters.categories.includes(initialCategory)) {
currentFilters.categories = [initialCategory];
categoryFilter.value = initialCategory;
}
categoryFilter.addEventListener("change", () => { categoryFilter.addEventListener("change", () => {
currentFilters.categories = categoryFilter.value currentFilters.categories = categoryFilter.value
? [categoryFilter.value] ? [categoryFilter.value]
: []; : [];
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
} }
const initialQuery = getQueryParam("q");
if (searchInput) searchInput.value = initialQuery;
applyFiltersAndRender();
// Search input handler // Search input handler
searchInput?.addEventListener( searchInput?.addEventListener(
"input", "input",
debounce(() => applyFiltersAndRender(), 200) debounce(() => {
applyFiltersAndRender();
syncUrlState(searchInput);
}, 200)
); );
// Clear filters // Clear filters
@@ -177,6 +204,7 @@ export async function initToolsPage(): Promise<void> {
if (categoryFilter) categoryFilter.value = ""; if (categoryFilter) categoryFilter.value = "";
if (searchInput) searchInput.value = ""; if (searchInput) searchInput.value = "";
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
setupCopyConfigHandlers(); setupCopyConfigHandlers();

View File

@@ -1,12 +1,20 @@
/** /**
* Workflows page functionality * Workflows page functionality
*/ */
import { createChoices, getChoicesValues, type Choices } from "../choices"; import {
createChoices,
getChoicesValues,
setChoicesValues,
type Choices,
} from "../choices";
import { FuzzySearch, type SearchItem } from "../search"; import { FuzzySearch, type SearchItem } from "../search";
import { import {
fetchData, fetchData,
debounce, debounce,
getQueryParam,
getQueryParamValues,
setupActionHandlers, setupActionHandlers,
updateQueryParams,
} from "../utils"; } from "../utils";
import { setupModal, openFileModal } from "../modal"; import { setupModal, openFileModal } from "../modal";
import { import {
@@ -106,6 +114,14 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
resourceListHandlersReady = true; resourceListHandlersReady = true;
} }
function syncUrlState(searchInput: HTMLInputElement | null): void {
updateQueryParams({
q: searchInput?.value ?? "",
trigger: currentFilters.triggers,
sort: currentSort === "title" ? "" : currentSort,
});
}
export async function initWorkflowsPage(): Promise<void> { export async function initWorkflowsPage(): Promise<void> {
const list = document.getElementById("resource-list"); const list = document.getElementById("resource-list");
const searchInput = document.getElementById( const searchInput = document.getElementById(
@@ -139,14 +155,33 @@ export async function initWorkflowsPage(): Promise<void> {
"label", "label",
true true
); );
const initialQuery = getQueryParam("q");
const initialTriggers = getQueryParamValues("trigger").filter((trigger) =>
data.filters.triggers.includes(trigger)
);
const initialSort = getQueryParam("sort");
if (searchInput) searchInput.value = initialQuery;
if (initialTriggers.length > 0) {
currentFilters.triggers = initialTriggers;
setChoicesValues(triggerSelect, initialTriggers);
}
if (initialSort === "lastUpdated") {
currentSort = initialSort;
if (sortSelect) sortSelect.value = initialSort;
}
document.getElementById("filter-trigger")?.addEventListener("change", () => { document.getElementById("filter-trigger")?.addEventListener("change", () => {
currentFilters.triggers = getChoicesValues(triggerSelect); currentFilters.triggers = getChoicesValues(triggerSelect);
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
sortSelect?.addEventListener("change", () => { sortSelect?.addEventListener("change", () => {
currentSort = sortSelect.value as WorkflowSortOption; currentSort = sortSelect.value as WorkflowSortOption;
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
const countEl = document.getElementById("results-count"); const countEl = document.getElementById("results-count");
@@ -156,7 +191,10 @@ export async function initWorkflowsPage(): Promise<void> {
searchInput?.addEventListener( searchInput?.addEventListener(
"input", "input",
debounce(() => applyFiltersAndRender(), 200) debounce(() => {
applyFiltersAndRender();
syncUrlState(searchInput);
}, 200)
); );
clearFiltersBtn?.addEventListener("click", () => { clearFiltersBtn?.addEventListener("click", () => {
@@ -166,8 +204,10 @@ export async function initWorkflowsPage(): Promise<void> {
if (searchInput) searchInput.value = ""; if (searchInput) searchInput.value = "";
if (sortSelect) sortSelect.value = "title"; if (sortSelect) sortSelect.value = "title";
applyFiltersAndRender(); applyFiltersAndRender();
syncUrlState(searchInput);
}); });
applyFiltersAndRender();
setupModal(); setupModal();
setupActionHandlers(); setupActionHandlers();
} }

View File

@@ -235,6 +235,83 @@ export async function shareFile(filePath: string): Promise<boolean> {
return copyToClipboard(deepLinkUrl); return copyToClipboard(deepLinkUrl);
} }
type QueryParamValue = string | string[] | boolean | null | undefined;
/**
* Read a single query parameter.
*/
export function getQueryParam(name: string): string {
if (typeof window === "undefined") return "";
return new URLSearchParams(window.location.search).get(name)?.trim() ?? "";
}
/**
* Read repeated query parameter values.
*/
export function getQueryParamValues(name: string): string[] {
if (typeof window === "undefined") return [];
const values = new URLSearchParams(window.location.search)
.getAll(name)
.map((value) => value.trim())
.filter(Boolean);
return Array.from(new Set(values));
}
/**
* Read a boolean-style query parameter.
*/
export function getQueryParamFlag(name: string): boolean {
const value = getQueryParam(name).toLowerCase();
return value === "1" || value === "true" || value === "yes";
}
/**
* Update query parameters while preserving the current hash.
*/
export function updateQueryParams(
updates: Record<string, QueryParamValue>
): void {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
for (const [key, value] of Object.entries(updates)) {
url.searchParams.delete(key);
if (Array.isArray(value)) {
for (const item of value) {
const normalized = item.trim();
if (normalized) {
url.searchParams.append(key, normalized);
}
}
continue;
}
if (typeof value === "boolean") {
if (value) {
url.searchParams.set(key, "1");
}
continue;
}
if (typeof value === "string") {
const normalized = value.trim();
if (normalized) {
url.searchParams.set(key, normalized);
}
}
}
const search = url.searchParams.toString();
const nextUrl = `${url.pathname}${search ? `?${search}` : ""}${url.hash}`;
const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
if (nextUrl !== currentUrl) {
history.replaceState(null, "", nextUrl);
}
}
/** /**
* Show a toast notification * Show a toast notification
*/ */