Query Builder - User guide¶
The expression and its tree¶
import { parseExpression, serializeExpression, countConditions, sameExpression, arityOf } from 'vue-fastedgy';
const tree = parseExpression(['|', ['name', '=', 'Novel'], ['quantity', '=', 0]]);
serializeExpression(tree); // ['|', [['name', '=', 'Novel'], ['quantity', '=', 0]]]
countConditions(tree); // 2
sameExpression([['a', '=', 1]], ['a', '=', 1]); // true
arityOf('between'); // 'two'
The root of a tree is always a group. A node is a group (joint, children), a rule (field, operator, value), an any block (field, negated, its own group) or an opaque item the reader did not recognise. Writing leaves out the incomplete rules and the groups they empty, writes a group of one item as that item, and only writes [joint, [items]].
| Arity | Operators | Value |
|---|---|---|
none | is empty, is not empty, is true, is false | absent |
one | everything else | a scalar |
two | between | [start, end] |
list | in, not in | a non-empty array |
sub | any, not any | a sub-expression or null |
Editing a tree¶
import { useQueryExpression } from 'vue-fastedgy';
const query = useQueryExpression(() => props.expression);
const root = query.tree.value.id;
const group = query.addGroup(root, '|');
query.addRule(group, { field: 'plan', operator: 'in', value: ['plus'] });
query.addAny(root, 'workspace_users');
query.update(ruleId, { operator: '!=' });
query.setJoint(root, '|');
query.remove(ruleId);
emit('update:expression', query.expression.value);
The tree is read again when the expression it is given changes meaning, never when it comes back saying what the tree just wrote: the row being typed stays.
Fields¶
import { filterableFields, resolveFieldPath, fieldOperators, relationKindOf } from 'vue-fastedgy';
const metadatas = await useMetadataStore().getMetadatas();
filterableFields(metadatas.household, { exclude: ['invitation_code'] });
resolveFieldPath(metadatas, 'household', 'workspace_users.user.email'); // chain, field, model
resolveFieldPath(metadatas, 'household', 'owner.id').path; // 'owner'
fieldOperators(metadatas, metadatas.household.fields.owner); // the relation's and its key's
id, search_value, created_by, updated_by and sequence are not offered, nor the computed, binary, spatial and fulltext fields.
The list¶
useDataIterator (and useDataTable, useDataGrid) send [restrictive, filter, expression, quick filters, search], leaving out the parts that are empty:
| Part | Set by |
|---|---|
| restrictive | the filter option, a rule, a list of rules or a function returning them: always applied |
filter | the screen's own controls: the free filter, a ref the list returns |
expression | the query builder |
| quick filters | the values of the quick filters, through the rules they stand for |
| search | the search text, 300 ms after it last changed |
const list = useDataIterator('support_ticket', { filter: ['contact', '=', contactId] });
list.filter.value = ['priority', '=', 'high']; // a select of the screen
The option and the ref share a name, not a role: the screen never lets go of the first, its controls set and clear the second. combinedFilter holds the whole, the filter a read and an export send.
| State | Kept in the URL as |
|---|---|
currentPage | p, from the second page on |
pageSize | s, read back when it is one of availablePageSizes |
orderBy | order_by (name:asc,created_at:desc), left out while it is defaultOrderBy |
expression | f |
view | cv |
quick | qf, the quick filters away from their default, as JSON |
search | q |
the scroll of scrollTarget | sl, restored after the first read |
The free filter is not kept in the URL. A list that appends its pages (append: true), entered on p, reads every page up to it in one request, so the scroll it restores lands on its rows.
A list drawn inside another screen, such as a tab of a record shown over a list, leaves the URL to that screen with url: false: it reads nothing from it and writes nothing to it.
Two lists of one screen keep their state apart with a prefix: url: { prefix: 'done_' } reads and writes every key of the list after it (done_p, done_q, done_f…, the keys of views.state included). The keys of every list of a page are written together, in one replace of the query.
const open = useDataIterator('task', { filter: ['done_at', 'is empty'] });
const done = useDataIterator('task', { filter: ['done_at', 'is not empty'], url: { prefix: 'done_' } });
The URL is followed: when it changes from outside (back, forward, a link to the same screen), the list holds what it says, as a list entered on it would (the view a new cv names read and opened, a size the URL leaves out kept), then reads once. Its own writes, late ones included, are not taken for a change.
searchFields makes the search an OR of icontains on those fields rather than a fulltext match:
toggleSort(field) sorts on a column ascending, then descending, then goes back to defaultOrderBy (or turns it around, when the default order is on that very column). A term written without a direction (name) is ascending, as the server reads it, in the option, in order_by and for getSortDirection.
loadMore() adds the next page to the rows, whether the list appends or pages, and refresh() reads again every page the rows hold, in one request, without going back to the first one.
A list sortable by hand (sortable, or the sortable of its metadata) sends the rank of its first row with the order, so that reordering a later page keeps its ranks, and takes the group its rows are moved to: resequence(ids, { groupField: 'status', groupValue: 3 }). The order goes to datasetPrefix, else where the api model answers, else under the prefix option; an application serving its dataset routes at the root under prefixed models passes datasetPrefix: ''. Its isSortable is false while a search, an expression, a quick filter or the free filter narrows the list: numbering the rows left on screen would mix their ranks with the others'. The restrictive filter fixes the list, and does not count; nor does a quick filter that keeps the order (keepsOrder: true), whose rows are ordered among themselves, as a list of « my tasks » is.
In the all mode of the selection (selection.all), every record of the filter is selected but those unchecked one by one, in selection.excluded: an action on the selection sends combinedFilter with ['id', 'not in', selection.excluded]. The selection is emptied when the filter changes, and kept from one page to another.
importData(file, { delimiter: ';' }) sends the delimiter of a CSV, which the server detects when it is left out.
A screen kept from one workspace to the next keeps its list: on the switch the workspace store announces, the list starts over as one opened in the new workspace, its state back to the opening, the metadata of that workspace waited for (and the manual order and the columns of a data table resolved again from it), its opening view read there, then one read.
Custom views¶
import { useCustomViews } from 'vue-fastedgy';
const list = useDataIterator('household', { prefix: '/console', views: { scope: '' } });
const views = useCustomViews('household', { prefix: '/console', list });
await views.ensure(); // when the menu opens
views.apply(view);
views.modified.value; // the list moved away from the current view
await views.create({ name: 'Premium', shared: true });
await views.save(views.current.value);
await views.setDefault(view, true); // for everyone
await views.setFavorite(view, true); // for the current user
With views, the iterator opens on the favorite of the user, else the one of everyone, and reads its first page once, already on it. A URL saying what the list shows (a link, a reload) wins over it. useOpeningView reads that view alone, for a list held otherwise. The views are read under views.prefix, else under the prefix of the api model the list was given, else under its prefix option.
A list holding more than its filter and its order names it in views.state, by field of the view: the view applies it on opening and from the menu, and saves it. Its key is where the application keeps it in the URL, which then wins over the view, and opened tells when the opening is done:
const groupBy = ref(null);
const list = useDataIterator('flow', {
views: {
state: { group_by: { get: () => groupBy.value, set: (value) => (groupBy.value = value), key: 'group' } },
},
});
The methods reject what the server refuses (a name already taken, a shared view the user does not manage): showing it is the interface's business.
Quick filters¶
A quick filter is a value the screen shows in a control of its own (a switch, a select, a date picker), and the rule that value stands for. defineQuickFilter makes one out of any component speaking v-model; the list reads its definition before the first page:
import { defineQuickFilter, useDataIterator } from 'vue-fastedgy';
const ClosedTickets = defineQuickFilter(
{ name: 'closed', default: false, filter: (shown) => (shown ? null : ['status', '=', 'opened']) },
SwitchField,
() => ({ label: t('Closed tickets') })
);
const list = useDataIterator('support_ticket', { quickFilters: [ClosedTickets] });
list.quick.closed; // the value applied: a boolean here, a number, a list…
The list keeps in the URL as qf the quick filters away from their default, and adds their rules to the filter it sends, after the expression and before the search. A quick filter defined with keepsOrder: true leaves the manual order on: the rows it leaves are ordered among themselves (« my tasks »), where any other narrowing turns it off. The query filter draws the quick filters of its list before its button (QueryFilterQuickFilters).
The list a record came from¶
import { listContext, useRecordContext } from 'vue-fastedgy';
router.push({ name: 'ticket', params: { id }, query: listContext(list) });
const { previous, next, go, inList } = useRecordContext(ticketApi, { routeName: 'ticket' });
The model enables the server half with @api_route_model(siblings=True).