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] : []);
|
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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user