mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-11 02:35:55 +00:00
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:
@@ -12,6 +12,16 @@ export function getChoicesValues(choices: Choices): string[] {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
/**
|
||||
* 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 { fetchData, debounce, setupDropdownCloseHandlers, setupActionHandlers } from '../utils';
|
||||
import {
|
||||
fetchData,
|
||||
debounce,
|
||||
getQueryParam,
|
||||
getQueryParamFlag,
|
||||
getQueryParamValues,
|
||||
setupDropdownCloseHandlers,
|
||||
setupActionHandlers,
|
||||
updateQueryParams,
|
||||
} from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
import { renderAgentsHtml, sortAgents, type AgentSortOption, type RenderableAgent } from './agents-render';
|
||||
|
||||
@@ -111,6 +125,16 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
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> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
@@ -132,23 +156,46 @@ export async function initAgentsPage(): Promise<void> {
|
||||
// Initialize Choices.js for model filter
|
||||
modelSelect = createChoices('#filter-model', { placeholderValue: 'All Models' });
|
||||
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', () => {
|
||||
currentFilters.models = getChoicesValues(modelSelect);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
// Initialize Choices.js for tool filter
|
||||
toolSelect = createChoices('#filter-tool', { placeholderValue: 'All Tools' });
|
||||
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', () => {
|
||||
currentFilters.tools = getChoicesValues(toolSelect);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
// Initialize sort select
|
||||
if (initialSort === 'lastUpdated') {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as AgentSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
const countEl = document.getElementById('results-count');
|
||||
@@ -156,11 +203,20 @@ export async function initAgentsPage(): Promise<void> {
|
||||
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', () => {
|
||||
currentFilters.hasHandoffs = handoffsCheckbox.checked;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
@@ -172,8 +228,10 @@ export async function initAgentsPage(): Promise<void> {
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
setupModal();
|
||||
setupDropdownCloseHandlers();
|
||||
setupActionHandlers();
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
/**
|
||||
* 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 {
|
||||
fetchData,
|
||||
debounce,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
showToast,
|
||||
downloadZipBundle,
|
||||
updateQueryParams,
|
||||
} from "../utils";
|
||||
import { setupModal, openFileModal } from "../modal";
|
||||
import {
|
||||
@@ -126,6 +134,15 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
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(
|
||||
hookId: string,
|
||||
btn: HTMLButtonElement
|
||||
@@ -210,9 +227,26 @@ export async function initHooksPage(): Promise<void> {
|
||||
"label",
|
||||
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", () => {
|
||||
currentFilters.hooks = getChoicesValues(hookSelect);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
// Setup tag filter
|
||||
@@ -225,20 +259,33 @@ export async function initHooksPage(): Promise<void> {
|
||||
"label",
|
||||
true
|
||||
);
|
||||
if (initialTags.length > 0) {
|
||||
currentFilters.tags = initialTags;
|
||||
setChoicesValues(tagSelect, initialTags);
|
||||
}
|
||||
document.getElementById("filter-tag")?.addEventListener("change", () => {
|
||||
currentFilters.tags = getChoicesValues(tagSelect);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
if (initialSort === "lastUpdated") {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
sortSelect?.addEventListener("change", () => {
|
||||
currentSort = sortSelect.value as HookSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
debounce(() => applyFiltersAndRender(), 200)
|
||||
debounce(() => {
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
}, 200)
|
||||
);
|
||||
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
@@ -249,6 +296,7 @@ export async function initHooksPage(): Promise<void> {
|
||||
if (searchInput) searchInput.value = "";
|
||||
if (sortSelect) sortSelect.value = "title";
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
setupModal();
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
/**
|
||||
* 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 { fetchData, debounce, setupDropdownCloseHandlers, setupActionHandlers } from '../utils';
|
||||
import {
|
||||
fetchData,
|
||||
debounce,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
setupDropdownCloseHandlers,
|
||||
setupActionHandlers,
|
||||
updateQueryParams,
|
||||
} from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
import {
|
||||
renderInstructionsHtml,
|
||||
@@ -93,6 +106,14 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
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> {
|
||||
const list = document.getElementById('resource-list');
|
||||
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.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', () => {
|
||||
currentFilters.extensions = getChoicesValues(extensionSelect);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as InstructionSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
const countEl = document.getElementById('results-count');
|
||||
@@ -127,7 +165,10 @@ export async function initInstructionsPage(): Promise<void> {
|
||||
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', () => {
|
||||
currentFilters = { extensions: [] };
|
||||
@@ -136,8 +177,10 @@ export async function initInstructionsPage(): Promise<void> {
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
setupModal();
|
||||
setupDropdownCloseHandlers();
|
||||
setupActionHandlers();
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
/**
|
||||
* 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 { fetchData, debounce } from '../utils';
|
||||
import {
|
||||
fetchData,
|
||||
debounce,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
updateQueryParams,
|
||||
} from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
import { renderPluginsHtml, type RenderablePlugin } from './plugins-render';
|
||||
|
||||
@@ -98,6 +109,13 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
resourceListHandlersReady = true;
|
||||
}
|
||||
|
||||
function syncUrlState(searchInput: HTMLInputElement | null): void {
|
||||
updateQueryParams({
|
||||
q: searchInput?.value ?? '',
|
||||
tag: currentFilters.tags,
|
||||
});
|
||||
}
|
||||
|
||||
export async function initPluginsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
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.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', () => {
|
||||
currentFilters.tags = getChoicesValues(tagSelect);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
const countEl = document.getElementById('results-count');
|
||||
@@ -133,15 +162,20 @@ export async function initPluginsPage(): Promise<void> {
|
||||
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', () => {
|
||||
currentFilters = { tags: [] };
|
||||
tagSelect.removeActiveItems();
|
||||
if (searchInput) searchInput.value = '';
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
setupModal();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
/**
|
||||
* 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 {
|
||||
fetchData,
|
||||
debounce,
|
||||
getQueryParam,
|
||||
getQueryParamFlag,
|
||||
getQueryParamValues,
|
||||
showToast,
|
||||
downloadZipBundle,
|
||||
updateQueryParams,
|
||||
} from "../utils";
|
||||
import { setupModal, openFileModal } from "../modal";
|
||||
import {
|
||||
@@ -120,6 +129,15 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
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(
|
||||
skillId: string,
|
||||
btn: HTMLButtonElement
|
||||
@@ -189,25 +207,52 @@ export async function initSkillsPage(): Promise<void> {
|
||||
"label",
|
||||
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", () => {
|
||||
currentFilters.categories = getChoicesValues(categorySelect);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener("change", () => {
|
||||
currentSort = sortSelect.value as SkillSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
debounce(() => applyFiltersAndRender(), 200)
|
||||
debounce(() => {
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
}, 200)
|
||||
);
|
||||
|
||||
hasAssetsCheckbox?.addEventListener("change", () => {
|
||||
currentFilters.hasAssets = hasAssetsCheckbox.checked;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
@@ -218,6 +263,7 @@ export async function initSkillsPage(): Promise<void> {
|
||||
if (searchInput) searchInput.value = "";
|
||||
if (sortSelect) sortSelect.value = "title";
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
setupModal();
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
* Tools page functionality
|
||||
*/
|
||||
import { FuzzySearch, type SearchableItem } from "../search";
|
||||
import { fetchData, debounce } from "../utils";
|
||||
import {
|
||||
fetchData,
|
||||
debounce,
|
||||
getQueryParam,
|
||||
updateQueryParams,
|
||||
} from "../utils";
|
||||
import { renderToolsHtml } from "./tools-render";
|
||||
|
||||
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 {
|
||||
if (copyHandlersReady) return;
|
||||
|
||||
@@ -157,18 +169,33 @@ export async function initToolsPage(): Promise<void> {
|
||||
)
|
||||
.join("");
|
||||
|
||||
const initialCategory = getQueryParam("category");
|
||||
if (initialCategory && data.filters.categories.includes(initialCategory)) {
|
||||
currentFilters.categories = [initialCategory];
|
||||
categoryFilter.value = initialCategory;
|
||||
}
|
||||
|
||||
categoryFilter.addEventListener("change", () => {
|
||||
currentFilters.categories = categoryFilter.value
|
||||
? [categoryFilter.value]
|
||||
: [];
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
}
|
||||
|
||||
const initialQuery = getQueryParam("q");
|
||||
if (searchInput) searchInput.value = initialQuery;
|
||||
|
||||
applyFiltersAndRender();
|
||||
|
||||
// Search input handler
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
debounce(() => applyFiltersAndRender(), 200)
|
||||
debounce(() => {
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
}, 200)
|
||||
);
|
||||
|
||||
// Clear filters
|
||||
@@ -177,6 +204,7 @@ export async function initToolsPage(): Promise<void> {
|
||||
if (categoryFilter) categoryFilter.value = "";
|
||||
if (searchInput) searchInput.value = "";
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
setupCopyConfigHandlers();
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
/**
|
||||
* 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 {
|
||||
fetchData,
|
||||
debounce,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
setupActionHandlers,
|
||||
updateQueryParams,
|
||||
} from "../utils";
|
||||
import { setupModal, openFileModal } from "../modal";
|
||||
import {
|
||||
@@ -106,6 +114,14 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
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> {
|
||||
const list = document.getElementById("resource-list");
|
||||
const searchInput = document.getElementById(
|
||||
@@ -139,14 +155,33 @@ export async function initWorkflowsPage(): Promise<void> {
|
||||
"label",
|
||||
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", () => {
|
||||
currentFilters.triggers = getChoicesValues(triggerSelect);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener("change", () => {
|
||||
currentSort = sortSelect.value as WorkflowSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
const countEl = document.getElementById("results-count");
|
||||
@@ -156,7 +191,10 @@ export async function initWorkflowsPage(): Promise<void> {
|
||||
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
debounce(() => applyFiltersAndRender(), 200)
|
||||
debounce(() => {
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
}, 200)
|
||||
);
|
||||
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
@@ -166,8 +204,10 @@ export async function initWorkflowsPage(): Promise<void> {
|
||||
if (searchInput) searchInput.value = "";
|
||||
if (sortSelect) sortSelect.value = "title";
|
||||
applyFiltersAndRender();
|
||||
syncUrlState(searchInput);
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
setupModal();
|
||||
setupActionHandlers();
|
||||
}
|
||||
|
||||
@@ -235,6 +235,83 @@ export async function shareFile(filePath: string): Promise<boolean> {
|
||||
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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user