@dxos/react-ui-query 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +5 -0
  3. package/dist/lib/index.mjs +637 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/types/src/components/QueryEditor/QueryEditor.d.ts +37 -0
  6. package/dist/types/src/components/QueryEditor/QueryEditor.d.ts.map +1 -0
  7. package/dist/types/src/components/QueryEditor/QueryEditor.stories.d.ts +42 -0
  8. package/dist/types/src/components/QueryEditor/QueryEditor.stories.d.ts.map +1 -0
  9. package/dist/types/src/components/QueryEditor/autocomplete.d.ts +8 -0
  10. package/dist/types/src/components/QueryEditor/autocomplete.d.ts.map +1 -0
  11. package/dist/types/src/components/QueryEditor/autocomplete.test.d.ts +2 -0
  12. package/dist/types/src/components/QueryEditor/autocomplete.test.d.ts.map +1 -0
  13. package/dist/types/src/components/QueryEditor/index.d.ts +3 -0
  14. package/dist/types/src/components/QueryEditor/index.d.ts.map +1 -0
  15. package/dist/types/src/components/QueryEditor/query-extension.d.ts +28 -0
  16. package/dist/types/src/components/QueryEditor/query-extension.d.ts.map +1 -0
  17. package/dist/types/src/components/QueryEditor/query-extension.test.d.ts +2 -0
  18. package/dist/types/src/components/QueryEditor/query-extension.test.d.ts.map +1 -0
  19. package/dist/types/src/components/QueryForm/Picker.d.ts +15 -0
  20. package/dist/types/src/components/QueryForm/Picker.d.ts.map +1 -0
  21. package/dist/types/src/components/QueryForm/QueryForm.d.ts +15 -0
  22. package/dist/types/src/components/QueryForm/QueryForm.d.ts.map +1 -0
  23. package/dist/types/src/components/QueryForm/QueryForm.stories.d.ts +26 -0
  24. package/dist/types/src/components/QueryForm/QueryForm.stories.d.ts.map +1 -0
  25. package/dist/types/src/components/QueryForm/index.d.ts +2 -0
  26. package/dist/types/src/components/QueryForm/index.d.ts.map +1 -0
  27. package/dist/types/src/components/QueryForm/query.d.ts +6 -0
  28. package/dist/types/src/components/QueryForm/query.d.ts.map +1 -0
  29. package/dist/types/src/components/index.d.ts +3 -0
  30. package/dist/types/src/components/index.d.ts.map +1 -0
  31. package/dist/types/src/hooks/index.d.ts +2 -0
  32. package/dist/types/src/hooks/index.d.ts.map +1 -0
  33. package/dist/types/src/hooks/useQueryBuilder.d.ts +6 -0
  34. package/dist/types/src/hooks/useQueryBuilder.d.ts.map +1 -0
  35. package/dist/types/src/index.d.ts +3 -0
  36. package/dist/types/src/index.d.ts.map +1 -0
  37. package/dist/types/src/translations.d.ts +13 -0
  38. package/dist/types/src/translations.d.ts.map +1 -0
  39. package/dist/types/tsconfig.tsbuildinfo +1 -0
  40. package/package.json +78 -0
  41. package/src/components/QueryEditor/QueryEditor.stories.tsx +165 -0
  42. package/src/components/QueryEditor/QueryEditor.tsx +136 -0
  43. package/src/components/QueryEditor/autocomplete.test.ts +46 -0
  44. package/src/components/QueryEditor/autocomplete.ts +76 -0
  45. package/src/components/QueryEditor/index.ts +7 -0
  46. package/src/components/QueryEditor/query-extension.test.ts +176 -0
  47. package/src/components/QueryEditor/query-extension.ts +532 -0
  48. package/src/components/QueryForm/Picker.tsx +47 -0
  49. package/src/components/QueryForm/QueryForm.stories.tsx +72 -0
  50. package/src/components/QueryForm/QueryForm.tsx +77 -0
  51. package/src/components/QueryForm/index.ts +5 -0
  52. package/src/components/QueryForm/query.ts +91 -0
  53. package/src/components/index.ts +6 -0
  54. package/src/hooks/index.ts +5 -0
  55. package/src/hooks/useQueryBuilder.ts +25 -0
  56. package/src/index.ts +6 -0
  57. package/src/translations.ts +22 -0
