@visns-studio/visns-components 1.0.0

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.
@@ -0,0 +1,598 @@
1
+ import React, { useCallback, useEffect, useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import axios from "axios";
4
+ import debounce from "lodash.debounce";
5
+ import moment from "moment";
6
+ import numeral from "numeral";
7
+ import Popup from "reactjs-popup";
8
+ import ReactDataGrid from "@inovua/reactdatagrid-community";
9
+ import { confirmAlert } from "react-confirm-alert";
10
+ import { toast } from "react-toastify";
11
+ import {
12
+ Backspace,
13
+ Check,
14
+ Clock,
15
+ CloudDownload,
16
+ Edit,
17
+ Ribbon,
18
+ Search,
19
+ } from "akar-icons";
20
+
21
+ import "@inovua/reactdatagrid-community/index.css";
22
+ import "react-confirm-alert/src/react-confirm-alert.css";
23
+
24
+ import CustomFetch from "./visns-fetch";
25
+ import Form from "./visns-form";
26
+
27
+ const loadData = async (
28
+ { skip, limit, sortInfo, currentData, filterValue, groupBy },
29
+ search,
30
+ ajaxSetting
31
+ ) => {
32
+ const url = ajaxSetting.url;
33
+
34
+ let page = Math.floor(skip / limit) + 1;
35
+ let params = { page, sortInfo, take: limit, search: search };
36
+
37
+ if (sortInfo) {
38
+ params.sortBy = sortInfo.name;
39
+ params.sort = sortInfo.dir === 1 ? "asc" : "desc";
40
+ }
41
+
42
+ if (ajaxSetting && ajaxSetting.where) {
43
+ params.where = ajaxSetting.where;
44
+ }
45
+
46
+ try {
47
+ const response = await axios.post(url, params);
48
+ const { data, total } = response.data;
49
+ return { data, count: total };
50
+ } catch (error) {
51
+ console.error("Error fetching data:", error);
52
+ return { data: [], count: 0 };
53
+ }
54
+ };
55
+
56
+ const DataGrid = (props) => {
57
+ const {
58
+ ajaxSetting,
59
+ form,
60
+ columns,
61
+ settings,
62
+ tableFilters,
63
+ historySearch,
64
+ historyUrl,
65
+ } = props;
66
+ const navigate = useNavigate();
67
+
68
+ /** Modal States */
69
+ const [formData, setFormData] = useState(form);
70
+ const [formType, setFormType] = useState("");
71
+ const [formId, setFormId] = useState(0);
72
+ const [modalShow, setModalShow] = useState(false);
73
+
74
+ /** Modal Functions */
75
+ const childDropdownCallback = (data) => {
76
+ let _form = { ...formData };
77
+
78
+ if (formData.fields.length > 0) {
79
+ formData.fields.forEach((a, b) => {
80
+ if (a.id === data.id) {
81
+ _form.fields[b].options = data.data;
82
+ }
83
+ });
84
+ }
85
+
86
+ setFormData(_form);
87
+ };
88
+
89
+ const modalOpen = (formType, formId) => {
90
+ setModalShow(true);
91
+ setFormType(formType);
92
+ setFormId(formId);
93
+ };
94
+
95
+ const modalClose = () => {
96
+ setModalShow(false);
97
+ };
98
+
99
+ /** Table States */
100
+ const [dataReload, setDataReload] = useState(0);
101
+ const [dSearch, setDSearch] = useState("");
102
+ const data = useCallback(
103
+ (params) => loadData(params, dSearch, ajaxSetting),
104
+ [ajaxSetting, dataReload, dSearch]
105
+ );
106
+ const [gridColumns, setGridColumns] = useState([]);
107
+ const [gridHeight, setGridHeight] = useState(550);
108
+ const limit = ajaxSetting.take || 10;
109
+ const [search, setSearch] = useState("");
110
+ const sortInfo =
111
+ ajaxSetting.sortBy && ajaxSetting.sort
112
+ ? {
113
+ name: ajaxSetting.sortBy,
114
+ dir: ajaxSetting.sort === "asc" ? 1 : -1,
115
+ }
116
+ : null;
117
+
118
+ /** Table Functions */
119
+ const handleChangeSearch = (e) => {
120
+ const { value } = e.target;
121
+ setSearch(value);
122
+ };
123
+
124
+ const handleSettingClick = (s, d) => {
125
+ switch (s.id) {
126
+ case "update":
127
+ modalOpen("update", d.id);
128
+ break;
129
+ case "delete":
130
+ confirmAlert({
131
+ title: "Delete Item",
132
+ message: `Are you sure you want to delete ${
133
+ d.name ? d.name : d.label ? d.label : "this item"
134
+ }?`,
135
+ buttons: [
136
+ {
137
+ label: "Yes",
138
+ onClick: () =>
139
+ CustomFetch(
140
+ form.url + "/" + d[form.primaryKey],
141
+ "DELETE",
142
+ {},
143
+ (result) => {
144
+ if (result.error === "") {
145
+ const min = 0;
146
+ const max = 999999999;
147
+ const randomNumber = Math.floor(
148
+ Math.random() *
149
+ (max - min + 1) +
150
+ min
151
+ );
152
+
153
+ setDataReload(randomNumber);
154
+
155
+ toast.success(
156
+ `The selected item has been deleted.`
157
+ );
158
+ } else {
159
+ toast.error(String(result.error));
160
+ }
161
+ }
162
+ ),
163
+ },
164
+ {
165
+ label: "No",
166
+ onClick: () => close(),
167
+ },
168
+ ],
169
+ });
170
+ break;
171
+ default:
172
+ confirmAlert({
173
+ title: s.title,
174
+ message: "",
175
+ buttons: [
176
+ {
177
+ label: "Yes",
178
+ onClick: () =>
179
+ CustomFetch(
180
+ s.url + "/" + d[form.primaryKey],
181
+ s.method,
182
+ {},
183
+ function (result) {
184
+ if (result.error === "") {
185
+ const min = 0;
186
+ const max = 999999999;
187
+ const randomNumber = Math.floor(
188
+ Math.random() *
189
+ (max - min + 1) +
190
+ min
191
+ );
192
+
193
+ setDataReload(randomNumber);
194
+
195
+ toast.success(String(s.title));
196
+ } else {
197
+ toast.error(String(result.error));
198
+ }
199
+ }
200
+ ),
201
+ },
202
+ {
203
+ label: "No",
204
+ onClick: () => close(),
205
+ },
206
+ ],
207
+ });
208
+ break;
209
+ }
210
+ };
211
+
212
+ const handleReload = () => {
213
+ setDataReload(dataReload + 1);
214
+ };
215
+
216
+ const onRowClick = useCallback((rowProps, event) => {
217
+ const cellElement = event.target.closest(".InovuaReactDataGrid__cell");
218
+ const columnIndex = Array.from(cellElement.parentNode.children).indexOf(
219
+ cellElement
220
+ );
221
+
222
+ if (columnIndex === rowProps.columns.length - 1) {
223
+ } else {
224
+ if (
225
+ rowProps.columns[columnIndex].link &&
226
+ rowProps.columns[columnIndex].link.url
227
+ ) {
228
+ if (rowProps.columns[columnIndex].link.folder) {
229
+ window.location.href =
230
+ rowProps.columns[columnIndex].link.url +
231
+ rowProps.data[rowProps.columns[columnIndex].link.key] +
232
+ "/" +
233
+ rowProps.columns[columnIndex].link.folder;
234
+ } else {
235
+ navigate(
236
+ `${rowProps.columns[columnIndex].link.url}${
237
+ rowProps.data[
238
+ rowProps.columns[columnIndex].link.key
239
+ ]
240
+ }`
241
+ );
242
+ }
243
+ } else {
244
+ modalOpen("update", rowProps.data[form.primaryKey]);
245
+ }
246
+ }
247
+ }, []);
248
+
249
+ const onRenderRow = useCallback((rowProps) => {
250
+ // save the original handlers to be called later
251
+ const { onClick } = rowProps;
252
+
253
+ rowProps.onClick = (event) => {
254
+ onRowClick(rowProps, event);
255
+ if (onClick) {
256
+ onClick(event);
257
+ }
258
+ };
259
+
260
+ // add a CSS class to rowProps
261
+ rowProps.className = "vs-datagrid--row";
262
+ }, []);
263
+
264
+ /** Table Hooks */
265
+ useEffect(() => {
266
+ const delayedSetDSearch = debounce(() => {
267
+ setDSearch(search);
268
+ }, 300); // Adjust the delay (in milliseconds) according to your needs
269
+
270
+ delayedSetDSearch();
271
+
272
+ return delayedSetDSearch.cancel; // Cleanup the debounce timer on unmount
273
+ }, [search, setDSearch]);
274
+
275
+ const renderSetting = (s, d) => {
276
+ let iconStyle = {};
277
+
278
+ if (s.active) {
279
+ if (
280
+ d.hasOwnProperty(s.active.id) &&
281
+ !s.active.hasOwnProperty("type") &&
282
+ d[s.active.id] === s.active.value
283
+ ) {
284
+ iconStyle = {
285
+ color: s.active.colour,
286
+ };
287
+ } else if (s.active.hasOwnProperty("type")) {
288
+ switch (s.active.type) {
289
+ case "greater":
290
+ if (d[s.active.id] > s.active.value) {
291
+ iconStyle = {
292
+ color: s.active.colour,
293
+ };
294
+ }
295
+ break;
296
+ case "not null":
297
+ if (d[s.active.id] !== null) {
298
+ iconStyle = {
299
+ color: s.active.colour,
300
+ };
301
+ }
302
+ break;
303
+ default:
304
+ break;
305
+ }
306
+ }
307
+ }
308
+
309
+ const getIconComponent = (IconComponent) => (
310
+ <IconComponent
311
+ data-tooltip-id="system-tooltip"
312
+ data-tooltip-content={
313
+ iconStyle && iconStyle.color ? s.active.title : s.title
314
+ }
315
+ strokeWidth={2}
316
+ size={18}
317
+ className="tdaction"
318
+ style={iconStyle}
319
+ />
320
+ );
321
+
322
+ switch (s.id) {
323
+ case "complete":
324
+ return getIconComponent(Check);
325
+ case "delete":
326
+ return getIconComponent(Backspace);
327
+ case "primary":
328
+ return getIconComponent(Ribbon);
329
+ case "ssa":
330
+ return getIconComponent(Clock);
331
+ case "update":
332
+ return getIconComponent(Edit);
333
+ default:
334
+ return null;
335
+ }
336
+ };
337
+
338
+ const renderColumnContextMenu = useCallback((menuProps, { cellProps }) => {
339
+ const filteredItems = menuProps.items
340
+ .map((item, key) => {
341
+ if (key >= 6 && key <= 9) {
342
+ return null; // Skip items with keys 6-9 [lock columns setting]
343
+ }
344
+ return item;
345
+ })
346
+ .filter(Boolean); // Filter out any null items
347
+
348
+ menuProps.items = filteredItems;
349
+ }, []);
350
+
351
+ useEffect(() => {
352
+ const renderColumn = (column) => {
353
+ switch (column.type) {
354
+ case "currency":
355
+ return {
356
+ name: column.id,
357
+ header: column.label,
358
+ defaultFlex: 1,
359
+ link: column.link ? column.link : {},
360
+ render: ({ data }) => {
361
+ if (data) {
362
+ data = numeral(data[column.id]).format(
363
+ "$0,0.00"
364
+ );
365
+ }
366
+ return <span>{data}</span>;
367
+ },
368
+ };
369
+ case "date":
370
+ return {
371
+ name: column.id,
372
+ header: column.label,
373
+ defaultFlex: 1,
374
+ link: column.link ? column.link : {},
375
+ render: ({ data }) => {
376
+ if (data && data[column.id]) {
377
+ data = moment(data[column.id]).format(
378
+ column.format ? column.format : "DD-MM-YYYY"
379
+ );
380
+ }
381
+ return <span>{data}</span>;
382
+ },
383
+ };
384
+ case "datetime":
385
+ return {
386
+ name: column.id,
387
+ header: column.label,
388
+ defaultFlex: 1,
389
+ link: column.link ? column.link : {},
390
+ render: ({ data }) => {
391
+ if (data && data[column.id]) {
392
+ data = moment(data[column.id]).format(
393
+ column.format
394
+ ? column.format
395
+ : "DD-MM-YYYY hh:mm A"
396
+ );
397
+ }
398
+ return <span>{data}</span>;
399
+ },
400
+ };
401
+ case "file":
402
+ return {
403
+ name: column.id,
404
+ header: column.label,
405
+ defaultFlex: 1,
406
+ link: column.link ? column.link : {},
407
+ render: ({ data }) => {
408
+ if (data) {
409
+ return (
410
+ <span>
411
+ <CloudDownload
412
+ data-tooltip-id="system-tooltip"
413
+ data-tooltip-content="Download File"
414
+ strokeWidth={2}
415
+ size={18}
416
+ className="tdaction"
417
+ />
418
+ </span>
419
+ );
420
+ } else {
421
+ return null;
422
+ }
423
+ },
424
+ };
425
+ case "option":
426
+ return {
427
+ name: column.id,
428
+ header: column.label,
429
+ defaultFlex: 1,
430
+ link: column.link ? column.link : {},
431
+ render: ({ data }) => {
432
+ if (data) {
433
+ data = column.options[data[column.id]];
434
+ }
435
+ return <span>{data}</span>;
436
+ },
437
+ };
438
+ case "relation":
439
+ let relationName = column.id.reduce(
440
+ (acc, id) => acc + `${id}.`,
441
+ ""
442
+ );
443
+ relationName += column.nameFrom;
444
+
445
+ return {
446
+ name: relationName,
447
+ header: column.label,
448
+ defaultFlex: 1,
449
+ link: column.link ? column.link : {},
450
+ render: ({ data }) => {
451
+ let value = column.id.reduce((acc, id) => {
452
+ if (acc === "") {
453
+ return data[id];
454
+ } else {
455
+ return acc[id];
456
+ }
457
+ }, "");
458
+
459
+ if (value) {
460
+ value = value[column.nameFrom];
461
+ }
462
+
463
+ return <span>{value}</span>;
464
+ },
465
+ };
466
+ case "stage":
467
+ return {
468
+ name: column.id,
469
+ header: column.label,
470
+ defaultFlex: 1,
471
+ link: column.link ? column.link : {},
472
+ render: ({ data }) => {
473
+ let stage = "";
474
+
475
+ column.keyIds.forEach((a, b) => {
476
+ if (data[a] === 1) {
477
+ stage = column.valueIds[b];
478
+ }
479
+ });
480
+
481
+ return <span>{stage}</span>;
482
+ },
483
+ };
484
+ case "time":
485
+ return {
486
+ name: column.id,
487
+ header: column.label,
488
+ defaultFlex: 1,
489
+ link: column.link ? column.link : {},
490
+ render: ({ data }) => {
491
+ if (data && data[column.id]) {
492
+ data = moment(data[column.id]).format(
493
+ column.format ? column.format : "hh:mm A"
494
+ );
495
+ }
496
+ return <span>{data}</span>;
497
+ },
498
+ };
499
+ default:
500
+ return {
501
+ name: column.id,
502
+ header: column.label,
503
+ defaultFlex: 1,
504
+ link: column.link ? column.link : {},
505
+ };
506
+ }
507
+ };
508
+
509
+ const newColumns = columns.map(renderColumn);
510
+
511
+ if (settings.length > 0) {
512
+ newColumns.push({
513
+ name: "setting",
514
+ header: "Action",
515
+ defaultWidth: 100,
516
+ textAlign: "center",
517
+ render: ({ data }) => {
518
+ return (
519
+ <div className="tdactions">
520
+ {settings.map((setting) => (
521
+ <span
522
+ key={`setting-${setting.id}`}
523
+ onClick={(e) => {
524
+ e.preventDefault();
525
+ e.stopPropagation();
526
+ handleSettingClick(setting, data);
527
+ }}
528
+ >
529
+ {renderSetting(setting, data)}
530
+ </span>
531
+ ))}
532
+ </div>
533
+ );
534
+ },
535
+ });
536
+ }
537
+
538
+ setGridColumns(newColumns);
539
+ }, [columns]);
540
+
541
+ // Data Grid Styling
542
+ const gridStyle = { minHeight: gridHeight };
543
+
544
+ return (
545
+ <>
546
+ <div className="filterInput--alt">
547
+ <div className="icon-search">
548
+ <Search strokeWidth={2} size={18} />
549
+ </div>
550
+ <input
551
+ type="text"
552
+ placeholder="Search"
553
+ value={search}
554
+ onChange={handleChangeSearch}
555
+ />
556
+ </div>
557
+ <div className="filterAction--alt">
558
+ <button
559
+ className="btn"
560
+ onClick={() => {
561
+ modalOpen("create", 0);
562
+ }}
563
+ >
564
+ <div className="icon-plus"></div> Create
565
+ </button>
566
+ </div>
567
+ <ReactDataGrid
568
+ columns={gridColumns}
569
+ dataSource={data}
570
+ defaultLimit={limit}
571
+ defaultSortInfo={sortInfo}
572
+ enableColumnAutosize={false}
573
+ idProperty="uniqueId"
574
+ style={gridStyle}
575
+ onRenderRow={onRenderRow}
576
+ pagination
577
+ renderColumnContextMenu={renderColumnContextMenu}
578
+ />
579
+ <Popup
580
+ open={modalShow}
581
+ onClose={modalClose}
582
+ closeOnDocumentClick={false}
583
+ >
584
+ <Form
585
+ closeModal={modalClose}
586
+ fetchTable={handleReload}
587
+ formSettings={formData}
588
+ formType={formType}
589
+ updateForm={setFormData}
590
+ columnId={formId}
591
+ childDropdownCallback={childDropdownCallback}
592
+ />
593
+ </Popup>
594
+ </>
595
+ );
596
+ };
597
+
598
+ export default DataGrid;
@@ -0,0 +1,102 @@
1
+ import _ from 'lodash';
2
+ import axios from 'axios';
3
+ import { trackPromise } from 'react-promise-tracker';
4
+
5
+ const CustomDownload = (
6
+ url,
7
+ method,
8
+ formData,
9
+ successCallback,
10
+ errorCallback
11
+ ) => {
12
+ let baseUrl = '';
13
+ let fileUpload = false;
14
+ let body;
15
+ let headers = {};
16
+ let options = {};
17
+
18
+ if (!_.isEmpty(formData)) {
19
+ Object.keys(formData).forEach((item) => {
20
+ if (formData[item] !== undefined) {
21
+ if (formData[item] instanceof File && formData[item] !== null) {
22
+ fileUpload = true;
23
+ }
24
+ }
25
+ });
26
+ }
27
+
28
+ if (method.toUpperCase() === 'PUT' || method.toUpperCase() === 'PATCH') {
29
+ if (fileUpload === false) {
30
+ body = {
31
+ ...body,
32
+ _method: method,
33
+ };
34
+ }
35
+ }
36
+
37
+ if (fileUpload === false) {
38
+ body = JSON.stringify(formData);
39
+ headers = {
40
+ ...headers,
41
+ 'Content-Type': 'application/json',
42
+ };
43
+ } else {
44
+ body = new FormData();
45
+
46
+ headers = {
47
+ ...headers,
48
+ 'Content-Type': 'multipart/form-data',
49
+ contentType: false,
50
+ processData: false,
51
+ };
52
+
53
+ if (!_.isEmpty(formData)) {
54
+ Object.keys(formData).forEach((item) => {
55
+ body.append(item, formData[item]);
56
+ });
57
+ }
58
+
59
+ if (
60
+ method.toUpperCase() === 'PUT' ||
61
+ method.toUpperCase() === 'PATCH'
62
+ ) {
63
+ if (fileUpload === false) {
64
+ } else {
65
+ body.append('_method', method);
66
+ method = 'POST';
67
+ }
68
+ }
69
+ }
70
+
71
+ options = {
72
+ method: method,
73
+ data: body,
74
+ headers: headers,
75
+ url: baseUrl + url,
76
+ responseType: 'blob',
77
+ };
78
+
79
+ trackPromise(
80
+ axios(options).then(
81
+ (result) => {
82
+ successCallback(result.data);
83
+ },
84
+ (error) => {
85
+ if (
86
+ error.response.hasOwnProperty('data') &&
87
+ error.response.data.hasOwnProperty('message') &&
88
+ error.response.data.message !== ''
89
+ ) {
90
+ if (error.response.data.message === 'Unauthenticated.') {
91
+ localStorage.clear();
92
+ }
93
+ errorCallback(error.response.data.message);
94
+ } else {
95
+ errorCallback(error);
96
+ }
97
+ }
98
+ )
99
+ );
100
+ };
101
+
102
+ export default CustomDownload;