@visns-studio/visns-components 5.0.3 → 5.0.5

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.
@@ -215,9 +215,9 @@ function Form({
215
215
  const handleSelectOption = () => {
216
216
  updateFormData({ [id]: inputValue });
217
217
 
218
- // Update any matching form data properties
219
- Object.keys(inputValue).forEach((key) => {
220
- if (formData.hasOwnProperty(key)) {
218
+ // Update any matching form data properties except for the second item
219
+ Object.keys(inputValue).forEach((key, index) => {
220
+ if (index !== 1 && formData.hasOwnProperty(key)) {
221
221
  updateFormData({ [key]: inputValue[key] });
222
222
  }
223
223
  });
@@ -287,6 +287,7 @@ function Form({
287
287
  // Switch cases to handle different actions
288
288
  switch (action.action) {
289
289
  case 'select-option':
290
+ case 'create-option': // New case to handle create-option
290
291
  handleSelectOption();
291
292
  break;
292
293
  case 'clear':
@@ -584,44 +585,44 @@ function Form({
584
585
 
585
586
  if (['create', 'update'].includes(formType)) {
586
587
  fields.forEach((item) => {
588
+ // Skip createOnly fields if formType is 'update'
587
589
  if (formType === 'update' && item.createOnly) {
588
590
  return;
589
591
  }
590
592
 
593
+ // Check if the item is required
591
594
  if (item.required) {
592
- if (
593
- formData[item.id] === null ||
594
- formData[item.id] === ''
595
- ) {
595
+ // Check if formData[item.id] is null, undefined, or an empty string
596
+ if (!formData[item.id]) {
597
+ // Check for required_check dependency
596
598
  if (
597
- item.hasOwnProperty('required_check') &&
598
599
  item.required_check &&
599
600
  !formData[item.required_check]
600
601
  ) {
601
602
  validation += `${item.label} is a required field.<br/>`;
602
603
  _inputClass[item.id] = styles.inputError;
603
604
  } else {
604
- console.info('b', formData, item.id);
605
605
  validation += `${item.label} is a required field.<br/>`;
606
606
  _inputClass[item.id] = styles.inputError;
607
607
  }
608
608
  } else {
609
- _inputClass[item.id] = '';
609
+ _inputClass[item.id] = ''; // Clear error class if valid
610
610
  }
611
611
  } else if (
612
- item.hasOwnProperty('required_rely') &&
613
612
  item.required_rely &&
614
613
  formData[item.required_rely] &&
615
- (formData[item.id] === null ||
616
- formData[item.id] === '')
614
+ !formData[item.id]
617
615
  ) {
616
+ // Check if the dependent required field is empty when it should be filled
618
617
  validation += `${item.label} is a required field.<br/>`;
619
618
  _inputClass[item.id] = styles.inputError;
620
619
  } else {
621
- _inputClass[item.id] = '';
620
+ _inputClass[item.id] = ''; // Clear error class if not required
622
621
  }
623
622
  });
624
623
 
624
+ console.info(_inputClass);
625
+
625
626
  setInputClass(_inputClass);
626
627
  }
627
628
 
@@ -1371,7 +1372,10 @@ function Form({
1371
1372
  <div className={styles.modal}>
1372
1373
  <div className={styles.modal__header}>
1373
1374
  <h1>{formSettings[formType]?.title}</h1>
1374
- <button className={styles.modal__close} onClick={closeModal}>
1375
+ <button
1376
+ className={styles.modal__close}
1377
+ onClick={closeModal}
1378
+ >
1375
1379
  <CircleX strokeWidth={2} size={24} />
1376
1380
  </button>
1377
1381
  </div>
@@ -1,54 +1,109 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import CustomFetch from './Fetch';
2
3
  import SelectList from './SelectList';
3
- import Select, { createFilter } from 'react-select';
4
+ import Select, { createFilter, components } from 'react-select';
5
+ import CreatableSelect from 'react-select/creatable';
4
6
  import styles from './styles/AsyncSelect.module.scss';
5
7
 
8
+ // Custom Option component to add a second line of text
9
+ const CustomOption = (props) => (
10
+ <components.Option {...props}>
11
+ <div>
12
+ {props.data.label}
13
+ {props.data.description && props.data.description !== '' && (
14
+ <div style={{ fontSize: '0.85em', color: '#888' }}>
15
+ {props.data.description}
16
+ </div>
17
+ )}
18
+ </div>
19
+ </components.Option>
20
+ );
21
+
6
22
  function MultiSelect({
7
23
  className,
8
- inputValue,
24
+ inputValue = [],
9
25
  multi,
10
26
  onChange,
11
- options,
27
+ options = [],
12
28
  placeholder,
13
29
  settings,
14
30
  style,
31
+ isCreatable = false,
32
+ creatableConfig = {},
15
33
  }) {
16
34
  const [selectOptions, setSelectOptions] = useState([]);
35
+ const [selectValue, setSelectValue] = useState([]);
17
36
 
18
37
  useEffect(() => {
19
- let _options = [];
38
+ const _options = Array.isArray(options)
39
+ ? options.map((a) => ({
40
+ value: a.id,
41
+ label: a.label || a.name || a.description || 'Unknown',
42
+ description: a.description || '', // Include description field
43
+ ...a,
44
+ }))
45
+ : [];
46
+ setSelectOptions(_options);
47
+ }, [options]);
20
48
 
21
- if (options.length > 0) {
22
- options.forEach((a) => {
23
- let _optionItem = {
24
- value: a.id,
25
- label: a.label,
26
- };
49
+ useEffect(() => {
50
+ const normalizeInputValue = (value) => {
51
+ // Ensure value is an array, wrapping single items if necessary
52
+ return Array.isArray(value) ? value : value ? [value] : [];
53
+ };
54
+
55
+ const _values = normalizeInputValue(inputValue).map((a) => ({
56
+ value: a.id,
57
+ label: a.label || a.name || a.description || 'Unknown',
58
+ description: a.description || '', // Include description field
59
+ ...a,
60
+ }));
27
61
 
28
- Object.keys(a).forEach((b) => {
29
- if (b !== 'id') {
30
- _optionItem = {
31
- ..._optionItem,
32
- [b]: a[b],
33
- };
34
- }
35
- });
62
+ setSelectValue(_values);
63
+ }, [inputValue]);
36
64
 
37
- _options.push(_optionItem);
38
- });
65
+ const handleCreate = async (inputValue) => {
66
+ if (creatableConfig.url && creatableConfig.method) {
67
+ const res = await CustomFetch(
68
+ creatableConfig.url,
69
+ creatableConfig.method,
70
+ { label: inputValue }
71
+ );
39
72
 
40
- setSelectOptions(_options);
73
+ if (res && res.data) {
74
+ const newOption = {
75
+ value: res.data.id,
76
+ label: res.data.label || inputValue,
77
+ description: res.data.description || 'Newly created item', // Default description for new items
78
+ ...res.data,
79
+ };
80
+
81
+ // Update options and trigger onChange with the new option
82
+ setSelectOptions((prevOptions) => [...prevOptions, newOption]);
83
+ setSelectValue((prevValues) =>
84
+ multi ? [...prevValues, newOption] : [newOption]
85
+ );
86
+
87
+ // Trigger onChange with the new option
88
+ onChange(
89
+ multi ? [...selectValue, newOption] : newOption,
90
+ { action: 'create-option' },
91
+ settings.id
92
+ );
93
+ }
41
94
  }
42
- }, [options]);
95
+ };
96
+
97
+ const SelectComponent = isCreatable ? CreatableSelect : Select;
43
98
 
44
99
  return (
45
- <Select
46
- closeMenuOnSelect={multi ? false : true}
100
+ <SelectComponent
101
+ closeMenuOnSelect={!multi}
47
102
  isClearable
48
103
  isSearchable
49
104
  isMulti={multi}
50
105
  filterOption={createFilter({ ignoreAccents: false })}
51
- components={{ SelectList }}
106
+ components={{ Option: CustomOption }} // Use the custom Option component
52
107
  options={selectOptions}
53
108
  onChange={(inputValue, action) => {
54
109
  onChange(inputValue, action, settings.id);
@@ -69,7 +124,8 @@ function MultiSelect({
69
124
  : '52px',
70
125
  }),
71
126
  }}
72
- value={inputValue}
127
+ value={selectValue}
128
+ onCreateOption={isCreatable ? handleCreate : undefined} // Only add if creatable
73
129
  placeholder={placeholder ? placeholder : 'Select...'}
74
130
  />
75
131
  );
@@ -12,7 +12,7 @@
12
12
  .fi__span {
13
13
  position: absolute;
14
14
  transition: all 200ms;
15
- opacity: 0.8;
15
+ opacity: 1;
16
16
  left: 0;
17
17
  transform-origin: top left;
18
18
  cursor: text;
@@ -164,10 +164,11 @@
164
164
  input:not(:placeholder-shown) + .fi__span,
165
165
  textarea:not(:placeholder-shown) + .fi__span,
166
166
  select:not(:placeholder-shown) + .fi__span {
167
- transform: translateY(-110%) translateX(10px) scale(0.75);
167
+ transform: translateY(-135%) translateX(10px) scale(0.9);
168
168
  opacity: 1;
169
169
  padding: 0;
170
170
  background: var(--bg-color);
171
- font-weight: 300;
171
+ font-weight: 400;
172
172
  transition: all 0.5s cubic-bezier(0.5, 0.5, 0, 1);
173
+ color: rgba(var(--paragraph-rgb), 0.65) !important;
173
174
  }
@@ -3,11 +3,11 @@
3
3
  input:not(:placeholder-shown) + .fi__span,
4
4
  textarea:not(:placeholder-shown) + .fi__span,
5
5
  select:not(:placeholder-shown) + .fi__span {
6
- transform: translateY(-40%) translateX(10px) scale(0.75);
6
+ transform: translateY(-40%) translateX(10px) scale(0.9);
7
7
  opacity: 1;
8
- padding: 0 4px;
8
+ padding: 0 0.25rem;
9
9
  background: var(--tertiary-color);
10
- font-weight: 300;
10
+ font-weight: 400;
11
11
  border-radius: var(--br);
12
12
  }
13
13
 
@@ -111,30 +111,72 @@ select:not(:placeholder-shown) + .fi__span {
111
111
  }
112
112
 
113
113
  .grid__subcontent {
114
- flex: 0 1 85%;
115
- background: white;
114
+ flex: 0 1 80%;
115
+ background: var(--tertiary-color);
116
116
  border-radius: var(--br);
117
- border: 1px solid var(--border-color);
117
+ border: 1px solid rgba(var(--primary-rgb), 0.05);
118
118
  box-sizing: border-box;
119
- padding: 0;
120
- margin: 0.5rem 0;
119
+ padding: 2px;
120
+ margin: 8px 0;
121
121
  height: auto;
122
122
  position: relative;
123
123
  }
124
124
 
125
+ .grid__subcontent h2 {
126
+ color: var(--primary-color);
127
+ font-size: 1.35em;
128
+ padding: 0;
129
+ margin: 0 0 30px 0;
130
+ }
131
+
125
132
  .gridtxt__header {
126
133
  width: 100%;
127
134
  display: block;
128
- background: var(--border-color);
135
+ background: rgba(var(--primary-rgb), 0.05);
129
136
  box-sizing: border-box;
130
137
  padding: 10px 20px;
131
- border-top-left-radius: var(--br);
132
- border-top-right-radius: var(--br);
138
+ border-radius: var(--br);
133
139
  margin-bottom: 0;
134
140
  font-weight: 700;
135
141
  text-align: center;
136
142
  color: var(--paragraph-color);
137
- position: relative;
143
+ }
144
+
145
+ .gridtxt__header span {
146
+ font-weight: 700;
147
+ }
148
+
149
+ .gridtxt > ul {
150
+ width: 100%;
151
+ display: flex;
152
+ justify-content: flex-start;
153
+ flex-direction: row;
154
+ flex-wrap: wrap;
155
+ list-style: none;
156
+ box-sizing: border-box;
157
+ padding: 0.85rem;
158
+ margin: 0;
159
+ }
160
+
161
+ .gridtxt > ul li {
162
+ flex: 0 0 calc(50% - 10px);
163
+ padding: 0.85rem;
164
+ box-sizing: border-box;
165
+ border: 1px solid rgba(var(--primary-rgb), 0.095);
166
+ color: var(--paragraph-color);
167
+ background: rgba(var(--primary-rgb), 0.015);
168
+ margin: 5px;
169
+ border-radius: var(--br);
170
+ line-height: 1.45;
171
+ font-size: 1rem;
172
+ }
173
+
174
+ .gridtxt > ul .fw-grid-item {
175
+ flex: 0 0 calc(100% - 10px);
176
+ }
177
+
178
+ .gridtxt > ul .notecolor {
179
+ border: 1px solid var(--secondary-color);
138
180
  }
139
181
 
140
182
  .formcontainer {
@@ -277,7 +319,7 @@ select:not(:placeholder-shown) + .fi__span {
277
319
  a {
278
320
  text-decoration: none;
279
321
  color: var(--secondary-color);
280
- font-weight: 300;
322
+ font-weight: 400;
281
323
  }
282
324
 
283
325
  svg {
@@ -10,6 +10,7 @@ import Dropzone from 'react-dropzone';
10
10
  import { confirmAlert } from 'react-confirm-alert';
11
11
  import truncate from 'truncate';
12
12
  import { CopyToClipboard } from 'react-copy-to-clipboard';
13
+ import { saveAs } from 'file-saver';
13
14
  import { toast } from 'react-toastify';
14
15
  import {
15
16
  CircleCheck,
@@ -75,6 +76,8 @@ function GenericDetail({
75
76
  if (activeTab) {
76
77
  const activeTabConfig = tabs.find((t) => t.id === activeTab.id);
77
78
 
79
+ console.info(tabs);
80
+
78
81
  if (type === 'view') {
79
82
  setActiveStage(stage.id);
80
83
  } else {
@@ -95,6 +98,32 @@ function GenericDetail({
95
98
 
96
99
  if (res.data.error === '') {
97
100
  handleReload();
101
+ toast.success('Stage updated successfully');
102
+ } else {
103
+ toast.error(res.data.error);
104
+ }
105
+ } else if (
106
+ stages &&
107
+ stages.url &&
108
+ stages.url !== '' &&
109
+ stages.key &&
110
+ stages.key !== '' &&
111
+ stages.urlParam &&
112
+ stages.urlParam !== ''
113
+ ) {
114
+ const url = `${stages.url}/${routeParams[urlParam]}`;
115
+
116
+ const res = await CustomFetch(
117
+ url,
118
+ stages.method || 'POST',
119
+ {
120
+ [stages.key]: stage.id,
121
+ }
122
+ );
123
+
124
+ if (res.data.error === '') {
125
+ handleReload();
126
+ toast.success('Stage updated successfully');
98
127
  } else {
99
128
  toast.error(res.data.error);
100
129
  }
@@ -436,10 +465,32 @@ function GenericDetail({
436
465
  if (activeTabConfig && activeTabConfig.type) {
437
466
  switch (activeTabConfig.type) {
438
467
  case 'dropzone':
439
- const downloadFile = (id) => {
440
- window.open(
441
- `/ajax/files/download/${id}/${activeTabConfig.folder}`
442
- );
468
+ const downloadFile = (file) => {
469
+ const { id, file_url, file_name } = file;
470
+
471
+ // If file_url exists, use it for the download, else use the id to download
472
+ const urlToDownload =
473
+ file_url || `/ajax/files/download/${id}`;
474
+
475
+ // Use fetch to retrieve the file as a blob
476
+ fetch(urlToDownload, {
477
+ method: 'GET',
478
+ headers: {
479
+ 'Content-Type':
480
+ 'application/octet-stream', // Specify content type as binary
481
+ },
482
+ })
483
+ .then((response) => response.blob()) // Convert response to Blob
484
+ .then((blob) => {
485
+ // Use FileSaver to download the file with the specified name
486
+ saveAs(blob, file_name);
487
+ })
488
+ .catch((error) => {
489
+ console.error(
490
+ 'Error downloading the file:',
491
+ error
492
+ );
493
+ });
443
494
  };
444
495
 
445
496
  const deleteFile = (e, id, name) => {
@@ -521,9 +572,13 @@ function GenericDetail({
521
572
  key: response.key,
522
573
  bucket: response.bucket,
523
574
  filename: file.name,
575
+ file_name: file.name,
524
576
  filesize: file.size,
577
+ file_size: file.size,
525
578
  extension:
526
579
  response.extension,
580
+ file_extension:
581
+ response.extension,
527
582
  file_relationship:
528
583
  activeTabConfig.file_relationship,
529
584
  };
@@ -533,9 +588,30 @@ function GenericDetail({
533
588
  'PUT',
534
589
  requestBody,
535
590
  (res) => {
536
- setFiles(
537
- res.data.files
538
- );
591
+ if (
592
+ activeTabConfig.file_relationship &&
593
+ res.data[
594
+ activeTabConfig
595
+ .file_relationship
596
+ ]
597
+ ) {
598
+ setFiles(
599
+ res.data[
600
+ activeTabConfig
601
+ .file_relationship
602
+ ]
603
+ );
604
+ } else {
605
+ if (
606
+ res?.data
607
+ ?.files
608
+ ) {
609
+ setFiles(
610
+ res.data
611
+ .files
612
+ );
613
+ }
614
+ }
539
615
  }
540
616
  );
541
617
  });
@@ -635,7 +711,7 @@ function GenericDetail({
635
711
  <li
636
712
  onClick={() => {
637
713
  downloadFile(
638
- a.id
714
+ a
639
715
  );
640
716
  }}
641
717
  key={'tf-' + b}
@@ -1377,8 +1453,17 @@ function GenericDetail({
1377
1453
 
1378
1454
  tabs.forEach((tab) => {
1379
1455
  if (tab.type && tab.type === 'dropzone') {
1380
- if (res.files) {
1381
- setFiles(res.files);
1456
+ if (
1457
+ tab.file_relationship &&
1458
+ tab.file_relationship !== ''
1459
+ ) {
1460
+ if (res[tab.file_relationship]) {
1461
+ setFiles(res[tab.file_relationship]);
1462
+ }
1463
+ } else {
1464
+ if (res.files) {
1465
+ setFiles(res.files);
1466
+ }
1382
1467
  }
1383
1468
  }
1384
1469
  });
@@ -1454,10 +1539,6 @@ function GenericDetail({
1454
1539
  <div className={styles.oppstatus}>
1455
1540
  <ul className={styles.opppath}>
1456
1541
  {stages.data.map((stage) => {
1457
- if (!stage.id) {
1458
- return null; // Skip rendering this stage
1459
- }
1460
-
1461
1542
  const isStageComplete =
1462
1543
  data[stages.key] >= stage.id;
1463
1544