@@ -0,0 +1,77 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Option from 'effect/Option';
6
+ import React, { useCallback, useMemo, useState } from 'react';
7
+
8
+ import { Filter, Obj, Query, type QueryAST, type Tag } from '@dxos/echo';
9
+ import { URI } from '@dxos/keys';
10
+ import { type ThemedClassName } from '@dxos/react-ui';
11
+ import { useTranslation } from '@dxos/react-ui';
12
+ import { mx } from '@dxos/ui-theme';
13
+
14
+ import { translationKey } from '#translations';
15
+
16
+ import { Picker } from './Picker.tsx';
17
+ import { extractTag, extractTypename } from './query.ts';
18
+
19
+ export type QueryFormProps = ThemedClassName<{
20
+ initialQuery?: QueryAST.Query;
21
+ types?: { value: URI.URI; label: string }[];
22
+ tags?: Tag.Tag[];
23
+ onChange?: (query: Query.Any) => void;
24
+ }>;
25
+
26
+ // TODO(wittjosiah): Support more complex queries and traversals.
27
+ export const QueryForm = ({ classNames, initialQuery, types, tags, onChange }: QueryFormProps) => {
28
+ const { t } = useTranslation(translationKey);
29
+
30
+ const initialType = initialQuery ? Option.getOrUndefined(extractTypename(initialQuery)) : undefined;
31
+ const initialTag = initialQuery ? Option.getOrUndefined(extractTag(initialQuery)) : undefined;
32
+
33
+ const [type, setType] = useState<URI.URI | null>(initialType ?? null);
34
+ const [tag, setTag] = useState<string | null>(initialTag ?? null);
35
+
36
+ const tagOptions = useMemo(() => tags?.map((tag) => ({ value: Obj.getURI(tag), label: tag.label })), [tags]);
37
+
38
+ const handleChange = useCallback(
39
+ ({ type, tag }: { type: URI.URI | null; tag: string | null }) => {
40
+ const typeFilter = type ? Filter.type(type) : null;
41
+ const tagFilter = tag ? Filter.tag(tag) : null;
42
+ const combined =
43
+ typeFilter && tagFilter ? Filter.and(typeFilter, tagFilter) : (typeFilter ?? tagFilter ?? Filter.nothing());
44
+ onChange?.(Query.select(combined));
45
+ },
46
+ [onChange],
47
+ );
48
+
49
+ const handleTypeChange = useCallback(
50
+ (value: string | null) => {
51
+ // Select.onValueChange returns bare string; values come from types[].value which are URI.URI.
52
+ const uri = value as URI.URI | null;
53
+ setType(uri);
54
+ handleChange({ type: uri, tag });
55
+ },
56
+ [handleChange, tag],
57
+ );
58
+
59
+ const handleTagChange = useCallback(
60
+ (tag: string | null) => {
61
+ setTag(tag);
62
+ handleChange({ type, tag });
63
+ },
64
+ [handleChange, type],
65
+ );
66
+
67
+ return (
68
+ <div className={mx('grid grid-cols-2 gap-2', classNames)}>
69
+ {types && (
70
+ <Picker placeholder={t('picker-type.placeholder')} values={types} value={type} onChange={handleTypeChange} />
71
+ )}
72
+ {tags && (
73
+ <Picker placeholder={t('picker-tag.placeholder')} values={tagOptions} value={tag} onChange={handleTagChange} />
74
+ )}
75
+ </div>
76
+ );
77
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './QueryForm.tsx';
@@ -0,0 +1,91 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Match from 'effect/Match';
6
+ import * as Option from 'effect/Option';
7
+
8
+ import { type QueryAST } from '@dxos/echo';
9
+ import { type URI } from '@dxos/keys';
10
+
11
+ // Helper to extract type URI from query AST
12
+ export const extractTypename = (query: QueryAST.Query): Option.Option<URI.URI> => {
13
+ return Match.value(query).pipe(
14
+ Match.withReturnType<Option.Option<URI.URI>>(),
15
+ Match.when({ type: 'select' }, (q) => extractTypenameFromFilter(q.filter)),
16
+ Match.when({ type: 'filter' }, (q) => {
17
+ const selectionTypename = extractTypename(q.selection);
18
+ const filterTypename = extractTypenameFromFilter(q.filter);
19
+ return Option.isSome(selectionTypename) ? selectionTypename : filterTypename;
20
+ }),
21
+ Match.when({ type: 'options' }, (q) => extractTypename(q.query)),
22
+ Match.when({ type: 'from' }, (q) => extractTypename(q.query)),
23
+ Match.when({ type: 'order' }, (q) => extractTypename(q.query)),
24
+ Match.when({ type: 'limit' }, (q) => extractTypename(q.query)),
25
+ Match.orElse(() => Option.none()),
26
+ );
27
+ };
28
+
29
+ // Helper to extract tag from query AST
30
+ export const extractTag = (query: QueryAST.Query): Option.Option<string> => {
31
+ return Match.value(query).pipe(
32
+ Match.withReturnType<Option.Option<string>>(),
33
+ Match.when({ type: 'select' }, (q) => extractTagFromFilter(q.filter)),
34
+ Match.when({ type: 'filter' }, (q) => {
35
+ const selectionTag = extractTag(q.selection);
36
+ const filterTag = extractTagFromFilter(q.filter);
37
+ return Option.isSome(filterTag) ? filterTag : selectionTag;
38
+ }),
39
+ Match.when({ type: 'options' }, (q) => extractTag(q.query)),
40
+ Match.when({ type: 'from' }, (q) => extractTag(q.query)),
41
+ Match.when({ type: 'order' }, (q) => extractTag(q.query)),
42
+ Match.when({ type: 'limit' }, (q) => extractTag(q.query)),
43
+ Match.orElse(() => Option.none()),
44
+ );
45
+ };
46
+
47
+ // Helper to extract type URI from filter AST
48
+ const extractTypenameFromFilter = (filter: QueryAST.Filter): Option.Option<URI.URI> => {
49
+ return Match.value(filter).pipe(
50
+ Match.withReturnType<Option.Option<URI.URI>>(),
51
+ Match.when({ type: 'object' }, (f) => Option.fromNullishOr(f.typename)),
52
+ Match.when({ type: 'and' }, (f) =>
53
+ f.filters.reduce(
54
+ (acc: Option.Option<URI.URI>, filterItem: QueryAST.Filter) =>
55
+ Option.isSome(acc) ? acc : extractTypenameFromFilter(filterItem),
56
+ Option.none<URI.URI>(),
57
+ ),
58
+ ),
59
+ Match.when({ type: 'or' }, (f) =>
60
+ f.filters.reduce(
61
+ (acc: Option.Option<URI.URI>, filterItem: QueryAST.Filter) =>
62
+ Option.isSome(acc) ? acc : extractTypenameFromFilter(filterItem),
63
+ Option.none<URI.URI>(),
64
+ ),
65
+ ),
66
+ Match.orElse(() => Option.none()),
67
+ );
68
+ };
69
+
70
+ // Helper to extract tag from filter AST
71
+ const extractTagFromFilter = (filter: QueryAST.Filter): Option.Option<string> => {
72
+ return Match.value(filter).pipe(
73
+ Match.withReturnType<Option.Option<string>>(),
74
+ Match.when({ type: 'tag' }, (f) => Option.some(f.tag)),
75
+ Match.when({ type: 'and' }, (f) =>
76
+ f.filters.reduce(
77
+ (acc: Option.Option<string>, filterItem: QueryAST.Filter) =>
78
+ Option.isSome(acc) ? acc : extractTagFromFilter(filterItem),
79
+ Option.none<string>(),
80
+ ),
81
+ ),
82
+ Match.when({ type: 'or' }, (f) =>
83
+ f.filters.reduce(
84
+ (acc: Option.Option<string>, filterItem: QueryAST.Filter) =>
85
+ Option.isSome(acc) ? acc : extractTagFromFilter(filterItem),
86
+ Option.none<string>(),
87
+ ),
88
+ ),
89
+ Match.orElse(() => Option.none()),
90
+ );
91
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './QueryEditor/index.ts';
6
+ export * from './QueryForm/index.ts';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './useQueryBuilder.ts';
@@ -0,0 +1,25 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useEffect, useMemo, useState } from 'react';
6
+
7
+ import { Filter } from '@dxos/echo';
8
+ import { QueryBuilder } from '@dxos/echo-query';
9
+ /**
10
+ * Creates a filter from a query string.
11
+ */
12
+ export const useQueryBuilder = (query?: string): Filter.Any => {
13
+ const builder = useMemo(() => new QueryBuilder(), []);
14
+ const [filter, setFilter] = useState<Filter.Any>(Filter.everything());
15
+ useEffect(() => {
16
+ if (query) {
17
+ const { filter } = builder.build(query);
18
+ setFilter(filter ?? Filter.everything());
19
+ } else {
20
+ setFilter(Filter.everything());
21
+ }
22
+ }, [builder, query]);
23
+
24
+ return filter;
25
+ };
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './components/index.ts';
6
+ export * from './hooks/index.ts';
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Resource } from '@dxos/react-ui';
6
+
7
+ export const translationKey = '@dxos/react-ui-query';
8
+
9
+ export const translations = [
10
+ {
11
+ 'en-US': {
12
+ [translationKey]: {
13
+ 'query-editor.placeholder': 'Enter text to filter, or a query (e.g., "#tag")',
14
+
15
+ 'picker-select.label': 'Select',
16
+ 'picker-none.label': 'None',
17
+ 'picker-type.placeholder': 'Type',
18
+ 'picker-tag.placeholder': 'Tag',
19
+ },
20
+ },
21
+ },
22
+ ] as const satisfies Resource[];