@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.
@@ -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
- marginBlock: '1em',
19
- marginInline: '2em',
15
+ const Container = styled.div`
16
+ margin-block: 1em;
20
17
 
21
- '> .btn-grp': {
22
- display: 'flex',
23
- flexDirection: 'row',
24
- marginBlock: '0.4em 0.6em',
18
+ > .btn-grp {
19
+ display: flex;
20
+ flex-direction: row;
21
+ margin-block: 1em;
25
22
 
26
- '> *': {
27
- marginInlineStart: '0.6em',
23
+ > * {
24
+ margin-inline-start: 0.6em;
28
25
 
29
- '&:first-child': {
30
- marginInlineStart: 0,
26
+ &:first-child: {
27
+ margin-inline-start: 0;
31
28
  },
32
29
  },
33
30
  },
34
- });
35
-
36
- export const Button = styled.button({
37
- '&:hover': {
38
- backgroundColor: 'rgba(243, 244, 246, 0.5)',
39
- },
40
- color: 'rgb(55, 65, 81)',
41
- boxShadow: '1px 1px 2px #0002, inset 2px 2px 4px #0001',
42
- paddingBlock: '0.2em',
43
- paddingInline: '0.5em',
44
- userSelect: 'none',
45
- cursor: 'pointer',
46
- });
47
-
48
- const Table = styled.div({
49
- display: 'grid',
50
- gridTemplateColumns: '4em auto max-content',
51
- maxHeight: '30vh',
52
- overflowY: 'scroll',
53
- '& > *': {
54
- marginBlock: '2px',
55
- paddingInline: '4px',
56
- whiteSpace: 'nowrap',
57
- overflow: 'hidden',
58
- textOverflow: 'ellipsis',
59
- userSelect: 'none',
60
- },
61
- '& > input, & > *[for]': {
62
- cursor: 'pointer',
63
- },
64
- });
65
-
66
- const TabsContainer = styled.div({
67
- display: 'flex',
68
- flexDirection: 'column',
69
- alignItems: 'stretch',
70
- justifyContent: 'stretch',
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
- const TabList = styled.div({
74
- display: 'flex',
75
- flexDirection: 'row',
76
- alignItems: 'stretch',
77
- justifyContent: 'flex-start',
78
- overflow: 'hidden',
79
- });
80
-
81
- const TabHeader = styled.label({
82
- outline: 'none',
83
- userSelect: 'none',
84
- paddingBlock: '0.4em',
85
- paddingInline: '1em 2em',
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
- const TabItem = styled.div({});
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', { keyPrefix: 'filters' });
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
- <Table>
138
- <label className="header">
139
- {t('header.visibility')}
140
- </label>
141
- <label className="header">
142
- {t('header.value')}
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
- <input
156
- type="checkbox"
157
- checked={field.rule?.type === 'one of' && field.rule.value.has(value)}
158
- id={id}
159
- aria-describedby={`${id}_label`}
160
- title={String(value)}
161
- onChange={({ target: { checked } }) => {
162
- if (field.rule?.type !== 'one of') {
163
- return;
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
- {[...field.rule.value].reduce<number>((sum, key) => {
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
- <Slider
298
- min={min}
299
- max={max}
300
- value={field.rule.value}
301
- onChange={handleChange}
302
- isDateTime
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
- <PureTabs
373
- selectedKey={which}
374
- tabs={tabs.map(tab => ({
375
- key: tab,
376
- label: t(tab.replaceAll(/ /g, '_')),
377
- }))}
378
- onSelected={sk => {
379
- setWhich(sk as typeof which);
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.replaceAll(/ /g, '_')}`}
391
- aria-labelledby={`filter-tab-${tab.replaceAll(/ /g, '_')}`}
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} />