@graphql-workbench/query-list
v0.1.0
Published
A web component that lists GraphQL queries with status and row actions
Readme
@graphql-workbench/query-list
<gwb-query-list> is a framework-agnostic web component that lists GraphQL queries. Each
row shows the query name, an optional status icon, and consumer-defined actions.
Installation
npm install @graphql-workbench/query-list@preact/signals-core is a peer dependency.
Usage
<gwb-query-list id="queries" label="Saved queries"></gwb-query-list>
<script type="module">
import '@graphql-workbench/query-list';
const list = document.getElementById('queries');
list.queries = [
{ id: 'get-user', name: 'GetUser', status: 'complete' },
{ id: 'list-repos', name: 'ListRepositories', status: 'running' },
{ id: 'create-issue', name: 'CreateIssue', status: 'error' },
{ id: 'update-profile', name: 'UpdateProfile' },
];
list.actions = [
{ id: 'run', label: 'Run', icon: 'M8 5v14l11-7z' },
{ id: 'delete', label: 'Delete' },
];
list.selectedId = 'get-user';
list.addEventListener('gwb-query-select', ({ detail }) => {
console.log('open', detail.id);
});
list.addEventListener('gwb-query-action', ({ detail }) => {
console.log(detail.actionId, detail.queryId);
});
</script>The package also publishes a standalone UMD bundle, dist/query-list.umd.js, which
registers the element and exposes a QueryList browser global.
Properties
| Property | Attribute | Description |
| ------------ | ------------- | --------------------------------------------------------------- |
| queries | none | The queries to list. Assign a new array to update them. |
| actions | none | The row actions, in toolbar order. |
| selectedId | selected-id | The selected query id, or null. Reflected. |
| theme | theme | 'auto' (default, follows the system), 'light', or 'dark'. |
| label | label | Accessible name of the list. Defaults to "Queries". |
| store | none | An optional shared store; see Store. |
Each query needs a unique string id and a name. Its optional status is
'running', 'error', or 'complete'; omit it or use null for no icon. Statuses are
announced to screen readers as text as well as shown as icons.
Assigning selectedId selects a row without emitting an event. It may name a query that
hasn't loaded yet; that row is selected once it appears.
Row actions
Each action has an id, a label, and an optional icon given as SVG path data in a
24×24 viewBox, such as a path from Material Symbols. Actions with an icon show it in
currentColor; actions without one show their label. Every button is named and titled
" ", for example "Run GetUser".
By default every row offers every action. Give a query an actions array of action ids
to limit its row, or an empty array for none:
list.queries = [
{ id: 'draft', name: 'Draft' },
{ id: 'shared', name: 'SharedQuery', actions: ['run'] },
];The actions appear in a toolbar over the hovered row, or over the selected row while
the list has keyboard focus. Activating one emits gwb-query-action; it does not
change the selection. The component never performs the action itself. If the action
removes the row, for example a delete, focus returns to the list.
Events
| Event | Detail | Emitted when |
| ------------------ | ----------------------- | --------------------------------------------------------- |
| gwb-query-select | { id } | The user selects a row with the pointer or keyboard. |
| gwb-query-action | { queryId, actionId } | The user activates a row action with the pointer or keys. |
Both events bubble and cross shadow DOM boundaries. Programmatic changes never emit them.
Keyboard
| Key | In the list | In the actions toolbar | | ------------- | -------------------------------------------------- | ------------------------------------------- | | Up / Down | Select the previous or next query, wrapping around | | | Home / End | Select the first or last query | | | Tab | Move to the selected query's first action, if any | Next action; leaves the list after the last | | Shift+Tab | Leave the list | Previous action, or back to the list | | Escape | | Back to the list | | Enter / Space | | Activate the focused action |
Selection follows the keyboard, so each move emits gwb-query-select.
Theming
Set CSS custom properties on the element:
gwb-query-list {
--gwb-ql-height: 320px;
--gwb-ql-font-size: 14px;
--gwb-ql-selected-bg: #0058d0;
}The list grows with its content until --gwb-ql-height bounds it, and then it scrolls.
- Layout and typography:
--gwb-ql-height,--gwb-ql-font-family,--gwb-ql-font-size - Colors:
--gwb-ql-bg,--gwb-ql-text-primary,--gwb-ql-text-secondary,--gwb-ql-border,--gwb-ql-selected-bg,--gwb-ql-selected-text,--gwb-ql-hover-bg,--gwb-ql-focus-ring - Status colors:
--gwb-ql-status-running,--gwb-ql-status-error,--gwb-ql-status-complete
Parts: list, row, row-selected, status, status-running, status-error,
status-complete, name, empty, actions, action, and action-icon.
Store
createQueryListStore() holds the list state in
Preact signals. Assign one to the store property
to share state between several lists. Import it from
@graphql-workbench/query-list/store to use it without loading the web component, for
example in Node or in a coordinator.
import { createQueryListStore } from '@graphql-workbench/query-list/store';
const store = createQueryListStore();
store.actions.value = [
{ id: 'run', label: 'Run', icon: 'M8 5v14l11-7z' },
{ id: 'delete', label: 'Delete' },
];
store.queries.value = [
{ id: 'get-user', name: 'GetUser', status: 'complete' },
{ id: 'list-repos', name: 'ListRepositories', status: 'running', actions: ['delete'] },
];
store.events.subscribe('selection-change', ({ id }) => {
console.log('selected', id);
});
store.events.subscribe('action-requested', ({ queryId, actionId }) => {
console.log(actionId, queryId);
});
store.select('get-user');
store.requestAction('get-user', 'run');queriesis consumer-owned. Items are never mutated; assign a new array to update them. Each item needs a unique stringidand aname. When ids repeat, the first item wins.statusis'running','error','complete', or omitted for no icon.actionslists every row action. An item's optionalactionsarray of action ids limits which actions its row offers.iconis SVG path data in a 24×24 viewBox.rowsandselectedQueryare read-only signals derived from the inputs.select(id)selects a loaded query, or clears the selection withnull. It returns whether the selection changed. Pass{ emitEvents: false }when applying a change you have already observed.requestAction(queryId, actionId)emitsaction-requestedwhen the action is available on that row, and returnsfalseotherwise.- Assigning a signal directly, such as
store.selectedId.value = 'get-user', never emits an event. AselectedIdfor a query that hasn't loaded yet is kept until it arrives. - Pass
{ theme }with a shared signal to coordinate the theme with other stores. Calldispose()when the owner is finished.
An assigned store is authoritative: the element's properties read and write it, and every list bound to it renders the same state. It stays owned by you; removing it from the element or disconnecting the element never disposes it. Store actions you call directly update every bound list without emitting DOM events. A user interaction emits its store event once and its DOM event once, from the list that handled it. Without an assigned store, each element creates its own.
Snapshots
store.snapshot() returns detached, deeply frozen JSON state: the queries (id,
name, and per-row actions), selectedId, and theme. Runtime status and the
list-level action definitions are not included. parseQueryListStoreSnapshot(value)
validates a snapshot without changing anything, and store.restore(value) validates and
then applies it in one batch without emitting events.
Showing execution status
The list does not depend on @graphql-workbench/execute-query, but its statuses map
onto that component's execution states. Subscribe to the execution store's status
signal and update the query you are executing:
import type { ExecuteQueryStore, ExecutionStatus } from '@graphql-workbench/execute-query';
import type { QueryList, QueryListStatus } from '@graphql-workbench/query-list';
function listStatus(status: ExecutionStatus): QueryListStatus | null {
if (status === 'waiting' || status === 'streaming') return 'running';
if (status === 'complete' || status === 'error') return status;
return null; // idle and cancelled show no icon
}
function trackExecution(list: QueryList, execution: ExecuteQueryStore, queryId: string) {
return execution.status.subscribe((status) => {
list.queries = list.queries.map((query) =>
query.id === queryId ? { ...query, status: listStatus(status) } : query,
);
});
}trackExecution() returns the unsubscribe function; call it before tracking another
query.
License
MIT
