@visns-studio/visns-components 5.6.1 → 5.6.3

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/package.json CHANGED
@@ -84,7 +84,7 @@
84
84
  "react-dom": "^17.0.0 || ^18.0.0"
85
85
  },
86
86
  "name": "@visns-studio/visns-components",
87
- "version": "5.6.1",
87
+ "version": "5.6.3",
88
88
  "description": "Various packages to assist in the development of our Custom Applications.",
89
89
  "main": "src/index.js",
90
90
  "files": [
@@ -9,6 +9,7 @@ function TableFilter({
9
9
  type,
10
10
  }) {
11
11
  const [visibleChildren, setVisibleChildren] = useState(null);
12
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
12
13
  const isFirstRender = useRef(true);
13
14
 
14
15
  useEffect(() => {
@@ -116,6 +117,9 @@ function TableFilter({
116
117
  if (collapsible && filtertype === 'parent') {
117
118
  setVisibleChildren(id);
118
119
  }
120
+
121
+ // Close mobile menu after selection on small screens
122
+ setMobileMenuOpen(false);
119
123
  };
120
124
 
121
125
  const renderContent = (d) => {
@@ -184,14 +188,40 @@ function TableFilter({
184
188
  }
185
189
  };
186
190
 
191
+ const toggleMobileMenu = () => {
192
+ setMobileMenuOpen(!mobileMenuOpen);
193
+ };
194
+
195
+ // Find active filter for mobile display
196
+ const activeFilter = filters.find((filter) => filter.show === true);
197
+ const activeLabel = activeFilter ? activeFilter.label : 'Select Option';
198
+
187
199
  return (
188
- <ul className={styles.tableFilter}>
189
- {filters.map((item, key) => (
190
- <React.Fragment key={'table-filter-' + key}>
191
- {renderContent(item)}
192
- </React.Fragment>
193
- ))}
194
- </ul>
200
+ <div className={styles.tableFilterContainer}>
201
+ <div className={styles.mobileToggle} onClick={toggleMobileMenu}>
202
+ <span className={styles.activeFilterLabel}>{activeLabel}</span>
203
+ <span
204
+ className={`${styles.mobileMenuIcon} ${
205
+ mobileMenuOpen ? styles.open : ''
206
+ }`}
207
+ >
208
+ <span></span>
209
+ <span></span>
210
+ <span></span>
211
+ </span>
212
+ </div>
213
+ <ul
214
+ className={`${styles.tableFilter} ${
215
+ mobileMenuOpen ? styles.mobileOpen : ''
216
+ }`}
217
+ >
218
+ {filters.map((item, key) => (
219
+ <React.Fragment key={'table-filter-' + key}>
220
+ {renderContent(item)}
221
+ </React.Fragment>
222
+ ))}
223
+ </ul>
224
+ </div>
195
225
  );
196
226
  }
197
227
 
@@ -474,11 +474,17 @@ const GenericReport = ({ setting = {} }) => {
474
474
  const [gridColumns, setGridColumns] = useState([]);
475
475
  const [isLoading, setIsLoading] = useState(false);
476
476
 
477
+ // State for JSON field keys
478
+ const [jsonFieldKeys, setJsonFieldKeys] = useState({});
479
+ const [loadingJsonKeys, setLoadingJsonKeys] = useState({});
480
+ const [showJsonKeySelector, setShowJsonKeySelector] = useState({});
481
+
477
482
  // Extract additional settings with defaults
478
483
  const {
479
484
  reportsUrl = '/ajax/reportBuilder/reports',
480
485
  executeUrl = '/ajax/reportBuilder/execute',
481
486
  suggestedJoinsUrl = '/ajax/reportBuilder/getSuggestedJoins',
487
+ jsonFieldKeysUrl = '/ajax/reportBuilder/getJsonFieldKeys',
482
488
  } = setting;
483
489
 
484
490
  // Fetch all database tables and saved reports on component mount
@@ -1051,6 +1057,118 @@ const GenericReport = ({ setting = {} }) => {
1051
1057
  setShowFilteringSection(true);
1052
1058
  };
1053
1059
 
1060
+ // Fetch JSON field keys for a specific table and column
1061
+ const fetchJsonFieldKeys = async (tableName, columnName) => {
1062
+ const cacheKey = `${tableName}.${columnName}`;
1063
+
1064
+ // Check if we're already loading this data
1065
+ if (loadingJsonKeys[cacheKey]) {
1066
+ return;
1067
+ }
1068
+
1069
+ // Check if we already have this data cached
1070
+ if (jsonFieldKeys[cacheKey]) {
1071
+ return;
1072
+ }
1073
+
1074
+ // Set loading state
1075
+ setLoadingJsonKeys((prev) => ({
1076
+ ...prev,
1077
+ [cacheKey]: true,
1078
+ }));
1079
+
1080
+ try {
1081
+ const res = await CustomFetch(jsonFieldKeysUrl, 'POST', {
1082
+ table: tableName,
1083
+ column: columnName,
1084
+ limit: 100, // Analyze up to 100 records
1085
+ });
1086
+
1087
+ if (res.data?.success && res.data?.data?.keys) {
1088
+ // Store the keys in state
1089
+ setJsonFieldKeys((prev) => ({
1090
+ ...prev,
1091
+ [cacheKey]: res.data.data.keys,
1092
+ }));
1093
+ } else {
1094
+ console.warn(
1095
+ `No JSON keys found for ${tableName}.${columnName} or invalid response format`
1096
+ );
1097
+ // Set empty array for this field
1098
+ setJsonFieldKeys((prev) => ({
1099
+ ...prev,
1100
+ [cacheKey]: [],
1101
+ }));
1102
+ }
1103
+ } catch (error) {
1104
+ console.error(
1105
+ `Error fetching JSON field keys for ${tableName}.${columnName}:`,
1106
+ error
1107
+ );
1108
+ toast.error(
1109
+ `Failed to load JSON field keys for ${formatName(
1110
+ tableName
1111
+ )}.${formatName(columnName)}`
1112
+ );
1113
+ // Set empty array for this field on error
1114
+ setJsonFieldKeys((prev) => ({
1115
+ ...prev,
1116
+ [cacheKey]: [],
1117
+ }));
1118
+ } finally {
1119
+ // Update loading state
1120
+ setLoadingJsonKeys((prev) => ({
1121
+ ...prev,
1122
+ [cacheKey]: false,
1123
+ }));
1124
+ }
1125
+ };
1126
+
1127
+ // Check if a column is a JSON type
1128
+ const isJsonColumn = (tableName, columnName) => {
1129
+ // First check the main table
1130
+ if (tableName === selectedTable) {
1131
+ const column = tableColumns.find((col) => col.name === columnName);
1132
+ return (
1133
+ column && column.type && column.type.toLowerCase() === 'json'
1134
+ );
1135
+ }
1136
+
1137
+ // Then check joined tables
1138
+ for (const join of joins) {
1139
+ if (join.targetTable === tableName && join.availableColumns) {
1140
+ const column = join.availableColumns.find(
1141
+ (col) => col.name === columnName
1142
+ );
1143
+ return (
1144
+ column &&
1145
+ column.type &&
1146
+ column.type.toLowerCase() === 'json'
1147
+ );
1148
+ }
1149
+ }
1150
+
1151
+ return false;
1152
+ };
1153
+
1154
+ // Toggle JSON key selector visibility
1155
+ const toggleJsonKeySelector = (groupIndex, filterIndex) => {
1156
+ const key = `${groupIndex}-${filterIndex}`;
1157
+ setShowJsonKeySelector((prev) => ({
1158
+ ...prev,
1159
+ [key]: !prev[key],
1160
+ }));
1161
+ };
1162
+
1163
+ // Handle JSON key selection
1164
+ const handleJsonKeySelect = (groupIndex, filterIndex, key) => {
1165
+ // Update the filter value with the selected JSON key
1166
+ handleFilterCriterionChange(groupIndex, filterIndex, 'jsonKey', key);
1167
+
1168
+ // Hide the selector
1169
+ toggleJsonKeySelector(groupIndex, filterIndex);
1170
+ };
1171
+
1054
1172
  // Update a filter criterion
1055
1173
  const handleFilterCriterionChange = (
1056
1174
  groupIndex,
@@ -1067,6 +1185,34 @@ const GenericReport = ({ setting = {} }) => {
1067
1185
  updatedFilterCriteria.groups[groupIndex].filters[filterIndex][field] =
1068
1186
  value;
1069
1187
 
1188
+ // If we're changing the table or column, check if it's a JSON field
1189
+ if (field === 'table' || field === 'column') {
1190
+ const filter =
1191
+ updatedFilterCriteria.groups[groupIndex].filters[filterIndex];
1192
+ const tableName = field === 'table' ? value : filter.table;
1193
+ const columnName = field === 'column' ? value : filter.column;
1194
+
1195
+ // If both table and column are set, check if it's a JSON field
1196
+ if (tableName && columnName) {
1197
+ const isJson = isJsonColumn(tableName, columnName);
1198
+
1199
+ // If it's a JSON field, fetch the keys
1200
+ if (isJson) {
1201
+ fetchJsonFieldKeys(tableName, columnName);
1202
+
1203
+ // Add a jsonKey field if it doesn't exist
1204
+ if (!filter.jsonKey) {
1205
+ filter.jsonKey = '';
1206
+ }
1207
+ } else {
1208
+ // If it's not a JSON field, remove the jsonKey field if it exists
1209
+ if (filter.jsonKey) {
1210
+ delete filter.jsonKey;
1211
+ }
1212
+ }
1213
+ }
1214
+ }
1215
+
1070
1216
  setFilterCriteria(updatedFilterCriteria);
1071
1217
  };
1072
1218
 
@@ -3615,31 +3761,217 @@ const GenericReport = ({ setting = {} }) => {
3615
3761
  styles.joinField
3616
3762
  }
3617
3763
  >
3618
- <label>
3619
- Value:
3620
- </label>
3621
- <input
3622
- type="text"
3623
- className={
3624
- styles.formControl
3625
- }
3626
- value={
3627
- criterion.value
3628
- }
3629
- onChange={(
3630
- e
3631
- ) =>
3632
- handleFilterCriterionChange(
3633
- groupIndex,
3634
- filterIndex,
3635
- 'value',
3636
- e
3637
- .target
3638
- .value
3639
- )
3640
- }
3641
- placeholder="Enter filter value"
3642
- />
3764
+ {/* Check if this is a JSON field */}
3765
+ {criterion.table &&
3766
+ criterion.column &&
3767
+ isJsonColumn(
3768
+ criterion.table,
3769
+ criterion.column
3770
+ ) ? (
3771
+ <>
3772
+ <label>
3773
+ JSON
3774
+ Path:
3775
+ </label>
3776
+ <div
3777
+ className={
3778
+ styles.jsonKeyInputContainer
3779
+ }
3780
+ >
3781
+ <input
3782
+ type="text"
3783
+ className={
3784
+ styles.formControl
3785
+ }
3786
+ value={
3787
+ criterion.jsonKey ||
3788
+ ''
3789
+ }
3790
+ onChange={(
3791
+ e
3792
+ ) =>
3793
+ handleFilterCriterionChange(
3794
+ groupIndex,
3795
+ filterIndex,
3796
+ 'jsonKey',
3797
+ e
3798
+ .target
3799
+ .value
3800
+ )
3801
+ }
3802
+ placeholder="Enter JSON path (e.g., data.name)"
3803
+ />
3804
+ <button
3805
+ type="button"
3806
+ className={
3807
+ styles.jsonKeyButton
3808
+ }
3809
+ onClick={() =>
3810
+ toggleJsonKeySelector(
3811
+ groupIndex,
3812
+ filterIndex
3813
+ )
3814
+ }
3815
+ title="Select from available JSON keys"
3816
+ >
3817
+
3818
+ </button>
3819
+ </div>
3820
+
3821
+ {/* JSON key selector dropdown */}
3822
+ {showJsonKeySelector[
3823
+ `${groupIndex}-${filterIndex}`
3824
+ ] && (
3825
+ <div
3826
+ className={
3827
+ styles.jsonKeyDropdown
3828
+ }
3829
+ >
3830
+ {loadingJsonKeys[
3831
+ `${criterion.table}.${criterion.column}`
3832
+ ] ? (
3833
+ <div
3834
+ className={
3835
+ styles.jsonKeyLoading
3836
+ }
3837
+ >
3838
+ Loading
3839
+ available
3840
+ JSON
3841
+ keys...
3842
+ </div>
3843
+ ) : jsonFieldKeys[
3844
+ `${criterion.table}.${criterion.column}`
3845
+ ]
3846
+ ?.length >
3847
+ 0 ? (
3848
+ <div
3849
+ className={
3850
+ styles.jsonKeyList
3851
+ }
3852
+ >
3853
+ {jsonFieldKeys[
3854
+ `${criterion.table}.${criterion.column}`
3855
+ ].map(
3856
+ (
3857
+ keyInfo,
3858
+ idx
3859
+ ) => (
3860
+ <div
3861
+ key={
3862
+ idx
3863
+ }
3864
+ className={
3865
+ styles.jsonKeyItem
3866
+ }
3867
+ onClick={() =>
3868
+ handleJsonKeySelect(
3869
+ groupIndex,
3870
+ filterIndex,
3871
+ keyInfo.key
3872
+ )
3873
+ }
3874
+ title={`Used in ${keyInfo.percentage}% of records. Example: ${keyInfo.example}`}
3875
+ >
3876
+ <span
3877
+ className={
3878
+ styles.jsonKeyName
3879
+ }
3880
+ >
3881
+ {
3882
+ keyInfo.key
3883
+ }
3884
+ </span>
3885
+ <span
3886
+ className={
3887
+ styles.jsonKeyFrequency
3888
+ }
3889
+ >
3890
+ {
3891
+ keyInfo.percentage
3892
+ }
3893
+ %
3894
+ </span>
3895
+ </div>
3896
+ )
3897
+ )}
3898
+ </div>
3899
+ ) : (
3900
+ <div
3901
+ className={
3902
+ styles.jsonKeyEmpty
3903
+ }
3904
+ >
3905
+ No
3906
+ JSON
3907
+ keys
3908
+ found.
3909
+ Enter
3910
+ a
3911
+ path
3912
+ manually.
3913
+ </div>
3914
+ )}
3915
+ </div>
3916
+ )}
3917
+
3918
+ <label>
3919
+ Value:
3920
+ </label>
3921
+ <input
3922
+ type="text"
3923
+ className={
3924
+ styles.formControl
3925
+ }
3926
+ value={
3927
+ criterion.value ||
3928
+ ''
3929
+ }
3930
+ onChange={(
3931
+ e
3932
+ ) =>
3933
+ handleFilterCriterionChange(
3934
+ groupIndex,
3935
+ filterIndex,
3936
+ 'value',
3937
+ e
3938
+ .target
3939
+ .value
3940
+ )
3941
+ }
3942
+ placeholder="Enter filter value"
3943
+ />
3944
+ </>
3945
+ ) : (
3946
+ <>
3947
+ <label>
3948
+ Value:
3949
+ </label>
3950
+ <input
3951
+ type="text"
3952
+ className={
3953
+ styles.formControl
3954
+ }
3955
+ value={
3956
+ criterion.value ||
3957
+ ''
3958
+ }
3959
+ onChange={(
3960
+ e
3961
+ ) =>
3962
+ handleFilterCriterionChange(
3963
+ groupIndex,
3964
+ filterIndex,
3965
+ 'value',
3966
+ e
3967
+ .target
3968
+ .value
3969
+ )
3970
+ }
3971
+ placeholder="Enter filter value"
3972
+ />
3973
+ </>
3974
+ )}
3643
3975
  </div>
3644
3976
  )}
3645
3977
  </div>
@@ -1157,6 +1157,94 @@
1157
1157
  margin: 16px 0;
1158
1158
  }
1159
1159
 
1160
+ .jsonKeyInputContainer {
1161
+ position: relative;
1162
+ display: flex;
1163
+ align-items: center;
1164
+ width: 100%;
1165
+
1166
+ .jsonKeyButton {
1167
+ position: absolute;
1168
+ right: 0;
1169
+ top: 0;
1170
+ height: 100%;
1171
+ background: none;
1172
+ border: none;
1173
+ border-left: 1px solid var(--border-color, #d1d5db);
1174
+ padding: 0 10px;
1175
+ color: var(--paragraph-color, #4b5563);
1176
+ cursor: pointer;
1177
+ font-size: 0.8em;
1178
+ transition: all 0.2s ease;
1179
+
1180
+ &:hover {
1181
+ background-color: #f3f4f6;
1182
+ color: var(--primary-color, #2563eb);
1183
+ }
1184
+ }
1185
+
1186
+ input {
1187
+ padding-right: 30px;
1188
+ }
1189
+ }
1190
+
1191
+ .jsonKeyDropdown {
1192
+ position: relative;
1193
+ width: 100%;
1194
+ max-height: 200px;
1195
+ overflow-y: auto;
1196
+ background-color: white;
1197
+ border: 1px solid var(--border-color, #d1d5db);
1198
+ border-radius: 6px;
1199
+ margin-top: 4px;
1200
+ margin-bottom: 12px;
1201
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
1202
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
1203
+ z-index: 100;
1204
+ }
1205
+
1206
+ .jsonKeyList {
1207
+ padding: 4px 0;
1208
+ }
1209
+
1210
+ .jsonKeyItem {
1211
+ display: flex;
1212
+ justify-content: space-between;
1213
+ align-items: center;
1214
+ padding: 8px 12px;
1215
+ cursor: pointer;
1216
+ transition: all 0.2s ease;
1217
+ font-size: 0.85em;
1218
+
1219
+ &:hover {
1220
+ background-color: #f3f4f6;
1221
+ }
1222
+
1223
+ .jsonKeyName {
1224
+ flex: 1;
1225
+ overflow: hidden;
1226
+ text-overflow: ellipsis;
1227
+ white-space: nowrap;
1228
+ color: var(--primary-color, #2563eb);
1229
+ font-weight: 500;
1230
+ }
1231
+
1232
+ .jsonKeyFrequency {
1233
+ color: var(--muted-color, #6b7280);
1234
+ font-size: 0.85em;
1235
+ margin-left: 8px;
1236
+ }
1237
+ }
1238
+
1239
+ .jsonKeyLoading,
1240
+ .jsonKeyEmpty {
1241
+ padding: 12px;
1242
+ text-align: center;
1243
+ color: var(--muted-color, #6b7280);
1244
+ font-size: 0.85em;
1245
+ font-style: italic;
1246
+ }
1247
+
1160
1248
  .jsonPreview {
1161
1249
  max-width: 100%;
1162
1250
  max-height: 150px;
@@ -1,8 +1,15 @@
1
+ .tableFilterContainer {
2
+ width: 100%;
3
+ position: relative;
4
+ display: block;
5
+ }
6
+
1
7
  .tableFilter {
2
8
  list-style: none;
3
9
  padding: 0;
4
10
  margin: 0;
5
11
  font-size: 1rem;
12
+ width: 100%;
6
13
  }
7
14
 
8
15
  .collapsibleMenu {
@@ -51,3 +58,81 @@
51
58
  background: var(--item-color) !important;
52
59
  color: var(--paragraph-color) !important;
53
60
  }
61
+
62
+ /* Mobile menu toggle button */
63
+ .mobileToggle {
64
+ display: none;
65
+ align-items: center;
66
+ justify-content: space-between;
67
+ padding: 12px 15px;
68
+ background: var(--primary-color, #0b3b2e);
69
+ color: white;
70
+ border-radius: 4px;
71
+ cursor: pointer;
72
+ margin-bottom: 10px;
73
+ border: none;
74
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
75
+ }
76
+
77
+ .activeFilterLabel {
78
+ font-weight: 600;
79
+ color: white;
80
+ }
81
+
82
+ .mobileMenuIcon {
83
+ display: flex;
84
+ flex-direction: column;
85
+ justify-content: space-between;
86
+ width: 20px;
87
+ height: 14px;
88
+ position: relative;
89
+ transition: all 0.3s ease;
90
+ }
91
+
92
+ .mobileMenuIcon span {
93
+ display: block;
94
+ height: 2px;
95
+ width: 100%;
96
+ background-color: white;
97
+ border-radius: 2px;
98
+ transition: all 0.3s ease;
99
+ }
100
+
101
+ .mobileMenuIcon.open span:nth-child(1) {
102
+ transform: translateY(6px) rotate(45deg);
103
+ }
104
+
105
+ .mobileMenuIcon.open span:nth-child(2) {
106
+ opacity: 0;
107
+ }
108
+
109
+ .mobileMenuIcon.open span:nth-child(3) {
110
+ transform: translateY(-6px) rotate(-45deg);
111
+ }
112
+
113
+ /* Media queries for responsive design */
114
+ @media (max-width: 1024px) {
115
+ .mobileToggle {
116
+ display: flex;
117
+ }
118
+
119
+ .tableFilter {
120
+ display: none;
121
+ max-height: 0;
122
+ overflow: hidden;
123
+ transition: max-height 0.3s ease;
124
+ }
125
+
126
+ .tableFilter.mobileOpen {
127
+ display: block;
128
+ max-height: 1000px;
129
+ overflow-y: auto;
130
+ border: 1px solid rgba(0, 0, 0, 0.1);
131
+ border-radius: 4px;
132
+ margin-bottom: 10px;
133
+ }
134
+
135
+ .link {
136
+ padding: 12px 20px; /* Larger touch targets for mobile */
137
+ }
138
+ }