@quillsql/react 1.5.0 → 1.5.2

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/src/Dashboard.tsx CHANGED
@@ -1,15 +1,21 @@
1
1
  // @ts-nocheck
2
- import React, { useContext, useEffect, useState } from 'react';
2
+ import React, { useContext, useEffect, useState, useRef } from 'react';
3
3
  import Chart from './Chart';
4
4
  import {
5
5
  ClientContext,
6
6
  DashboardContext,
7
7
  DateFilterContext,
8
8
  ThemeContext,
9
+ DashboardFiltersContext,
9
10
  } from './Context';
10
11
  import { startOfToday, sub } from 'date-fns';
11
12
  import { DateRangePicker } from './DateRangePicker/index';
12
13
  import axios from 'axios';
14
+ import Modal from './components/Modal/Modal';
15
+ import { HoveredValueContext, SelectedValueContext } from './contexts';
16
+ import { DropdownItem } from './components/Dropdown';
17
+ import { ArrowDownHeadIcon } from './assets';
18
+ import { useInternalState, useSelectOnKeyDown } from './hooks';
13
19
 
14
20
  interface DashboardProps {
15
21
  name?: string;
@@ -42,6 +48,8 @@ export default function Dashboard({
42
48
  const [client, _] = useContext(ClientContext);
43
49
  const [theme, _] = useContext(ThemeContext);
44
50
  const { dateFilter, dateFilterDispatch } = useContext(DateFilterContext);
51
+ const { dashboardFiltersDispatch } = useContext(DashboardFiltersContext);
52
+ const [configFilters, setConfigFilters] = useState([]);
45
53
 
46
54
  const setGlobalDateFilter = (startDate, endDate) => {
47
55
  dateFilterDispatch({
@@ -59,7 +67,7 @@ export default function Dashboard({
59
67
  // );
60
68
 
61
69
  const response = await axios.get(
62
- 'https://quill-344421.uc.r.appspot.com/dashsections',
70
+ 'https://quill-344421.uc.r.appspot.com/dashconfig',
63
71
  {
64
72
  params: {
65
73
  publicKey: publicKey,
@@ -76,7 +84,8 @@ export default function Dashboard({
76
84
  throw new Error(`HTTP error! Status: ${response.status}`);
77
85
  }
78
86
 
79
- setDashboardSections(response.data);
87
+ setDashboardSections(response.data.sections);
88
+ setConfigFilters(response.data.filters || []);
80
89
  } catch (error) {
81
90
  console.error('Error fetching data:', error);
82
91
  }
@@ -105,21 +114,69 @@ export default function Dashboard({
105
114
  }
106
115
  };
107
116
 
117
+ const onChangeFilter = (value, filter) => {
118
+ dashboardFiltersDispatch({
119
+ type: 'ADD_DASHBOARD_FILTER',
120
+ id: filter._id,
121
+ data: {
122
+ ...filter,
123
+ selectedValue: filter.options.find(
124
+ elem => elem[removeQuotes(filter.field)] === value[2]
125
+ )[removeQuotes(filter.field)],
126
+ },
127
+ });
128
+ };
129
+
108
130
  if (!dashboardSections) {
109
131
  return null;
110
132
  }
111
133
  return (
112
134
  <div style={containerStyle}>
113
- {!hideDateFilter ? (
114
- <div style={{ width: 420, marginBottom: 25, marginLeft: 25 }}>
115
- <DateRangePicker
116
- // change to be set on the dashboard / section as default date range
117
- defaultValue={[undefined, undefined, '90d']}
118
- onValueChange={onChangeDateFilter}
119
- theme={theme}
120
- />
135
+ <div
136
+ style={{
137
+ display: 'flex',
138
+ boxSizing: 'content-box',
139
+ flexDirection: 'row',
140
+ alignItems: 'center',
141
+ }}
142
+ >
143
+ {!hideDateFilter ? (
144
+ <div style={{ width: 420, marginBottom: 25, marginLeft: 25 }}>
145
+ <div
146
+ style={{
147
+ marginBottom: 6,
148
+ fontWeight: '600',
149
+ color: theme.secondaryTextColor,
150
+ fontFamily: theme.fontFamily,
151
+ fontSize: 14,
152
+ }}
153
+ >
154
+ Date
155
+ </div>
156
+ <DateRangePicker
157
+ // change to be set on the dashboard / section as default date range
158
+ defaultValue={[undefined, undefined, '90d']}
159
+ onValueChange={onChangeDateFilter}
160
+ theme={theme}
161
+ />
162
+ </div>
163
+ ) : null}
164
+ <div style={{ width: 280, marginBottom: 25, marginLeft: 25 }}>
165
+ {configFilters.map((elem, index) => (
166
+ <Filter
167
+ key={'filter' + configFilters.field + index}
168
+ onValueChange={value => onChangeFilter(value, elem)}
169
+ defaultValue={[
170
+ undefined,
171
+ undefined,
172
+ elem.options[removeQuotes(elem.field)],
173
+ ]}
174
+ theme={theme}
175
+ filter={elem}
176
+ />
177
+ ))}
121
178
  </div>
122
- ) : null}
179
+ </div>
123
180
  {Object.keys(dashboardSections)
124
181
  .sort(function (a, b) {
125
182
  return a.length - b.length;
@@ -504,3 +561,215 @@ export default function Dashboard({
504
561
  </div>
505
562
  );
506
563
  }
564
+
565
+ function Filter({ defaultValue, theme, onValueChange, filter }) {
566
+ const dropdownOptions = [
567
+ { value: 'popupbagelsmardens', text: 'popupbagelsmardens' },
568
+ { value: 'popupbagelswestport', text: 'popupbagelswestport' },
569
+ { value: 'popupbagelseasthampton', text: 'popupbagelseasthampton' },
570
+ { value: 'popupbagelsgreenwich', text: 'popupbagelsgreenwich' },
571
+ { value: 'popupbagelsredding', text: 'popupbagelsredding' },
572
+ { value: 'popupbagelsthompson', text: 'popupbagelsthompson' },
573
+ ];
574
+ const dropdownRef = useRef(null);
575
+ const [showDropdown, setShowDropdown] = useState(false);
576
+ const [selectedValue, setSelectedValue] = useInternalState(
577
+ defaultValue,
578
+ undefined
579
+ );
580
+ const selectedDropdownValue = selectedValue ? selectedValue[2] ?? null : null;
581
+
582
+ const handleDropdownOptionClick = (dropdownValue: string) => {
583
+ setSelectedValue([undefined, undefined, dropdownValue]);
584
+ onValueChange?.([undefined, undefined, dropdownValue]);
585
+ setShowDropdown(false);
586
+ };
587
+
588
+ const [hoveredDropdownValue, handleDropdownKeyDown] = useSelectOnKeyDown(
589
+ handleDropdownOptionClick,
590
+ dropdownOptions.map((option: DateRangePickerOption) => option.value),
591
+ showDropdown,
592
+ setShowDropdown,
593
+ selectedDropdownValue as string
594
+ );
595
+
596
+ return (
597
+ <div style={{ position: 'relative', width: '100%' }}>
598
+ <div
599
+ style={{
600
+ marginBottom: 6,
601
+ fontWeight: '600',
602
+ color: theme.secondaryTextColor,
603
+ fontSize: 14,
604
+ fontFamily: theme.fontFamily,
605
+ }}
606
+ >
607
+ {filter.label}
608
+ </div>
609
+ <FilterDropdown
610
+ showDropdown={showDropdown}
611
+ setShowDropdown={setShowDropdown}
612
+ handleDropdownKeyDown={handleDropdownKeyDown}
613
+ dropdownRef={dropdownRef}
614
+ theme={theme}
615
+ selectedDropdownValue={selectedDropdownValue}
616
+ dropdownOptions={filter.options}
617
+ field={filter.field}
618
+ label={filter.labelField}
619
+ />
620
+ <FilterModal
621
+ showDropdown={showDropdown}
622
+ setShowDropdown={setShowDropdown}
623
+ dropdownRef={dropdownRef}
624
+ theme={theme}
625
+ hoveredDropdownValue={hoveredDropdownValue}
626
+ selectedDropdownValue={selectedDropdownValue}
627
+ dropdownOptions={filter.options}
628
+ handleDropdownOptionClick={handleDropdownOptionClick}
629
+ field={filter.field}
630
+ label={filter.labelField}
631
+ />
632
+ </div>
633
+ );
634
+ }
635
+
636
+ function removeQuotes(str) {
637
+ if (str.startsWith('"') && str.endsWith('"')) {
638
+ return str.slice(1, -1);
639
+ } else {
640
+ return str;
641
+ }
642
+ }
643
+
644
+ function FilterDropdown({
645
+ setShowDropdown,
646
+ dropdownRef,
647
+ showDropdown,
648
+ handleDropdownKeyDown,
649
+ theme,
650
+ dropdownPlaceholder = 'Select',
651
+ selectedDropdownValue,
652
+ dropdownOptions,
653
+ field,
654
+ label,
655
+ }) {
656
+ const dropdownText = selectedDropdownValue
657
+ ? String(
658
+ dropdownOptions.find(
659
+ option => option[removeQuotes(field)] === selectedDropdownValue
660
+ )[removeQuotes(label)]
661
+ )
662
+ : dropdownPlaceholder;
663
+ return (
664
+ <div
665
+ style={{
666
+ display: 'flex',
667
+ alignItems: 'center',
668
+ justifyContent: 'space-between',
669
+ borderRadius: '0.375rem',
670
+ background: theme?.backgroundColor,
671
+ fontFamily: theme?.fontFamily,
672
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
673
+ }}
674
+ >
675
+ <button
676
+ type="button"
677
+ style={{
678
+ // fontFamily: theme?.fontFamily,
679
+ borderColor: theme?.borderColor || '#E5E7EB',
680
+ borderStyle: 'solid',
681
+ borderWidth: 1,
682
+ cursor: 'pointer',
683
+ marginLeft: -1,
684
+ borderRadius: '0.375rem',
685
+ // width: '12rem',
686
+ width: '100%',
687
+ overflow: 'hidden',
688
+ textOverflow: 'ellipsis',
689
+ whiteSpace: 'nowrap',
690
+ paddingLeft: '1rem',
691
+ paddingRight: '1rem',
692
+ display: 'inline-flex',
693
+ minHeight: 38,
694
+ justifyContent: 'space-between',
695
+ alignItems: 'center',
696
+ background: theme?.backgroundColor,
697
+ fontSize: theme?.fontSizeSmall || '0.875rem',
698
+ // fontWeight: theme?.fontWeightMedium || '800',
699
+ }}
700
+ ref={dropdownRef}
701
+ onClick={() => setShowDropdown(!showDropdown)}
702
+ onKeyDown={handleDropdownKeyDown}
703
+ // disabled={disabled}
704
+ >
705
+ <p
706
+ style={{
707
+ margin: 0,
708
+ fontFamily: theme?.fontFamily,
709
+ color: theme?.primaryTextColor || '#364153',
710
+ overflow: 'hidden',
711
+ textOverflow: 'ellipsis',
712
+ whiteSpace: 'nowrap',
713
+ fontWeight: theme?.fontWeightMedium || '500',
714
+ fontSize: theme?.fontSizeSmall || '0.875rem',
715
+ }}
716
+ >
717
+ {dropdownText}
718
+ </p>
719
+ <ArrowDownHeadIcon
720
+ style={{
721
+ height: '1.25rem',
722
+ width: '1.25rem',
723
+ flex: 'none',
724
+ color: theme?.secondaryTextColor,
725
+ marginRight: '-0.25rem',
726
+ }}
727
+ aria-hidden="true"
728
+ />
729
+ </button>
730
+ </div>
731
+ );
732
+ }
733
+
734
+ function FilterModal({
735
+ setShowDropdown,
736
+ dropdownRef,
737
+ showDropdown,
738
+ theme,
739
+ selectedDropdownValue,
740
+ hoveredDropdownValue,
741
+ dropdownOptions,
742
+ handleDropdownOptionClick,
743
+ field,
744
+ label,
745
+ }) {
746
+ return (
747
+ <Modal
748
+ showModal={showDropdown}
749
+ setShowModal={setShowDropdown}
750
+ parentRef={dropdownRef}
751
+ theme={theme}
752
+ >
753
+ <SelectedValueContext.Provider
754
+ value={{
755
+ selectedValue: selectedDropdownValue,
756
+ handleValueChange: handleDropdownOptionClick,
757
+ }}
758
+ >
759
+ <HoveredValueContext.Provider
760
+ value={{ hoveredValue: hoveredDropdownValue }}
761
+ >
762
+ {dropdownOptions.map((row, index: number) => (
763
+ <DropdownItem
764
+ key={row[removeQuotes(field)]}
765
+ value={row[removeQuotes(field)]}
766
+ text={row[removeQuotes(label)]}
767
+ theme={theme}
768
+ lastItem={dropdownOptions.length - 1 === index}
769
+ />
770
+ ))}
771
+ </HoveredValueContext.Provider>
772
+ </SelectedValueContext.Provider>
773
+ </Modal>
774
+ );
775
+ }
@@ -32,7 +32,7 @@ const DropdownItem = React.forwardRef<HTMLButtonElement, DropdownItemProps>(
32
32
  width: '100%',
33
33
  overflow: 'hidden',
34
34
  textOverflow: 'ellipsis',
35
- padding: '0.75rem 2rem',
35
+ padding: '0.75rem 1.25rem',
36
36
  fontSize: '0.875rem',
37
37
  borderStyle: 'solid',
38
38
  borderBottomWidth: lastItem ? '0px' : '1px',
@@ -2,40 +2,63 @@ import { useContext, useEffect, useState } from 'react';
2
2
  import { ClientContext, DashboardContext } from '../Context';
3
3
  import axios from 'axios';
4
4
 
5
- export const useQuill = (chartId: string) => {
5
+ interface Report {
6
+ name: string;
7
+ rows: any[];
8
+ columns: Array<{ name: string }>;
9
+ }
10
+
11
+ export const useQuill = (
12
+ chartId: string
13
+ ): {
14
+ data: Report | null;
15
+ loading: boolean;
16
+ error: string | null;
17
+ } => {
6
18
  // @ts-ignore
7
19
  const { dashboard, dispatch } = useContext(DashboardContext);
8
20
  const [client] = useContext(ClientContext);
9
- const [report, setReport] = useState(null);
21
+ const [data, setData] = useState<Report | null>(null);
22
+ const [loading, setLoading] = useState(true);
23
+ const [error, setError] = useState<string | null>(null);
10
24
 
11
25
  useEffect(() => {
12
26
  let isSubscribed = true;
13
27
  async function getChartOptions(id: string) {
14
28
  if (isSubscribed) {
15
29
  if (dashboard[id]) {
16
- setReport(dashboard[id]);
30
+ setData(dashboard[id]);
17
31
  return;
18
32
  }
19
33
  // @ts-ignore
20
34
  const { publicKey, customerId, environment } = client;
21
- const resp = await axios.get(
22
- 'https://quill-344421.uc.r.appspot.com/item',
23
- {
24
- params: {
25
- id: chartId,
26
- orgId: customerId,
27
- publicKey: publicKey,
28
- },
29
- headers: {
30
- environment: environment || undefined,
31
- },
35
+ try {
36
+ const resp = await axios.get(
37
+ 'https://quill-344421.uc.r.appspot.com/item',
38
+ {
39
+ params: {
40
+ id: chartId,
41
+ orgId: customerId,
42
+ publicKey: publicKey,
43
+ },
44
+ headers: {
45
+ environment: environment || undefined,
46
+ },
47
+ }
48
+ );
49
+ setLoading(false);
50
+ setData(resp.data);
51
+ dispatch({ type: 'UPDATE_DASHBOARD_ITEM', id, data: resp.data });
52
+ } catch (e) {
53
+ if (typeof e === 'string' || (typeof e === 'object' && e !== null)) {
54
+ setError(stringifyIfObject(e));
55
+ } else {
56
+ setError('error');
32
57
  }
33
- );
34
- setReport(resp.data);
35
- dispatch({ type: 'UPDATE_DASHBOARD_ITEM', id, data: resp.data });
58
+ }
36
59
  }
37
60
  }
38
- if (chartId && !report) {
61
+ if (chartId && !data) {
39
62
  getChartOptions(chartId);
40
63
  }
41
64
  return () => {
@@ -43,5 +66,16 @@ export const useQuill = (chartId: string) => {
43
66
  };
44
67
  }, [chartId, dashboard]);
45
68
 
46
- return report;
69
+ return { data, loading, error };
47
70
  };
71
+
72
+ function stringifyIfObject(e: string | object) {
73
+ if (typeof e === 'string') {
74
+ // do nothing if e is a string
75
+ return e;
76
+ } else if (typeof e === 'object' && e !== null) {
77
+ // stringify e if it's an object
78
+ return JSON.stringify(e);
79
+ }
80
+ return e; // returns the input as is for other types
81
+ }