@kanaries/graphic-walker 0.3.11 → 0.3.13
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/dist/assets/transform.worker-a12fb3d8.js.map +1 -0
- package/dist/components/appRoot.d.ts +5 -4
- package/dist/fields/filterField/slider.d.ts +0 -1
- package/dist/graphic-walker.es.js +24578 -24272
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +260 -128
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/interfaces.d.ts +78 -1
- package/dist/renderer/pureRenderer.d.ts +4 -12
- package/dist/utils/chartIndexControl.d.ts +7 -0
- package/dist/utils/vegaApiExport.d.ts +3 -9
- package/package.json +2 -1
- package/src/assets/kanaries-logo.svg +1 -0
- package/src/components/appRoot.tsx +65 -7
- package/src/components/modal.tsx +9 -9
- package/src/fields/filterField/filterEditDialog.tsx +33 -8
- package/src/fields/filterField/slider.tsx +127 -85
- package/src/fields/filterField/tabs.tsx +301 -184
- package/src/index.tsx +3 -3
- package/src/interfaces.ts +83 -1
- package/src/lib/execExp.ts +1 -1
- package/src/locales/en-US.json +10 -3
- package/src/locales/ja-JP.json +10 -3
- package/src/locales/zh-CN.json +10 -3
- package/src/renderer/hooks.ts +6 -0
- package/src/renderer/index.tsx +7 -0
- package/src/renderer/pureRenderer.tsx +16 -18
- package/src/utils/chartIndexControl.ts +39 -0
- package/src/utils/vegaApiExport.ts +127 -10
- package/src/vis/react-vega.tsx +31 -11
- package/src/visualSettings/index.tsx +299 -70
- package/dist/assets/transform.worker-90e4f506.js.map +0 -1
|
@@ -1,107 +1,127 @@
|
|
|
1
1
|
import { observer } from 'mobx-react-lite';
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useMemo, useRef } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
|
|
6
6
|
import type { IFilterField, IFilterRule } from '../../interfaces';
|
|
7
7
|
import { useGlobalStore } from '../../store';
|
|
8
|
-
import PureTabs from '../../components/tabs/defaultTab';
|
|
9
8
|
import Slider from './slider';
|
|
10
9
|
|
|
11
|
-
|
|
12
10
|
export type RuleFormProps = {
|
|
13
11
|
field: IFilterField;
|
|
14
12
|
onChange: (rule: IFilterRule) => void;
|
|
15
13
|
};
|
|
16
14
|
|
|
17
|
-
const Container = styled.div
|
|
18
|
-
|
|
19
|
-
marginInline: '2em',
|
|
15
|
+
const Container = styled.div`
|
|
16
|
+
margin-block: 1em;
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
display:
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
> .btn-grp {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: row;
|
|
21
|
+
margin-block: 1em;
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
> * {
|
|
24
|
+
margin-inline-start: 0.6em;
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
&:first-child: {
|
|
27
|
+
margin-inline-start: 0;
|
|
31
28
|
},
|
|
32
29
|
},
|
|
33
30
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export const Button = styled.button
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
color:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export const Button = styled.button`
|
|
34
|
+
:hover: {
|
|
35
|
+
background-color: rgba(243, 244, 246, 0.5);
|
|
36
|
+
};
|
|
37
|
+
color: rgb(55, 65, 81);
|
|
38
|
+
border: 1px solid rgb(226 232 240);
|
|
39
|
+
border-radius: 0.5em;
|
|
40
|
+
padding-block: 0.4em;
|
|
41
|
+
padding-inline: 1em;
|
|
42
|
+
user-select: none;
|
|
43
|
+
font-weight: bold;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const Table = styled.div`
|
|
48
|
+
display: grid;
|
|
49
|
+
grid-template-columns: 4em auto max-content;
|
|
50
|
+
max-height: 30vh;
|
|
51
|
+
overflow-y: scroll;
|
|
52
|
+
|
|
53
|
+
& > * {
|
|
54
|
+
padding-block: 0.6em;
|
|
55
|
+
padding-inline: 0.2em;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
text-overflow: ellipsis;
|
|
59
|
+
user-select: none;
|
|
60
|
+
border-bottom: 0.8px solid rgb(226 232 240);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
& > input,
|
|
64
|
+
& > *[for] {
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const TabsContainer = styled.div`
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
align-items: stretch;
|
|
73
|
+
justify-content: stretch;
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const CalendarInputContainer = styled.div`
|
|
77
|
+
display: flex;
|
|
78
|
+
padding-block: 1em;
|
|
79
|
+
width: 100%;
|
|
80
|
+
|
|
81
|
+
> .calendar-input {
|
|
82
|
+
width: 100%;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
> .calendar-input:first-child {
|
|
86
|
+
margin-right: 0.5em;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
> .calendar-input:last-child {
|
|
90
|
+
margin-left: 0.5em;
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
const TabPanel = styled.div``;
|
|
95
|
+
|
|
96
|
+
const TabItem = styled.div``;
|
|
97
|
+
|
|
98
|
+
const StatusCheckbox: React.FC<{currentNum: number; totalNum: number; onChange: () => void}> = props => {
|
|
99
|
+
const { currentNum, totalNum, onChange } = props;
|
|
100
|
+
const checkboxRef = useRef(null);
|
|
72
101
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
borderWidth: '1px',
|
|
87
|
-
borderRadius: '4px 4px 0 0',
|
|
88
|
-
position: 'relative',
|
|
89
|
-
|
|
90
|
-
'&[aria-selected]': {
|
|
91
|
-
borderBottomColor: '#0000',
|
|
92
|
-
zIndex: 15,
|
|
93
|
-
},
|
|
94
|
-
'&[aria-selected=false]': {
|
|
95
|
-
backgroundColor: '#f8f8f8',
|
|
96
|
-
borderBottomColor: '#e2e2e2',
|
|
97
|
-
cursor: 'pointer',
|
|
98
|
-
zIndex: 14,
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const TabPanel = styled.div({});
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
if (!checkboxRef.current) return;
|
|
104
|
+
const checkboxRefDOM = (checkboxRef.current as HTMLInputElement)
|
|
105
|
+
if (currentNum === totalNum) {
|
|
106
|
+
checkboxRefDOM.checked = true;
|
|
107
|
+
checkboxRefDOM.indeterminate = false;
|
|
108
|
+
} else if (currentNum < totalNum && currentNum > 0) {
|
|
109
|
+
checkboxRefDOM.indeterminate = true;
|
|
110
|
+
} else if (currentNum === 0) {
|
|
111
|
+
checkboxRefDOM.checked = false;
|
|
112
|
+
checkboxRefDOM.indeterminate = false;
|
|
113
|
+
}
|
|
114
|
+
}, [currentNum, totalNum])
|
|
103
115
|
|
|
104
|
-
|
|
116
|
+
return (
|
|
117
|
+
<input
|
|
118
|
+
type="checkbox"
|
|
119
|
+
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
|
120
|
+
ref={checkboxRef}
|
|
121
|
+
onChange={() => onChange()}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
105
125
|
|
|
106
126
|
export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
|
|
107
127
|
active,
|
|
@@ -121,7 +141,7 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
121
141
|
}, new Map<string | number, number>());
|
|
122
142
|
}, [dataSource, field]);
|
|
123
143
|
|
|
124
|
-
const { t } = useTranslation('translation'
|
|
144
|
+
const { t } = useTranslation('translation');
|
|
125
145
|
|
|
126
146
|
React.useEffect(() => {
|
|
127
147
|
if (active && field.rule?.type !== 'one of') {
|
|
@@ -132,19 +152,86 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
132
152
|
}
|
|
133
153
|
}, [active, onChange, field, count]);
|
|
134
154
|
|
|
155
|
+
const handleToggleFullOrEmptySet = () => {
|
|
156
|
+
if (!field.rule || field.rule.type !== 'one of') return;
|
|
157
|
+
const curSet = field.rule.value;
|
|
158
|
+
onChange({
|
|
159
|
+
type: 'one of',
|
|
160
|
+
value: new Set<number | string>(
|
|
161
|
+
curSet.size === count.size
|
|
162
|
+
? []
|
|
163
|
+
: count.keys()
|
|
164
|
+
),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const handleToggleReverseSet = () => {
|
|
168
|
+
if (!field.rule || field.rule.type !== 'one of') return;
|
|
169
|
+
const curSet = field.rule.value;
|
|
170
|
+
onChange({
|
|
171
|
+
type: 'one of',
|
|
172
|
+
value: new Set<number | string>(
|
|
173
|
+
[...count.keys()].filter(key => !curSet.has(key))
|
|
174
|
+
),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const handleSelectValue = (value, checked) => {
|
|
178
|
+
if (!field.rule || field.rule?.type !== 'one of') return;
|
|
179
|
+
const rule: IFilterRule = {
|
|
180
|
+
type: 'one of',
|
|
181
|
+
value: new Set(field.rule.value)
|
|
182
|
+
};
|
|
183
|
+
if (checked) {
|
|
184
|
+
rule.value.add(value);
|
|
185
|
+
} else {
|
|
186
|
+
rule.value.delete(value);
|
|
187
|
+
}
|
|
188
|
+
onChange(rule);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const selectedValueSum = useMemo(() => {
|
|
192
|
+
if (!field.rule) return 0;
|
|
193
|
+
return [...field.rule.value].reduce<number>((sum, key) => {
|
|
194
|
+
const s = dataSource.filter(which => which[field.fid] === key).length;
|
|
195
|
+
return sum + s;
|
|
196
|
+
}, 0)
|
|
197
|
+
}, [field.rule?.value])
|
|
198
|
+
|
|
135
199
|
return field.rule?.type === 'one of' ? (
|
|
136
200
|
<Container>
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
201
|
+
<div>{t('constant.filter_type.one_of')}</div>
|
|
202
|
+
<div className="text-gray-500">{t('constant.filter_type.one_of_desc')}</div>
|
|
203
|
+
<div className="btn-grp">
|
|
204
|
+
<Button
|
|
205
|
+
onClick={() => handleToggleFullOrEmptySet()}
|
|
206
|
+
>
|
|
207
|
+
{
|
|
208
|
+
field.rule.value.size === count.size
|
|
209
|
+
? t('filters.btn.unselect_all')
|
|
210
|
+
: t('filters.btn.select_all')
|
|
211
|
+
}
|
|
212
|
+
</Button>
|
|
213
|
+
<Button
|
|
214
|
+
onClick={() => handleToggleReverseSet()}
|
|
215
|
+
>
|
|
216
|
+
{t('filters.btn.reverse')}
|
|
217
|
+
</Button>
|
|
218
|
+
</div>
|
|
219
|
+
<Table className="bg-slate-50">
|
|
220
|
+
<div className="flex justify-center items-center">
|
|
221
|
+
<StatusCheckbox
|
|
222
|
+
currentNum={field.rule.value.size}
|
|
223
|
+
totalNum={count.size}
|
|
224
|
+
onChange={handleToggleFullOrEmptySet}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
<label className="header text-gray-500">
|
|
228
|
+
{t('filters.header.value')}
|
|
143
229
|
</label>
|
|
144
|
-
<label className="header">
|
|
145
|
-
{t('header.count')}
|
|
230
|
+
<label className="header text-gray-500">
|
|
231
|
+
{t('filters.header.count')}
|
|
146
232
|
</label>
|
|
147
233
|
</Table>
|
|
234
|
+
{/* <hr /> */}
|
|
148
235
|
<Table>
|
|
149
236
|
{
|
|
150
237
|
[...count.entries()].map(([value, count], idx) => {
|
|
@@ -152,31 +239,17 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
152
239
|
|
|
153
240
|
return (
|
|
154
241
|
<React.Fragment key={idx}>
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const rule: IFilterRule = {
|
|
167
|
-
type: 'one of',
|
|
168
|
-
value: new Set(field.rule.value)
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
if (checked) {
|
|
172
|
-
rule.value.add(value);
|
|
173
|
-
} else {
|
|
174
|
-
rule.value.delete(value);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
onChange(rule);
|
|
178
|
-
}}
|
|
179
|
-
/>
|
|
242
|
+
<div className="flex justify-center items-center">
|
|
243
|
+
<input
|
|
244
|
+
type="checkbox"
|
|
245
|
+
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
|
246
|
+
checked={field.rule?.type === 'one of' && field.rule.value.has(value)}
|
|
247
|
+
id={id}
|
|
248
|
+
aria-describedby={`${id}_label`}
|
|
249
|
+
title={String(value)}
|
|
250
|
+
onChange={({ target: { checked } }) => handleSelectValue(value, checked)}
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
180
253
|
<label
|
|
181
254
|
id={`${id}_label`}
|
|
182
255
|
htmlFor={id}
|
|
@@ -197,60 +270,45 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
197
270
|
<Table className="text-gray-600">
|
|
198
271
|
<label></label>
|
|
199
272
|
<label>
|
|
200
|
-
{t('selected_keys', { count: field.rule.value.size })}
|
|
273
|
+
{t('filters.selected_keys', { count: field.rule.value.size })}
|
|
201
274
|
</label>
|
|
202
275
|
<label>
|
|
203
|
-
{
|
|
204
|
-
const s = dataSource.filter(which => which[field.fid] === key).length;
|
|
205
|
-
|
|
206
|
-
return sum + s;
|
|
207
|
-
}, 0)}
|
|
276
|
+
{selectedValueSum}
|
|
208
277
|
</label>
|
|
209
278
|
</Table>
|
|
210
|
-
<div className="btn-grp">
|
|
211
|
-
<Button
|
|
212
|
-
onClick={() => {
|
|
213
|
-
if (field.rule?.type === 'one of') {
|
|
214
|
-
const curSet = field.rule.value;
|
|
215
|
-
|
|
216
|
-
onChange({
|
|
217
|
-
type: 'one of',
|
|
218
|
-
value: new Set<number | string>(
|
|
219
|
-
curSet.size === count.size
|
|
220
|
-
? []
|
|
221
|
-
: count.keys()
|
|
222
|
-
),
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}}
|
|
226
|
-
>
|
|
227
|
-
{
|
|
228
|
-
field.rule.value.size === count.size
|
|
229
|
-
? t('btn.unselect_all')
|
|
230
|
-
: t('btn.select_all')
|
|
231
|
-
}
|
|
232
|
-
</Button>
|
|
233
|
-
<Button
|
|
234
|
-
onClick={() => {
|
|
235
|
-
if (field.rule?.type === 'one of') {
|
|
236
|
-
const curSet = field.rule.value;
|
|
237
|
-
|
|
238
|
-
onChange({
|
|
239
|
-
type: 'one of',
|
|
240
|
-
value: new Set<number | string>(
|
|
241
|
-
[...count.keys()].filter(key => !curSet.has(key))
|
|
242
|
-
),
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}}
|
|
246
|
-
>
|
|
247
|
-
{t('btn.reverse')}
|
|
248
|
-
</Button>
|
|
249
|
-
</div>
|
|
250
279
|
</Container>
|
|
251
280
|
) : null;
|
|
252
281
|
});
|
|
253
282
|
|
|
283
|
+
interface CalendarInputProps {
|
|
284
|
+
min: number;
|
|
285
|
+
max: number;
|
|
286
|
+
value: number;
|
|
287
|
+
onChange: (value: number) => void;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const CalendarInput: React.FC<CalendarInputProps> = props => {
|
|
291
|
+
const { min, max, value, onChange } = props;
|
|
292
|
+
const dateStringFormatter = (timestamp: number) => {
|
|
293
|
+
return new Date(timestamp).toISOString().slice(0, 19);
|
|
294
|
+
}
|
|
295
|
+
const handleSubmitDate = (value) => {
|
|
296
|
+
if (new Date(value).getTime() <= max && new Date(value).getTime() >= min) {
|
|
297
|
+
onChange(new Date(value).getTime())
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return (
|
|
301
|
+
<input
|
|
302
|
+
className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
303
|
+
type="datetime-local"
|
|
304
|
+
min={dateStringFormatter(min)}
|
|
305
|
+
max={dateStringFormatter(max)}
|
|
306
|
+
defaultValue={dateStringFormatter(value)}
|
|
307
|
+
onChange={(e) => handleSubmitDate(e.target.value)}
|
|
308
|
+
/>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
254
312
|
export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
|
|
255
313
|
active,
|
|
256
314
|
field,
|
|
@@ -259,6 +317,8 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
|
|
|
259
317
|
const { commonStore } = useGlobalStore();
|
|
260
318
|
const { currentDataset: { dataSource } } = commonStore;
|
|
261
319
|
|
|
320
|
+
const { t } = useTranslation('translation');
|
|
321
|
+
|
|
262
322
|
const sorted = React.useMemo(() => {
|
|
263
323
|
return dataSource.reduce<number[]>((list, d) => {
|
|
264
324
|
try {
|
|
@@ -293,14 +353,29 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
|
|
|
293
353
|
}, []);
|
|
294
354
|
|
|
295
355
|
return field.rule?.type === 'temporal range' ? (
|
|
296
|
-
<Container>
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
356
|
+
<Container className="overflow-visible">
|
|
357
|
+
<div>{t('constant.filter_type.temporal_range')}</div>
|
|
358
|
+
<div className="text-gray-500">{t('constant.filter_type.temporal_range_desc')}</div>
|
|
359
|
+
<CalendarInputContainer>
|
|
360
|
+
<div className="calendar-input">
|
|
361
|
+
<div className="my-1">{t('filters.range.start_value')}</div>
|
|
362
|
+
<CalendarInput
|
|
363
|
+
min={min}
|
|
364
|
+
max={field.rule.value[1]}
|
|
365
|
+
value={field.rule.value[0]}
|
|
366
|
+
onChange={(value) => handleChange([value, field.rule?.value[1]])}
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="calendar-input">
|
|
370
|
+
<div className="my-1">{t('filters.range.end_value')}</div>
|
|
371
|
+
<CalendarInput
|
|
372
|
+
min={field.rule.value[0]}
|
|
373
|
+
max={max}
|
|
374
|
+
value={field.rule.value[1]}
|
|
375
|
+
onChange={(value) => handleChange([field.rule?.value[0], value])}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
</CalendarInputContainer>
|
|
304
379
|
</Container>
|
|
305
380
|
) : null;
|
|
306
381
|
});
|
|
@@ -313,6 +388,8 @@ export const FilterRangeRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
313
388
|
const { commonStore } = useGlobalStore();
|
|
314
389
|
const { currentDataset: { dataSource } } = commonStore;
|
|
315
390
|
|
|
391
|
+
const { t } = useTranslation('translation', { keyPrefix: 'constant.filter_type' });
|
|
392
|
+
|
|
316
393
|
const sorted = React.useMemo(() => {
|
|
317
394
|
return dataSource.map(d => d[field.fid]).sort((a, b) => a - b);
|
|
318
395
|
}, [dataSource, field]);
|
|
@@ -339,6 +416,8 @@ export const FilterRangeRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
339
416
|
|
|
340
417
|
return field.rule?.type === 'range' ? (
|
|
341
418
|
<Container>
|
|
419
|
+
<div>{t('range')}</div>
|
|
420
|
+
<div className="text-gray-500">{t('range_desc')}</div>
|
|
342
421
|
<Slider
|
|
343
422
|
min={min}
|
|
344
423
|
max={max}
|
|
@@ -355,6 +434,21 @@ const filterTabs: Record<IFilterRule['type'], React.FC<RuleFormProps & { active:
|
|
|
355
434
|
'temporal range': FilterTemporalRangeRule,
|
|
356
435
|
};
|
|
357
436
|
|
|
437
|
+
const tabOptionDict = {
|
|
438
|
+
"one of": {
|
|
439
|
+
key: "one_of",
|
|
440
|
+
descKey: "one_of_desc"
|
|
441
|
+
},
|
|
442
|
+
"range": {
|
|
443
|
+
key: "range",
|
|
444
|
+
descKey: "range_desc"
|
|
445
|
+
},
|
|
446
|
+
"temporal range": {
|
|
447
|
+
key: "temporal_range",
|
|
448
|
+
descKey: "temporal_range_desc"
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
358
452
|
export interface TabsProps extends RuleFormProps {
|
|
359
453
|
tabs: IFilterRule['type'][];
|
|
360
454
|
}
|
|
@@ -366,19 +460,42 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
|
366
460
|
const { t } = useTranslation('translation', { keyPrefix: 'constant.filter_type' });
|
|
367
461
|
|
|
368
462
|
const [which, setWhich] = React.useState(field.rule?.type ?? tabs[0]!);
|
|
369
|
-
|
|
463
|
+
React.useEffect(() => {
|
|
464
|
+
if (!tabs.includes(which)) setWhich(tabs[0]);
|
|
465
|
+
}, [tabs])
|
|
466
|
+
|
|
370
467
|
return (
|
|
371
468
|
<TabsContainer>
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
469
|
+
<div>
|
|
470
|
+
{
|
|
471
|
+
tabs.map((option) => {
|
|
472
|
+
return (
|
|
473
|
+
<div className="flex my-2" key={option}>
|
|
474
|
+
<div className="align-top">
|
|
475
|
+
<input
|
|
476
|
+
type="radio"
|
|
477
|
+
className="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
|
478
|
+
id={option}
|
|
479
|
+
checked={option === which}
|
|
480
|
+
onChange={e => setWhich((e.target as HTMLInputElement).value as typeof which)}
|
|
481
|
+
name="filter_type"
|
|
482
|
+
value={option}
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
<div className="ml-3">
|
|
486
|
+
<label htmlFor={option}>
|
|
487
|
+
{t(tabOptionDict[option].key)}
|
|
488
|
+
</label>
|
|
489
|
+
<div className="text-gray-500">
|
|
490
|
+
{t(tabOptionDict[option].descKey)}
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
)
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
</div>
|
|
498
|
+
<hr className="my-0.5"/>
|
|
382
499
|
<TabPanel>
|
|
383
500
|
{
|
|
384
501
|
tabs.map((tab, i) => {
|
|
@@ -387,8 +504,8 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
|
387
504
|
return draggableFieldState === null ? null : (
|
|
388
505
|
<TabItem
|
|
389
506
|
key={i}
|
|
390
|
-
id={`filter-panel-${tab
|
|
391
|
-
aria-labelledby={`filter-tab-${tab
|
|
507
|
+
id={`filter-panel-${tabOptionDict[tab]}`}
|
|
508
|
+
aria-labelledby={`filter-tab-${tabOptionDict[tab]}`}
|
|
392
509
|
role="tabpanel"
|
|
393
510
|
hidden={which !== tab}
|
|
394
511
|
tabIndex={0}
|
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef } from "react";
|
|
1
|
+
import React, { type ForwardedRef, forwardRef } from "react";
|
|
2
2
|
import { DOM } from "@kanaries/react-beautiful-dnd";
|
|
3
3
|
import { observer } from "mobx-react-lite";
|
|
4
4
|
import App, { IGWProps } from "./App";
|
|
@@ -6,7 +6,7 @@ import { StoreWrapper } from "./store/index";
|
|
|
6
6
|
import { FieldsContextWrapper } from "./fields/fieldsContext";
|
|
7
7
|
import { ShadowDom } from "./shadow-dom";
|
|
8
8
|
import AppRoot from "./components/appRoot";
|
|
9
|
-
import type { IGWHandler } from "./interfaces";
|
|
9
|
+
import type { IGWHandler, IGWHandlerInsider } from "./interfaces";
|
|
10
10
|
|
|
11
11
|
import "./empty_sheet.css";
|
|
12
12
|
|
|
@@ -24,7 +24,7 @@ export const GraphicWalker = observer(forwardRef<IGWHandler, IGWProps>((props, r
|
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
26
|
<StoreWrapper keepAlive={props.keepAlive} storeRef={storeRef}>
|
|
27
|
-
<AppRoot ref={ref}>
|
|
27
|
+
<AppRoot ref={ref as ForwardedRef<IGWHandlerInsider>}>
|
|
28
28
|
<ShadowDom onMount={handleMount} onUnmount={handleUnmount}>
|
|
29
29
|
<FieldsContextWrapper>
|
|
30
30
|
<App {...props} />
|