@quillsql/react 1.5.0 → 1.5.1

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,68 @@ 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
+ fontSize: 14,
151
+ }}
152
+ >
153
+ Date
154
+ </div>
155
+ <DateRangePicker
156
+ // change to be set on the dashboard / section as default date range
157
+ defaultValue={[undefined, undefined, '90d']}
158
+ onValueChange={onChangeDateFilter}
159
+ theme={theme}
160
+ />
161
+ </div>
162
+ ) : null}
163
+ <div style={{ width: 280, marginBottom: 25, marginLeft: 25 }}>
164
+ {configFilters.map((elem, index) => (
165
+ <Filter
166
+ key={'filter' + configFilters.field + index}
167
+ onValueChange={value => onChangeFilter(value, elem)}
168
+ defaultValue={[
169
+ undefined,
170
+ undefined,
171
+ elem.options[removeQuotes(elem.field)],
172
+ ]}
173
+ theme={theme}
174
+ filter={elem}
175
+ />
176
+ ))}
121
177
  </div>
122
- ) : null}
178
+ </div>
123
179
  {Object.keys(dashboardSections)
124
180
  .sort(function (a, b) {
125
181
  return a.length - b.length;
@@ -504,3 +560,214 @@ export default function Dashboard({
504
560
  </div>
505
561
  );
506
562
  }
563
+
564
+ function Filter({ defaultValue, theme, onValueChange, filter }) {
565
+ const dropdownOptions = [
566
+ { value: 'popupbagelsmardens', text: 'popupbagelsmardens' },
567
+ { value: 'popupbagelswestport', text: 'popupbagelswestport' },
568
+ { value: 'popupbagelseasthampton', text: 'popupbagelseasthampton' },
569
+ { value: 'popupbagelsgreenwich', text: 'popupbagelsgreenwich' },
570
+ { value: 'popupbagelsredding', text: 'popupbagelsredding' },
571
+ { value: 'popupbagelsthompson', text: 'popupbagelsthompson' },
572
+ ];
573
+ const dropdownRef = useRef(null);
574
+ const [showDropdown, setShowDropdown] = useState(false);
575
+ const [selectedValue, setSelectedValue] = useInternalState(
576
+ defaultValue,
577
+ undefined
578
+ );
579
+ const selectedDropdownValue = selectedValue ? selectedValue[2] ?? null : null;
580
+
581
+ const handleDropdownOptionClick = (dropdownValue: string) => {
582
+ setSelectedValue([undefined, undefined, dropdownValue]);
583
+ onValueChange?.([undefined, undefined, dropdownValue]);
584
+ setShowDropdown(false);
585
+ };
586
+
587
+ const [hoveredDropdownValue, handleDropdownKeyDown] = useSelectOnKeyDown(
588
+ handleDropdownOptionClick,
589
+ dropdownOptions.map((option: DateRangePickerOption) => option.value),
590
+ showDropdown,
591
+ setShowDropdown,
592
+ selectedDropdownValue as string
593
+ );
594
+
595
+ return (
596
+ <div style={{ position: 'relative', width: '100%' }}>
597
+ <div
598
+ style={{
599
+ marginBottom: 6,
600
+ fontWeight: '600',
601
+ color: theme.secondaryTextColor,
602
+ fontSize: 14,
603
+ }}
604
+ >
605
+ {filter.label}
606
+ </div>
607
+ <FilterDropdown
608
+ showDropdown={showDropdown}
609
+ setShowDropdown={setShowDropdown}
610
+ handleDropdownKeyDown={handleDropdownKeyDown}
611
+ dropdownRef={dropdownRef}
612
+ theme={theme}
613
+ selectedDropdownValue={selectedDropdownValue}
614
+ dropdownOptions={filter.options}
615
+ field={filter.field}
616
+ label={filter.labelField}
617
+ />
618
+ <FilterModal
619
+ showDropdown={showDropdown}
620
+ setShowDropdown={setShowDropdown}
621
+ dropdownRef={dropdownRef}
622
+ theme={theme}
623
+ hoveredDropdownValue={hoveredDropdownValue}
624
+ selectedDropdownValue={selectedDropdownValue}
625
+ dropdownOptions={filter.options}
626
+ handleDropdownOptionClick={handleDropdownOptionClick}
627
+ field={filter.field}
628
+ label={filter.labelField}
629
+ />
630
+ </div>
631
+ );
632
+ }
633
+
634
+ function removeQuotes(str) {
635
+ if (str.startsWith('"') && str.endsWith('"')) {
636
+ return str.slice(1, -1);
637
+ } else {
638
+ return str;
639
+ }
640
+ }
641
+
642
+ function FilterDropdown({
643
+ setShowDropdown,
644
+ dropdownRef,
645
+ showDropdown,
646
+ handleDropdownKeyDown,
647
+ theme,
648
+ dropdownPlaceholder = 'Select',
649
+ selectedDropdownValue,
650
+ dropdownOptions,
651
+ field,
652
+ label,
653
+ }) {
654
+ const dropdownText = selectedDropdownValue
655
+ ? String(
656
+ dropdownOptions.find(
657
+ option => option[removeQuotes(field)] === selectedDropdownValue
658
+ )[removeQuotes(label)]
659
+ )
660
+ : dropdownPlaceholder;
661
+ return (
662
+ <div
663
+ style={{
664
+ display: 'flex',
665
+ alignItems: 'center',
666
+ justifyContent: 'space-between',
667
+ borderRadius: '0.375rem',
668
+ background: theme?.backgroundColor,
669
+ fontFamily: theme?.fontFamily,
670
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
671
+ }}
672
+ >
673
+ <button
674
+ type="button"
675
+ style={{
676
+ // fontFamily: theme?.fontFamily,
677
+ borderColor: theme?.borderColor || '#E5E7EB',
678
+ borderStyle: 'solid',
679
+ borderWidth: 1,
680
+ cursor: 'pointer',
681
+ marginLeft: -1,
682
+ borderRadius: '0.375rem',
683
+ // width: '12rem',
684
+ width: '100%',
685
+ overflow: 'hidden',
686
+ textOverflow: 'ellipsis',
687
+ whiteSpace: 'nowrap',
688
+ paddingLeft: '1rem',
689
+ paddingRight: '1rem',
690
+ display: 'inline-flex',
691
+ minHeight: 38,
692
+ justifyContent: 'space-between',
693
+ alignItems: 'center',
694
+ background: theme?.backgroundColor,
695
+ fontSize: theme?.fontSizeSmall || '0.875rem',
696
+ // fontWeight: theme?.fontWeightMedium || '800',
697
+ }}
698
+ ref={dropdownRef}
699
+ onClick={() => setShowDropdown(!showDropdown)}
700
+ onKeyDown={handleDropdownKeyDown}
701
+ // disabled={disabled}
702
+ >
703
+ <p
704
+ style={{
705
+ margin: 0,
706
+ fontFamily: theme?.fontFamily,
707
+ color: theme?.primaryTextColor || '#364153',
708
+ overflow: 'hidden',
709
+ textOverflow: 'ellipsis',
710
+ whiteSpace: 'nowrap',
711
+ fontWeight: theme?.fontWeightMedium || '500',
712
+ fontSize: theme?.fontSizeSmall || '0.875rem',
713
+ }}
714
+ >
715
+ {dropdownText}
716
+ </p>
717
+ <ArrowDownHeadIcon
718
+ style={{
719
+ height: '1.25rem',
720
+ width: '1.25rem',
721
+ flex: 'none',
722
+ color: theme?.secondaryTextColor,
723
+ marginRight: '-0.25rem',
724
+ }}
725
+ aria-hidden="true"
726
+ />
727
+ </button>
728
+ </div>
729
+ );
730
+ }
731
+
732
+ function FilterModal({
733
+ setShowDropdown,
734
+ dropdownRef,
735
+ showDropdown,
736
+ theme,
737
+ selectedDropdownValue,
738
+ hoveredDropdownValue,
739
+ dropdownOptions,
740
+ handleDropdownOptionClick,
741
+ field,
742
+ label,
743
+ }) {
744
+ return (
745
+ <Modal
746
+ showModal={showDropdown}
747
+ setShowModal={setShowDropdown}
748
+ parentRef={dropdownRef}
749
+ theme={theme}
750
+ >
751
+ <SelectedValueContext.Provider
752
+ value={{
753
+ selectedValue: selectedDropdownValue,
754
+ handleValueChange: handleDropdownOptionClick,
755
+ }}
756
+ >
757
+ <HoveredValueContext.Provider
758
+ value={{ hoveredValue: hoveredDropdownValue }}
759
+ >
760
+ {dropdownOptions.map((row, index: number) => (
761
+ <DropdownItem
762
+ key={row[removeQuotes(field)]}
763
+ value={row[removeQuotes(field)]}
764
+ text={row[removeQuotes(label)]}
765
+ theme={theme}
766
+ lastItem={dropdownOptions.length - 1 === index}
767
+ />
768
+ ))}
769
+ </HoveredValueContext.Provider>
770
+ </SelectedValueContext.Provider>
771
+ </Modal>
772
+ );
773
+ }
@@ -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
+ }