@genspectrum/dashboard-components 0.19.2 → 0.19.4
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.
- package/custom-elements.json +383 -10
- package/dist/{LineageFilterChangedEvent-ixHQkq8y.js → LineageFilterChangedEvent-GgkxoF3X.js} +17 -5
- package/dist/LineageFilterChangedEvent-GgkxoF3X.js.map +1 -0
- package/dist/assets/mutationOverTimeWorker-ChQTFL68.js.map +1 -1
- package/dist/components.d.ts +184 -21
- package/dist/components.js +9352 -8683
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +69 -21
- package/dist/util.js +2 -1
- package/package.json +1 -1
- package/src/componentsEntrypoint.ts +3 -1
- package/src/preact/components/error-display.stories.tsx +2 -1
- package/src/preact/components/error-display.tsx +2 -3
- package/src/preact/components/min-max-range-slider.tsx +19 -4
- package/src/preact/components/resize-container.tsx +7 -10
- package/src/preact/components/tooltip.tsx +7 -4
- package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +9 -5
- package/src/preact/dateRangeFilter/date-range-filter.tsx +2 -1
- package/src/preact/dateRangeFilter/dateRangeOption.ts +2 -1
- package/src/preact/genomeViewer/CDSPlot.tsx +219 -0
- package/src/preact/genomeViewer/genome-data-viewer.stories.tsx +113 -0
- package/src/preact/genomeViewer/genome-data-viewer.tsx +69 -0
- package/src/preact/genomeViewer/loadGff3.spec.ts +61 -0
- package/src/preact/genomeViewer/loadGff3.ts +180 -0
- package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +3 -1
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +3 -2
- package/src/preact/locationFilter/LocationChangedEvent.ts +2 -1
- package/src/preact/locationFilter/location-filter.stories.tsx +3 -2
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +3 -2
- package/src/preact/mutationFilter/mutation-filter.tsx +2 -1
- package/src/preact/numberRangeFilter/NumberRangeFilterChangedEvent.ts +31 -0
- package/src/preact/numberRangeFilter/number-range-filter.stories.tsx +383 -0
- package/src/preact/numberRangeFilter/number-range-filter.tsx +159 -0
- package/src/preact/numberRangeFilter/useSelectedRangeReducer.ts +137 -0
- package/src/preact/shared/charts/colors.ts +1 -1
- package/src/preact/textFilter/TextFilterChangedEvent.ts +3 -1
- package/src/preact/textFilter/text-filter.stories.tsx +4 -3
- package/src/utilEntrypoint.ts +2 -0
- package/src/utils/gsEventNames.ts +11 -0
- package/src/web-components/input/gs-date-range-filter.stories.ts +4 -3
- package/src/web-components/input/gs-date-range-filter.tsx +3 -2
- package/src/web-components/input/gs-lineage-filter.stories.ts +3 -2
- package/src/web-components/input/gs-lineage-filter.tsx +2 -1
- package/src/web-components/input/gs-location-filter.stories.ts +3 -2
- package/src/web-components/input/gs-location-filter.tsx +2 -1
- package/src/web-components/input/gs-mutation-filter.stories.ts +3 -2
- package/src/web-components/input/gs-mutation-filter.tsx +2 -1
- package/src/web-components/input/gs-number-range-filter.spec.ts +27 -0
- package/src/web-components/input/gs-number-range-filter.stories.ts +96 -0
- package/src/web-components/input/gs-number-range-filter.tsx +148 -0
- package/src/web-components/input/gs-text-filter.stories.ts +5 -4
- package/src/web-components/input/gs-text-filter.tsx +2 -1
- package/src/web-components/input/index.ts +1 -0
- package/src/web-components/visualization/gs-genome-data-viewer.spec-d.ts +18 -0
- package/src/web-components/visualization/gs-genome-data-viewer.stories.ts +108 -0
- package/src/web-components/visualization/gs-genome-data-viewer.tsx +59 -0
- package/src/web-components/visualization/index.ts +1 -0
- package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
- package/standalone-bundle/dashboard-components.js +9613 -9059
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/dist/LineageFilterChangedEvent-ixHQkq8y.js.map +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useEffect, useReducer, useState } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
import { type NumberRange } from './NumberRangeFilterChangedEvent';
|
|
4
|
+
|
|
5
|
+
type InputState = {
|
|
6
|
+
min: string;
|
|
7
|
+
max: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RangeState = {
|
|
11
|
+
inputState: InputState;
|
|
12
|
+
range: NumberRange & { isValidRange: boolean };
|
|
13
|
+
wasDispatched: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useSelectedRangeReducer(initialValue: NumberRange) {
|
|
17
|
+
const [range, dispatchRange] = useReducer(
|
|
18
|
+
rangeReducer,
|
|
19
|
+
addRange({
|
|
20
|
+
min: initialValue.min?.toString() ?? '',
|
|
21
|
+
max: initialValue.max?.toString() ?? '',
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
const [isInitialRender, setIsInitialRender] = useState(true);
|
|
25
|
+
|
|
26
|
+
useEffect(
|
|
27
|
+
() => {
|
|
28
|
+
if (isInitialRender) {
|
|
29
|
+
setIsInitialRender(false);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
dispatchRange({
|
|
34
|
+
type: SetRangeActionType.SET_VALUE_FROM_CONTROLLED_INPUT,
|
|
35
|
+
range: initialValue,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
[initialValue], // eslint-disable-line react-hooks/exhaustive-deps -- only run this when initialValue changes
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return [range, dispatchRange] as const;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const SetRangeActionType = {
|
|
45
|
+
SET_MIN: 'setMin',
|
|
46
|
+
SET_MAX: 'setMax',
|
|
47
|
+
SET_VALUE_FROM_CONTROLLED_INPUT: 'setValueFromControlledInput',
|
|
48
|
+
DISPATCHED_EVENT: 'dispatchedEvent',
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
type SetRangeAction =
|
|
52
|
+
| {
|
|
53
|
+
type: typeof SetRangeActionType.SET_MIN | typeof SetRangeActionType.SET_MAX;
|
|
54
|
+
value: string;
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
type: typeof SetRangeActionType.SET_VALUE_FROM_CONTROLLED_INPUT;
|
|
58
|
+
range: NumberRange;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
type: typeof SetRangeActionType.DISPATCHED_EVENT;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function rangeReducer(currentState: RangeState, action: SetRangeAction) {
|
|
65
|
+
const { min, max } = currentState.inputState;
|
|
66
|
+
|
|
67
|
+
switch (action.type) {
|
|
68
|
+
case SetRangeActionType.SET_MIN:
|
|
69
|
+
return addRange({ min: action.value, max });
|
|
70
|
+
case SetRangeActionType.SET_MAX:
|
|
71
|
+
return addRange({ min, max: action.value });
|
|
72
|
+
|
|
73
|
+
case SetRangeActionType.SET_VALUE_FROM_CONTROLLED_INPUT:
|
|
74
|
+
return addRange({
|
|
75
|
+
min: action.range.min?.toString() ?? '',
|
|
76
|
+
max: action.range.max?.toString() ?? '',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
case SetRangeActionType.DISPATCHED_EVENT:
|
|
80
|
+
return {
|
|
81
|
+
...currentState,
|
|
82
|
+
wasDispatched: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addRange(inputState: InputState): RangeState {
|
|
88
|
+
const parsedMin = parseRangeValue(inputState.min);
|
|
89
|
+
const parsedMax = parseRangeValue(inputState.max);
|
|
90
|
+
|
|
91
|
+
const range = {
|
|
92
|
+
min: parsedMin.value,
|
|
93
|
+
max: parsedMax.value,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const isValidRange = parsedMin.valid && parsedMax.valid && isValid(range);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
inputState,
|
|
100
|
+
range: {
|
|
101
|
+
...range,
|
|
102
|
+
isValidRange,
|
|
103
|
+
},
|
|
104
|
+
wasDispatched: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseRangeValue(inputValue: string) {
|
|
109
|
+
const trimmedInput = inputValue.trim();
|
|
110
|
+
|
|
111
|
+
if (trimmedInput === '') {
|
|
112
|
+
return {
|
|
113
|
+
valid: true,
|
|
114
|
+
value: undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const value = Number(trimmedInput);
|
|
119
|
+
if (!Number.isFinite(value)) {
|
|
120
|
+
return {
|
|
121
|
+
valid: false,
|
|
122
|
+
value: undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
valid: true,
|
|
128
|
+
value,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isValid(range: NumberRange) {
|
|
133
|
+
if (range.min === undefined || range.max === undefined) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return range.min <= range.max;
|
|
137
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { gsEventNames } from '../../utils/gsEventNames';
|
|
2
|
+
|
|
1
3
|
type LapisTextFilter = Record<string, string | undefined>;
|
|
2
4
|
|
|
3
5
|
export class TextFilterChangedEvent extends CustomEvent<LapisTextFilter> {
|
|
4
6
|
constructor(detail: LapisTextFilter) {
|
|
5
|
-
super(
|
|
7
|
+
super(gsEventNames.textFilterChanged, {
|
|
6
8
|
detail,
|
|
7
9
|
bubbles: true,
|
|
8
10
|
composed: true,
|
|
@@ -5,6 +5,7 @@ import data from './__mockData__/aggregated_hosts.json';
|
|
|
5
5
|
import { TextFilter, type TextFilterProps } from './text-filter';
|
|
6
6
|
import { previewHandles } from '../../../.storybook/preview';
|
|
7
7
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
8
|
+
import { gsEventNames } from '../../utils/gsEventNames';
|
|
8
9
|
import { LapisUrlContextProvider } from '../LapisUrlContext';
|
|
9
10
|
import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
|
|
10
11
|
|
|
@@ -13,7 +14,7 @@ const meta: Meta<TextFilterProps> = {
|
|
|
13
14
|
component: TextFilter,
|
|
14
15
|
parameters: {
|
|
15
16
|
actions: {
|
|
16
|
-
handles: [
|
|
17
|
+
handles: [gsEventNames.textFilterChanged, ...previewHandles],
|
|
17
18
|
},
|
|
18
19
|
fetchMock: {
|
|
19
20
|
mocks: [
|
|
@@ -93,7 +94,7 @@ export const RemoveInitialValue: StoryObj<TextFilterProps> = {
|
|
|
93
94
|
|
|
94
95
|
const changedListenerMock = fn();
|
|
95
96
|
await step('Setup event listener mock', () => {
|
|
96
|
-
canvasElement.addEventListener(
|
|
97
|
+
canvasElement.addEventListener(gsEventNames.textFilterChanged, changedListenerMock);
|
|
97
98
|
});
|
|
98
99
|
|
|
99
100
|
await waitFor(async () => {
|
|
@@ -136,7 +137,7 @@ export const KeepsPartialInputInInputField: StoryObj<TextFilterProps> = {
|
|
|
136
137
|
|
|
137
138
|
const changedListenerMock = fn();
|
|
138
139
|
await step('Setup event listener mock', () => {
|
|
139
|
-
canvasElement.addEventListener(
|
|
140
|
+
canvasElement.addEventListener(gsEventNames.textFilterChanged, changedListenerMock);
|
|
140
141
|
});
|
|
141
142
|
const inputField = () => canvas.getByPlaceholderText('Enter a host name', { exact: false });
|
|
142
143
|
async function typeAndBlur(input: string) {
|
package/src/utilEntrypoint.ts
CHANGED
|
@@ -38,3 +38,5 @@ export { LineageFilterChangedEvent } from './preact/lineageFilter/LineageFilterC
|
|
|
38
38
|
export { TextFilterChangedEvent } from './preact/textFilter/TextFilterChangedEvent';
|
|
39
39
|
|
|
40
40
|
export type { MutationAnnotations, MutationAnnotation } from './web-components/mutation-annotations-context';
|
|
41
|
+
|
|
42
|
+
export { gsEventNames } from './utils/gsEventNames';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const gsEventNames = {
|
|
2
|
+
error: 'gs-error',
|
|
3
|
+
dateRangeFilterChanged: 'gs-date-range-filter-changed',
|
|
4
|
+
dateRangeOptionChanged: 'gs-date-range-option-changed',
|
|
5
|
+
mutationFilterChanged: 'gs-mutation-filter-changed',
|
|
6
|
+
lineageFilterChanged: 'gs-lineage-filter-changed',
|
|
7
|
+
locationChanged: 'gs-location-changed',
|
|
8
|
+
textFilterChanged: 'gs-text-filter-changed',
|
|
9
|
+
numberRangeFilterChanged: 'gs-number-range-filter-changed',
|
|
10
|
+
numberRangeValueChanged: 'gs-number-range-value-changed',
|
|
11
|
+
} as const;
|
|
@@ -10,6 +10,7 @@ import './gs-date-range-filter';
|
|
|
10
10
|
import '../gs-app';
|
|
11
11
|
import { toYYYYMMDD } from '../../preact/dateRangeFilter/dateConversion';
|
|
12
12
|
import { dateRangeOptionPresets } from '../../preact/dateRangeFilter/dateRangeOption';
|
|
13
|
+
import { gsEventNames } from '../../utils/gsEventNames';
|
|
13
14
|
import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
14
15
|
|
|
15
16
|
const codeExample = String.raw`
|
|
@@ -29,7 +30,7 @@ const meta: Meta<Required<DateRangeFilterProps>> = {
|
|
|
29
30
|
component: 'gs-date-range-filter',
|
|
30
31
|
parameters: withComponentDocs({
|
|
31
32
|
actions: {
|
|
32
|
-
handles: [
|
|
33
|
+
handles: [gsEventNames.dateRangeFilterChanged, gsEventNames.dateRangeOptionChanged, ...previewHandles],
|
|
33
34
|
},
|
|
34
35
|
fetchMock: {},
|
|
35
36
|
componentDocs: {
|
|
@@ -144,8 +145,8 @@ export const FiresEvents: StoryObj<Required<DateRangeFilterProps>> = {
|
|
|
144
145
|
const filterChangedListenerMock = fn();
|
|
145
146
|
const optionChangedListenerMock = fn();
|
|
146
147
|
await step('Setup event listener mock', () => {
|
|
147
|
-
canvasElement.addEventListener(
|
|
148
|
-
canvasElement.addEventListener(
|
|
148
|
+
canvasElement.addEventListener(gsEventNames.dateRangeFilterChanged, filterChangedListenerMock);
|
|
149
|
+
canvasElement.addEventListener(gsEventNames.dateRangeOptionChanged, optionChangedListenerMock);
|
|
149
150
|
});
|
|
150
151
|
|
|
151
152
|
await step('Expect last 6 months to be selected', async () => {
|
|
@@ -5,6 +5,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { DateRangeFilter, type DateRangeFilterProps } from '../../preact/dateRangeFilter/date-range-filter';
|
|
7
7
|
import { type DateRangeOptionChangedEvent } from '../../preact/dateRangeFilter/dateRangeOption';
|
|
8
|
+
import { type gsEventNames } from '../../utils/gsEventNames';
|
|
8
9
|
import { type Equals, type Expect } from '../../utils/typeAssertions';
|
|
9
10
|
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
10
11
|
|
|
@@ -143,8 +144,8 @@ declare global {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
interface HTMLElementEventMap {
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
[gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
|
|
148
|
+
[gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -9,6 +9,7 @@ import '../gs-app';
|
|
|
9
9
|
import './gs-lineage-filter';
|
|
10
10
|
import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
|
|
11
11
|
import { type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
|
|
12
|
+
import { gsEventNames } from '../../utils/gsEventNames';
|
|
12
13
|
import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
13
14
|
|
|
14
15
|
const codeExample = String.raw`
|
|
@@ -25,7 +26,7 @@ const meta: Meta<Required<LineageFilterProps>> = {
|
|
|
25
26
|
component: 'gs-lineage-filter',
|
|
26
27
|
parameters: withComponentDocs({
|
|
27
28
|
actions: {
|
|
28
|
-
handles: [
|
|
29
|
+
handles: [gsEventNames.lineageFilterChanged, ...previewHandles],
|
|
29
30
|
},
|
|
30
31
|
fetchMock: {
|
|
31
32
|
mocks: [
|
|
@@ -182,7 +183,7 @@ export const FiresEvent: StoryObj<Required<LineageFilterProps>> = {
|
|
|
182
183
|
const inputField = () => canvas.getByPlaceholderText('Enter a lineage');
|
|
183
184
|
const listenerMock = fn();
|
|
184
185
|
await step('Setup event listener mock', () => {
|
|
185
|
-
canvasElement.addEventListener(
|
|
186
|
+
canvasElement.addEventListener(gsEventNames.lineageFilterChanged, listenerMock);
|
|
186
187
|
});
|
|
187
188
|
|
|
188
189
|
await step('wait until data is loaded', async () => {
|
|
@@ -3,6 +3,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { type LineageFilterChangedEvent } from '../../preact/lineageFilter/LineageFilterChangedEvent';
|
|
5
5
|
import { LineageFilter, type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
|
|
6
|
+
import { type gsEventNames } from '../../utils/gsEventNames';
|
|
6
7
|
import type { Equals, Expect } from '../../utils/typeAssertions';
|
|
7
8
|
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
8
9
|
|
|
@@ -91,7 +92,7 @@ declare global {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
interface HTMLElementEventMap {
|
|
94
|
-
|
|
95
|
+
[gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -10,6 +10,7 @@ import '../gs-app';
|
|
|
10
10
|
import './gs-location-filter';
|
|
11
11
|
import data from '../../preact/locationFilter/__mockData__/aggregated.json';
|
|
12
12
|
import { type LocationFilterProps } from '../../preact/locationFilter/location-filter';
|
|
13
|
+
import { gsEventNames } from '../../utils/gsEventNames';
|
|
13
14
|
import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
14
15
|
|
|
15
16
|
const codeExample = String.raw`
|
|
@@ -26,7 +27,7 @@ const meta: Meta = {
|
|
|
26
27
|
component: 'gs-location-filter',
|
|
27
28
|
parameters: withComponentDocs({
|
|
28
29
|
actions: {
|
|
29
|
-
handles: [
|
|
30
|
+
handles: [gsEventNames.locationChanged, ...previewHandles],
|
|
30
31
|
},
|
|
31
32
|
componentDocs: {
|
|
32
33
|
opensShadowDom: true,
|
|
@@ -189,7 +190,7 @@ export const FiresEvent: StoryObj<LocationFilterProps> = {
|
|
|
189
190
|
|
|
190
191
|
const listenerMock = fn();
|
|
191
192
|
await step('Setup event listener mock', () => {
|
|
192
|
-
canvasElement.addEventListener(
|
|
193
|
+
canvasElement.addEventListener(gsEventNames.locationChanged, listenerMock);
|
|
193
194
|
});
|
|
194
195
|
|
|
195
196
|
await step('wait until data is loaded', async () => {
|
|
@@ -3,6 +3,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { type LocationChangedEvent } from '../../preact/locationFilter/LocationChangedEvent';
|
|
5
5
|
import { LocationFilter, type LocationFilterProps } from '../../preact/locationFilter/location-filter';
|
|
6
|
+
import { type gsEventNames } from '../../utils/gsEventNames';
|
|
6
7
|
import type { Equals, Expect } from '../../utils/typeAssertions';
|
|
7
8
|
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
8
9
|
|
|
@@ -95,7 +96,7 @@ declare global {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
interface HTMLElementEventMap {
|
|
98
|
-
|
|
99
|
+
[gsEventNames.locationChanged]: LocationChangedEvent;
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -7,6 +7,7 @@ import { previewHandles } from '../../../.storybook/preview';
|
|
|
7
7
|
import { LAPIS_URL, REFERENCE_GENOME_ENDPOINT } from '../../constants';
|
|
8
8
|
import '../gs-app';
|
|
9
9
|
import { type MutationFilterProps } from '../../preact/mutationFilter/mutation-filter';
|
|
10
|
+
import { gsEventNames } from '../../utils/gsEventNames';
|
|
10
11
|
import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
11
12
|
import './gs-mutation-filter';
|
|
12
13
|
|
|
@@ -21,7 +22,7 @@ const meta: Meta<MutationFilterProps> = {
|
|
|
21
22
|
component: 'gs-mutation-filter',
|
|
22
23
|
parameters: withComponentDocs({
|
|
23
24
|
actions: {
|
|
24
|
-
handles: [
|
|
25
|
+
handles: [gsEventNames.mutationFilterChanged, ...previewHandles],
|
|
25
26
|
},
|
|
26
27
|
fetchMock: {},
|
|
27
28
|
componentDocs: {
|
|
@@ -73,7 +74,7 @@ export const FiresFilterChangedEvent: StoryObj<MutationFilterProps> = {
|
|
|
73
74
|
const inputField = () => canvas.getByPlaceholderText('Enter a mutation', { exact: false });
|
|
74
75
|
const listenerMock = fn();
|
|
75
76
|
await step('Setup event listener mock', () => {
|
|
76
|
-
canvasElement.addEventListener(
|
|
77
|
+
canvasElement.addEventListener(gsEventNames.mutationFilterChanged, listenerMock);
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
await step('wait until data is loaded', async () => {
|
|
@@ -4,6 +4,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
|
4
4
|
import { ReferenceGenomesAwaiter } from '../../preact/components/ReferenceGenomesAwaiter';
|
|
5
5
|
import { MutationFilter, type MutationFilterProps } from '../../preact/mutationFilter/mutation-filter';
|
|
6
6
|
import type { MutationsFilter } from '../../types';
|
|
7
|
+
import { type gsEventNames } from '../../utils/gsEventNames';
|
|
7
8
|
import type { Equals, Expect } from '../../utils/typeAssertions';
|
|
8
9
|
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
9
10
|
|
|
@@ -95,7 +96,7 @@ declare global {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
interface HTMLElementEventMap {
|
|
98
|
-
|
|
99
|
+
[gsEventNames.mutationFilterChanged]: CustomEvent<MutationsFilter>;
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expectTypeOf, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { NumberRangeFilterComponent } from './gs-number-range-filter';
|
|
4
|
+
import { type NumberRangeFilterProps } from '../../preact/numberRangeFilter/number-range-filter';
|
|
5
|
+
|
|
6
|
+
describe('gs-number-range-filter types', () => {
|
|
7
|
+
test('should match', ({}) => {
|
|
8
|
+
expectTypeOf(NumberRangeFilterComponent.prototype)
|
|
9
|
+
.toHaveProperty('value')
|
|
10
|
+
.toEqualTypeOf<NumberRangeFilterProps['value']>();
|
|
11
|
+
expectTypeOf(NumberRangeFilterComponent.prototype)
|
|
12
|
+
.toHaveProperty('lapisField')
|
|
13
|
+
.toEqualTypeOf<NumberRangeFilterProps['lapisField']>();
|
|
14
|
+
expectTypeOf(NumberRangeFilterComponent.prototype)
|
|
15
|
+
.toHaveProperty('sliderMin')
|
|
16
|
+
.toEqualTypeOf<NumberRangeFilterProps['sliderMin']>();
|
|
17
|
+
expectTypeOf(NumberRangeFilterComponent.prototype)
|
|
18
|
+
.toHaveProperty('sliderMax')
|
|
19
|
+
.toEqualTypeOf<NumberRangeFilterProps['sliderMax']>();
|
|
20
|
+
expectTypeOf(NumberRangeFilterComponent.prototype)
|
|
21
|
+
.toHaveProperty('sliderStep')
|
|
22
|
+
.toEqualTypeOf<NumberRangeFilterProps['sliderStep']>();
|
|
23
|
+
expectTypeOf(NumberRangeFilterComponent.prototype)
|
|
24
|
+
.toHaveProperty('width')
|
|
25
|
+
.toEqualTypeOf<NumberRangeFilterProps['width']>();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import '../gs-app';
|
|
2
|
+
import './gs-number-range-filter';
|
|
3
|
+
|
|
4
|
+
import { type Meta, type StoryObj } from '@storybook/web-components';
|
|
5
|
+
import { html } from 'lit';
|
|
6
|
+
|
|
7
|
+
import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
|
|
8
|
+
import { LAPIS_URL } from '../../constants';
|
|
9
|
+
import { type NumberRangeFilterProps } from '../../preact/numberRangeFilter/number-range-filter';
|
|
10
|
+
import { gsEventNames } from '../../utils/gsEventNames';
|
|
11
|
+
|
|
12
|
+
const codeExample = String.raw`
|
|
13
|
+
<gs-number-range-filter
|
|
14
|
+
value='{"ageFrom": 10, "ageTo": 90}'
|
|
15
|
+
lapisField="age"
|
|
16
|
+
sliderMin="0"
|
|
17
|
+
sliderMax="100"
|
|
18
|
+
sliderStep="1"
|
|
19
|
+
width="50%"
|
|
20
|
+
></gs-number-range-filter>`;
|
|
21
|
+
|
|
22
|
+
const meta: Meta<NumberRangeFilterProps> = {
|
|
23
|
+
title: 'Input/Number range filter',
|
|
24
|
+
component: 'gs-number-range-filter',
|
|
25
|
+
parameters: withComponentDocs({
|
|
26
|
+
actions: {
|
|
27
|
+
handles: [gsEventNames.numberRangeFilterChanged, gsEventNames.numberRangeValueChanged],
|
|
28
|
+
},
|
|
29
|
+
componentDocs: {
|
|
30
|
+
opensShadowDom: true,
|
|
31
|
+
expectsChildren: false,
|
|
32
|
+
codeExample,
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
tags: ['autodocs'],
|
|
36
|
+
argTypes: {
|
|
37
|
+
value: {
|
|
38
|
+
control: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
lapisField: {
|
|
43
|
+
control: {
|
|
44
|
+
type: 'text',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
sliderMin: {
|
|
48
|
+
control: {
|
|
49
|
+
type: 'number',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
sliderMax: {
|
|
53
|
+
control: {
|
|
54
|
+
type: 'number',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
sliderStep: {
|
|
58
|
+
control: {
|
|
59
|
+
type: 'number',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
width: {
|
|
63
|
+
control: {
|
|
64
|
+
type: 'text',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default meta;
|
|
71
|
+
|
|
72
|
+
export const Default: StoryObj<NumberRangeFilterProps> = {
|
|
73
|
+
render: (args) => {
|
|
74
|
+
return html`
|
|
75
|
+
<gs-app lapis="${LAPIS_URL}">
|
|
76
|
+
<gs-number-range-filter
|
|
77
|
+
.value=${args.value}
|
|
78
|
+
.lapisField=${args.lapisField}
|
|
79
|
+
.sliderMin=${args.sliderMin}
|
|
80
|
+
.sliderMax=${args.sliderMax}
|
|
81
|
+
.sliderStep=${args.sliderStep}
|
|
82
|
+
.width=${args.width}
|
|
83
|
+
>
|
|
84
|
+
</gs-number-range-filter>
|
|
85
|
+
</gs-app>
|
|
86
|
+
`;
|
|
87
|
+
},
|
|
88
|
+
args: {
|
|
89
|
+
lapisField: 'age',
|
|
90
|
+
value: { min: 10, max: 90 },
|
|
91
|
+
sliderMin: 0,
|
|
92
|
+
sliderMax: 100,
|
|
93
|
+
sliderStep: 0.1,
|
|
94
|
+
width: '100%',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
2
|
+
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type NumberRangeFilterChangedEvent,
|
|
6
|
+
type NumberRangeValueChangedEvent,
|
|
7
|
+
} from '../../preact/numberRangeFilter/NumberRangeFilterChangedEvent';
|
|
8
|
+
import { NumberRangeFilter } from '../../preact/numberRangeFilter/number-range-filter';
|
|
9
|
+
import { type gsEventNames } from '../../utils/gsEventNames';
|
|
10
|
+
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* ## Context
|
|
15
|
+
*
|
|
16
|
+
* This component lets the user specify filters for a `float` or `int` field in LAPIS.
|
|
17
|
+
* It consists of two text input fields and a slider.
|
|
18
|
+
*
|
|
19
|
+
* The slider must be restricted to a certain range.
|
|
20
|
+
* Users can however still enter values outside of this range in the text input fields.
|
|
21
|
+
*
|
|
22
|
+
* @fires {CustomEvent<Record<string, string | undefined>>} gs-number-range-filter-changed
|
|
23
|
+
* Fired when the slider is released,
|
|
24
|
+
* `onBlur` on the input fields after the user has typed a valid range in the input fields,
|
|
25
|
+
* or when one of the input fields is cleared.
|
|
26
|
+
* The `details` of this event contain an object with `${lapisField}From` and `${lapisField}To` as keys.
|
|
27
|
+
* The values are the numbers from the input fields or `undefined` if the input field is empty:
|
|
28
|
+
* ```
|
|
29
|
+
* {
|
|
30
|
+
* [`${lapisField}From`]: number | undefined
|
|
31
|
+
* [`${lapisField}To`]: number | undefined
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
* Example:
|
|
35
|
+
* ```
|
|
36
|
+
* {
|
|
37
|
+
* ageFrom: 18,
|
|
38
|
+
* ageTo: undefined
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @fires {CustomEvent<Record<string, string | undefined>>} gs-number-range-value-changed
|
|
43
|
+
* Similar to the `gs-number-range-filter-changed` event,
|
|
44
|
+
* but contains an `event.detail` that has a fixed format:
|
|
45
|
+
* ```
|
|
46
|
+
* {
|
|
47
|
+
* min: number | undefined
|
|
48
|
+
* max: number | undefined
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
* This event should be used when you want to control this component externally.
|
|
52
|
+
* The `event.detail` can be used as the value of the component.
|
|
53
|
+
* Example:
|
|
54
|
+
* ```
|
|
55
|
+
* {
|
|
56
|
+
* min: 18,
|
|
57
|
+
* max: undefined
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
@customElement('gs-number-range-filter')
|
|
62
|
+
export class NumberRangeFilterComponent extends PreactLitAdapter {
|
|
63
|
+
/**
|
|
64
|
+
* The value to use for this number filter.
|
|
65
|
+
*
|
|
66
|
+
* Must be of the form:
|
|
67
|
+
* ```
|
|
68
|
+
* {
|
|
69
|
+
* [`${lapisField}From`]: number | undefined
|
|
70
|
+
* [`${lapisField}To`]: number | undefined
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* This is the same format that the `gs-number-value-changed` event will emit.
|
|
75
|
+
*/
|
|
76
|
+
@property({ type: Object })
|
|
77
|
+
value: { min?: number; max?: number } = {};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Required.
|
|
81
|
+
*
|
|
82
|
+
* The LAPIS field name to use for this text filter.
|
|
83
|
+
* The field must exist on this LAPIS instance.
|
|
84
|
+
*/
|
|
85
|
+
@property()
|
|
86
|
+
lapisField = '';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The `min` value to use for the slider.
|
|
90
|
+
*/
|
|
91
|
+
@property({ type: Number })
|
|
92
|
+
sliderMin = 0;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The `max` value to use for the slider.
|
|
96
|
+
*/
|
|
97
|
+
@property({ type: Number })
|
|
98
|
+
sliderMax = 100;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The `step` value to use for the slider.
|
|
102
|
+
*
|
|
103
|
+
* This attribute has no effect on the text input.
|
|
104
|
+
*/
|
|
105
|
+
@property({ type: Number })
|
|
106
|
+
sliderStep = 1;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The width of the component.
|
|
110
|
+
*
|
|
111
|
+
* Visit https://genspectrum.github.io/dashboard-components/?path=/docs/concepts-size-of-components--docs for more information.
|
|
112
|
+
*/
|
|
113
|
+
@property({ type: String })
|
|
114
|
+
width: string = '100%';
|
|
115
|
+
|
|
116
|
+
override render() {
|
|
117
|
+
return (
|
|
118
|
+
<NumberRangeFilter
|
|
119
|
+
value={this.value}
|
|
120
|
+
lapisField={this.lapisField}
|
|
121
|
+
sliderMin={this.sliderMin}
|
|
122
|
+
sliderMax={this.sliderMax}
|
|
123
|
+
sliderStep={this.sliderStep}
|
|
124
|
+
width={this.width}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
declare global {
|
|
131
|
+
interface HTMLElementTagNameMap {
|
|
132
|
+
'gs-number-range-filter': NumberRangeFilterComponent;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface HTMLElementEventMap {
|
|
136
|
+
[gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
|
|
137
|
+
[gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
declare global {
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
143
|
+
namespace JSX {
|
|
144
|
+
interface IntrinsicElements {
|
|
145
|
+
'gs-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|