@visns-studio/visns-components 5.0.29 → 5.1.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.
package/package.json CHANGED
@@ -77,7 +77,7 @@
77
77
  "react-dom": "^17.0.0 || ^18.0.0"
78
78
  },
79
79
  "name": "@visns-studio/visns-components",
80
- "version": "5.0.29",
80
+ "version": "5.1.0",
81
81
  "description": "Various packages to assist in the development of our Custom Applications.",
82
82
  "main": "src/index.js",
83
83
  "files": [
@@ -2009,7 +2009,10 @@ const DataGrid = forwardRef(
2009
2009
  }
2010
2010
  }
2011
2011
 
2012
- if (typeof value === 'string') {
2012
+ if (
2013
+ typeof value === 'string' ||
2014
+ typeof value === 'number'
2015
+ ) {
2013
2016
  return <span>{value}</span>;
2014
2017
  } else {
2015
2018
  return null;
@@ -4,6 +4,7 @@ import {
4
4
  Bug,
5
5
  Calendar,
6
6
  Draft,
7
+ PeopleGroup,
7
8
  Person,
8
9
  Search,
9
10
  SignOut,
@@ -207,11 +208,18 @@ function Navigation({
207
208
  bug: Bug,
208
209
  calendar: Calendar,
209
210
  draft: Draft,
211
+ peopleGroup: PeopleGroup,
210
212
  person: Person,
211
213
  shield: Shield,
212
214
  signOut: SignOut,
213
215
  };
214
216
 
217
+ // Check if the setting's URL matches the current page
218
+ const isActive = n.url && n.url === `/${currentPage}`;
219
+
220
+ // Add active class if the setting is active
221
+ const activeClass = isActive ? styles.active : '';
222
+
215
223
  if (n.id === 'notifications') {
216
224
  return (
217
225
  <li>
@@ -237,6 +245,7 @@ function Navigation({
237
245
  data-tooltip-id="system-tooltip"
238
246
  data-tooltip-content={n.label}
239
247
  onClick={handleLogout}
248
+ className={activeClass} // Add active class to the logout button
240
249
  >
241
250
  <IconComponent size={19} strokeWidth={2} />
242
251
  </button>
@@ -246,12 +255,14 @@ function Navigation({
246
255
  const IconComponent = iconComponents[n.icon];
247
256
 
248
257
  return (
249
- <li>
258
+ <li className={activeClass}>
259
+ {' '}
260
+ {/* Apply active class to the list item */}
250
261
  <Link
251
262
  to={n.url}
252
263
  data-tooltip-id="system-tooltip"
253
264
  data-tooltip-content={n.label}
254
- className="tooltip"
265
+ className={`tooltip ${activeClass}`} // Apply active class to the link
255
266
  target={n.target ? n.target : '_self'}
256
267
  >
257
268
  <IconComponent size={19} strokeWidth={2} />
@@ -387,30 +398,51 @@ function Navigation({
387
398
  useEffect(() => {
388
399
  setNavData((prevNavData) => {
389
400
  const updateNavItems = (nav) => {
390
- const cleanUrl = (url) =>
391
- url && url.startsWith('/')
392
- ? url.substr(1).split('/')[0]
393
- : url
394
- ? url.split('/')[0]
395
- : '';
401
+ const cleanUrl = (url) => {
402
+ if (url) {
403
+ // Ensure the URL starts with a slash. If it doesn't, prepend it.
404
+ return url.startsWith('/') ? url : `/${url}`;
405
+ }
406
+ return '';
407
+ };
408
+
409
+ // Ensure both URLs have the leading slash for consistent comparison
396
410
  const currentUrl = cleanUrl(currentPage);
397
- const navUrl = nav.url
398
- ? cleanUrl(nav.url)
399
- ? cleanUrl(nav.url)
400
- : 'dashboard'
401
- : '';
411
+ const navUrl = nav.url ? cleanUrl(nav.url) : 'dashboard';
402
412
 
413
+ // Split current URL into segments
414
+ const currentUrlSegments = currentUrl
415
+ .split('/')
416
+ .filter(Boolean);
417
+
418
+ // Handle the case where navUrl is just '/'
403
419
  if (
404
- navUrl === currentUrl ||
405
- (nav.children &&
406
- nav.children.some(
407
- (child) => child.url === `/${currentPage}`
408
- ))
420
+ navUrl === '/' &&
421
+ (currentUrl === '/dashboard' || currentUrl === '/')
409
422
  ) {
410
- return { ...nav, class: 'nav-item active' };
423
+ return { ...nav, class: 'nav-item active' }; // Mark as active for dashboard if navUrl is '/'
424
+ }
425
+
426
+ // If current URL has 2 or fewer segments, check if navUrl is contained in currentUrl
427
+ if (currentUrlSegments.length <= 2) {
428
+ if (currentUrl.includes(navUrl) && nav.id !== 'dashboard') {
429
+ return { ...nav, class: 'nav-item active' }; // Mark as active if contained
430
+ }
411
431
  } else {
412
- return { ...nav, class: 'nav-item' };
432
+ // If more than 2 segments, use exact match logic
433
+ if (
434
+ navUrl === currentUrl ||
435
+ (nav.children &&
436
+ nav.children.some(
437
+ (child) =>
438
+ cleanUrl(child.url) === `/${currentPage}`
439
+ ))
440
+ ) {
441
+ return { ...nav, class: 'nav-item active' }; // Mark as active
442
+ }
413
443
  }
444
+
445
+ return { ...nav, class: 'nav-item' }; // Default class
414
446
  };
415
447
 
416
448
  return {
@@ -6,292 +6,119 @@ import { Search } from 'akar-icons';
6
6
  import CustomFetch from './Fetch';
7
7
  import Form from './Form';
8
8
 
9
- const QuickAction = ({ childRef, navConfig }) => {
9
+ import styles from './styles/QuickAction.module.scss';
10
+
11
+ const QuickAction = ({ childRef, setting }) => {
10
12
  const navigate = useNavigate();
11
13
  const location = useLocation();
12
14
  const searchBarRef = useRef(null);
13
15
 
14
- /** Modal Contact */
15
- const [contactModalShow, setContactModalShow] = useState(false);
16
- const [contactFormType, setContactFormType] = useState('create');
17
- const [contactFormId, setContactFormId] = useState(0);
18
- const [contactForm, setContactForm] = useState({
19
- primaryKey: 'id',
20
- url: '/ajax/contacts',
21
- create: {
22
- title: 'Create a new Contact entry',
23
- },
24
- update: {
25
- title: 'Update an Existing Contact entry',
26
- },
27
- fields: [
28
- {
29
- id: 'id',
30
- label: 'ID',
31
- type: 'hidden',
32
- required: false,
33
- size: 'half',
34
- },
35
- {
36
- id: 'title',
37
- label: 'Title',
38
- type: 'text',
39
- required: false,
40
- size: 'full',
41
- },
42
- {
43
- id: 'firstname',
44
- label: 'Firstname',
45
- type: 'text',
46
- required: true,
47
- size: 'half',
48
- },
49
- {
50
- id: 'surname',
51
- label: 'Surname',
52
- type: 'text',
53
- required: true,
54
- size: 'half',
55
- },
56
- {
57
- id: 'address1',
58
- label: 'Address',
59
- type: 'json',
60
- parent: 'address',
61
- required: false,
62
- size: 'half',
63
- },
64
- {
65
- id: 'suburb',
66
- label: 'Suburb',
67
- type: 'json',
68
- parent: 'address',
69
- required: false,
70
- size: 'half',
71
- },
72
- {
73
- id: 'postcode',
74
- label: 'Postcode',
75
- type: 'json',
76
- parent: 'address',
77
- required: false,
78
- size: 'half',
79
- },
80
- {
81
- id: 'state',
82
- label: 'State',
83
- type: 'json',
84
- parent: 'address',
85
- required: false,
86
- size: 'half',
87
- },
88
- {
89
- id: 'phone',
90
- label: 'Phone',
91
- type: 'phone',
92
- required: false,
93
- size: 'half',
94
- },
95
- {
96
- id: 'mobile',
97
- label: 'mobile',
98
- type: 'mobile',
99
- required: false,
100
- size: 'half',
101
- },
102
- {
103
- id: 'fax',
104
- label: 'Fax',
105
- type: 'phone',
106
- required: false,
107
- size: 'half',
108
- },
109
- {
110
- id: 'email',
111
- label: 'Email',
112
- type: 'text',
113
- required: false,
114
- size: 'half',
115
- },
116
- {
117
- id: 'client_id',
118
- label: 'User',
119
- type: 'multi-dropdown-ajax',
120
- url: '/ajax/clients/dropdown',
121
- relation: ['client'],
122
- relationName: 'name',
123
- required: false,
124
- size: 'half',
125
- },
126
- {
127
- id: 'position',
128
- label: 'Position',
129
- type: 'text',
130
- required: false,
131
- size: 'half',
132
- },
133
- {
134
- id: 'contact_types',
135
- label: 'Role Type (Select Applicable)',
136
- type: 'multi-dropdown-ajax',
137
- url: '/ajax/contactTypes/dropdown',
138
- required: false,
139
- multi: true,
140
- size: 'half',
141
- },
142
- {
143
- id: 'contacts_industries',
144
- label: 'Sector (Select Applicable)',
145
- type: 'multi-dropdown-ajax',
146
- url: '/ajax/industries/dropdown',
147
- required: true,
148
- multi: true,
149
- size: 'half',
150
- },
151
- {
152
- id: 'stakeholder_type_id',
153
- label: 'Stakeholder Type',
154
- type: 'dropdown-ajax',
155
- url: '/ajax/stakeholderTypes/dropdown',
156
- where: [],
157
- child: {
158
- id: 'stakeholder_category_id',
159
- url: '/ajax/stakeholderCategories/dropdown',
160
- },
161
- relation: ['stakeholderTypes'],
162
- relation_from: 'id',
163
- required: false,
164
- size: 'half',
165
- },
166
- {
167
- id: 'stakeholder_category_id',
168
- label: 'Stakeholder Category',
169
- type: 'dropdown',
170
- options: [],
171
- required: false,
172
- size: 'half',
173
- },
174
- {
175
- id: 'bio',
176
- label: 'Bio',
177
- type: 'richeditor',
178
- required: false,
179
- size: 'full',
180
- },
181
- ],
182
- });
16
+ /** State Variables */
17
+ const [search, setSearch] = useState('');
18
+ const [searchResults, setSearchResults] = useState([]);
19
+ const [showSearchResults, setShowSearchResults] = useState(false);
20
+ const [modalShow, setModalShow] = useState(false);
21
+ const [formType, setFormType] = useState('create');
22
+ const [formId, setFormId] = useState(0);
23
+ const [form, setForm] = useState({});
183
24
 
184
- const contactModalClose = (e) => {
185
- setContactModalShow(false);
25
+ /** Helper Functions */
26
+ const fetchSearchResults = async () => {
27
+ try {
28
+ const res = await CustomFetch(setting.search.url, 'POST', {
29
+ search,
30
+ });
31
+ setSearchResults(res.data);
32
+ setSearch('');
33
+ } catch (e) {
34
+ console.error(e);
35
+ }
186
36
  };
187
37
 
188
- const contactModalOpen = (ft, fi) => {
189
- setContactModalShow(true);
190
- setContactFormType(ft);
191
- setContactFormId(fi);
38
+ const handleSearch = (e) => {
39
+ const value = e.target.value;
40
+ setSearch(value);
41
+ if (e.key === 'Enter') fetchSearchResults();
192
42
  };
193
43
 
194
- const contactChildDropdownCallback = (data) => {
195
- let _form = contactForm;
196
-
197
- contactForm.fields.map((a, b) => {
198
- if (a.id === data.id) {
199
- _form.fields[b].options = data.data;
44
+ const handleSelectResult = (type, result) => {
45
+ const url = setting.search.types.find((t) => t.title === type)?.url;
46
+ if (url) {
47
+ const key = setting.search.types.find((t) => t.title === type)?.key;
48
+ if (key && result[key]) {
49
+ navigate(`${url}/${result[key]}`);
50
+ } else {
51
+ navigate(url);
200
52
  }
201
- });
202
-
203
- setContactForm(() => ({
204
- ..._form,
205
- }));
53
+ }
54
+ setSearchResults([]);
206
55
  };
207
56
 
208
- /** Modal Client */
209
- const [clientModalShow, setClientModalShow] = useState(false);
210
- const [clientFormType, setClientFormType] = useState('create');
211
- const [clientFormId, setClientFormId] = useState(0);
212
- const [clientForm, setClientForm] = useState({
213
- primaryKey: 'id',
214
- url: '/ajax/clients',
215
- create: {
216
- title: 'Create a new User entry',
217
- },
218
- update: {
219
- title: 'Update an Existing User entry',
220
- },
221
- fields: [
222
- {
223
- id: 'id',
224
- label: 'ID',
225
- type: 'hidden',
226
- required: false,
227
- size: 'half',
228
- },
229
- {
230
- id: 'name',
231
- label: 'Name',
232
- type: 'text',
233
- required: true,
234
- size: 'half',
235
- },
236
- {
237
- id: 'abn',
238
- label: 'ABN',
239
- type: 'text',
240
- required: false,
241
- size: 'half',
242
- },
243
- {
244
- id: 'notes',
245
- label: 'Notes',
246
- type: 'textarea',
247
- required: false,
248
- size: 'full',
249
- },
250
- ],
251
- });
252
-
253
- const clientModalClose = (e) => {
254
- setClientModalShow(false);
57
+ const handleButtonClick = (buttonSetting) => {
58
+ // Check if the form object is identical to the current form
59
+ if (JSON.stringify(buttonSetting.form) === JSON.stringify(form)) {
60
+ modalOpen('create', 0); // Call modalOpen if they are the same
61
+ } else {
62
+ setForm(buttonSetting.form); // Otherwise, update the form state
63
+ }
255
64
  };
256
65
 
257
- const clientModalOpen = (ft, fi) => {
258
- setClientModalShow(true);
259
- setClientFormType(ft);
260
- setClientFormId(fi);
66
+ const modalClose = () => setModalShow(false);
67
+ const modalOpen = (ft, fi) => {
68
+ setModalShow(true);
69
+ setFormType(ft);
70
+ setFormId(fi);
261
71
  };
262
72
 
263
- const clientChildDropdownCallback = (data) => {
264
- let _form = clientForm;
265
-
266
- clientForm.fields.map((a, b) => {
267
- if (a.id === data.id) {
268
- _form.fields[b].options = data.data;
269
- }
73
+ const childDropdownCallback = (data) => {
74
+ setForm((prevForm) => {
75
+ const updatedFields = prevForm.fields.map((field) =>
76
+ field.id === data.id ? { ...field, options: data.data } : field
77
+ );
78
+ return { ...prevForm, fields: updatedFields };
270
79
  });
271
-
272
- setClientForm(() => ({
273
- ..._form,
274
- }));
275
80
  };
276
81
 
277
- /** Universal Search */
278
- const [search, setSearch] = useState('');
279
- const [searchResults, setSearchResults] = useState([]);
280
- const [showSearchResults, setShowSearchResults] = useState(false);
82
+ /** Effect Hooks */
83
+ useEffect(() => {
84
+ if (form?.fields?.length) {
85
+ modalOpen('create', 0);
86
+ }
87
+ }, [form]);
88
+
89
+ useEffect(() => {
90
+ const hasSearchResults = setting.search.types.some(
91
+ (type) => searchResults[type.id]?.length > 0
92
+ );
93
+ setShowSearchResults(hasSearchResults);
94
+ }, [searchResults]);
281
95
 
282
- const ResultSection = ({ title, data, handleSelectResult }) => {
283
- if (data === null || data === undefined || data?.length === 0)
284
- return null;
96
+ useEffect(() => {
97
+ const handleClickOutside = (event) => {
98
+ if (
99
+ searchBarRef.current &&
100
+ !searchBarRef.current.contains(event.target)
101
+ ) {
102
+ setShowSearchResults(false);
103
+ }
104
+ };
105
+ document.addEventListener('mousedown', handleClickOutside);
106
+ return () =>
107
+ document.removeEventListener('mousedown', handleClickOutside);
108
+ }, []);
285
109
 
110
+ /** Component Structure */
111
+ const ResultSection = ({ title, data }) => {
112
+ if (!data?.length) return null;
286
113
  return (
287
114
  <>
288
- <div className="resultItem">
115
+ <div className={styles.resultItem}>
289
116
  <h3>{title}</h3>
290
117
  </div>
291
118
  {data.map((result) => (
292
119
  <div
293
120
  key={result.id}
294
- className="resultItem"
121
+ className={styles.resultItem}
295
122
  onClick={() => handleSelectResult(title, result)}
296
123
  >
297
124
  <p>
@@ -305,110 +132,26 @@ const QuickAction = ({ childRef, navConfig }) => {
305
132
  );
306
133
  };
307
134
 
308
- const handleSearch = (e) => {
309
- setSearchResults([]);
310
- setSearch(e.target.value);
311
-
312
- if (e.key === 'Enter') {
313
- // Check if the Enter key was pressed
314
- fetchSearchResults();
315
- }
316
- };
317
-
318
- const handleSelectResult = (type, d) => {
319
- switch (type) {
320
- case 'Contacts':
321
- navigate(`/contacts`);
322
- break;
323
- case 'Enquiries':
324
- navigate(`/leads/${d.id}`);
325
- break;
326
- case 'Users':
327
- navigate(`/clients/${d.id}`);
328
- break;
329
- }
330
- setSearchResults([]);
331
- };
332
-
333
- const fetchSearchResults = async () => {
334
- try {
335
- const res = await CustomFetch('/ajax/dashboard/search', 'POST', {
336
- search,
337
- });
338
-
339
- setSearchResults(res.data);
340
- setSearch('');
341
- } catch (e) {
342
- console.log(e);
343
- }
344
- };
345
-
346
- useEffect(() => {
347
- let ssr = false;
348
-
349
- if (searchResults?.contacts?.length > 0) {
350
- ssr = true;
351
- }
352
-
353
- if (searchResults?.enquiries?.length > 0) {
354
- ssr = true;
355
- }
356
-
357
- if (searchResults?.users?.length > 0) {
358
- ssr = true;
359
- }
360
-
361
- setShowSearchResults(ssr);
362
- }, [searchResults]);
363
-
364
- useEffect(() => {
365
- const handleClickOutside = (event) => {
366
- if (
367
- searchBarRef.current &&
368
- !searchBarRef.current.contains(event.target)
369
- ) {
370
- setShowSearchResults(false);
371
- }
372
- };
373
-
374
- document.addEventListener('mousedown', handleClickOutside);
375
- return () => {
376
- document.removeEventListener('mousedown', handleClickOutside);
377
- };
378
- }, []);
379
-
380
135
  return (
381
136
  <>
382
- <div className="quickActions">
137
+ <div className={styles.quickActions}>
383
138
  <span>Quick Actions</span>
384
- <button
385
- onClick={() => {
386
- navigate('/leads/create');
387
- }}
388
- >
389
- New Enquiry
390
- </button>
391
- <button
392
- onClick={() => {
393
- clientModalOpen('create', 0);
394
- }}
395
- >
396
- New User
397
- </button>
398
- <button
399
- onClick={() => {
400
- if (location.pathname === '/leads/create') {
401
- console.info('aa');
402
- childRef.current.contactOpen();
403
- } else {
404
- contactModalOpen('create', 0);
405
- }
406
- }}
407
- >
408
- New Contact
409
- </button>
139
+ {setting.buttons.map((b) => (
140
+ <button
141
+ key={`button-${b.id}`}
142
+ onClick={() => {
143
+ if (b.url) {
144
+ navigate(b.url);
145
+ } else {
146
+ handleButtonClick(b);
147
+ }
148
+ }}
149
+ >
150
+ {b.label}
151
+ </button>
152
+ ))}
410
153
 
411
- <div className="searchBar" ref={searchBarRef}>
154
+ <div className={styles.searchBar} ref={searchBarRef}>
412
155
  <input
413
156
  type="text"
414
157
  placeholder="Search..."
@@ -416,56 +159,36 @@ const QuickAction = ({ childRef, navConfig }) => {
416
159
  onChange={handleSearch}
417
160
  onKeyDown={handleSearch}
418
161
  />
419
-
420
- <div className="iconContainer">
162
+ <div className={styles.iconContainer}>
421
163
  <Search
422
164
  strokeWidth={2}
423
165
  size={18}
424
- className="searchIcon"
166
+ className={styles.searchIcon}
425
167
  />
426
168
  </div>
427
169
 
428
170
  {showSearchResults && (
429
- <div className="searchResults">
430
- <ResultSection
431
- title="Contacts"
432
- data={searchResults.contacts}
433
- handleSelectResult={handleSelectResult}
434
- />
435
- <ResultSection
436
- title="Enquiries"
437
- data={searchResults.enquiries}
438
- handleSelectResult={handleSelectResult}
439
- />
440
- <ResultSection
441
- title="Users"
442
- data={searchResults.users}
443
- handleSelectResult={handleSelectResult}
444
- />
171
+ <div className={styles.searchResults}>
172
+ {setting.search.types.map((t) => (
173
+ <ResultSection
174
+ key={t.id}
175
+ title={t.title}
176
+ data={searchResults[t.id]}
177
+ />
178
+ ))}
445
179
  </div>
446
180
  )}
447
181
  </div>
448
182
  </div>
449
183
 
450
- <Popup open={contactModalShow} onClose={contactModalClose}>
451
- <Form
452
- closeModal={contactModalClose}
453
- fetchTable={() => {}}
454
- formSettings={contactForm}
455
- formType={contactFormType}
456
- columnId={contactFormId}
457
- childDropdownCallback={contactChildDropdownCallback}
458
- />
459
- </Popup>
460
-
461
- <Popup open={clientModalShow} onClose={clientModalClose}>
184
+ <Popup open={modalShow} onClose={modalClose}>
462
185
  <Form
463
- closeModal={clientModalClose}
186
+ closeModal={modalClose}
464
187
  fetchTable={() => {}}
465
- formSettings={clientForm}
466
- formType={clientFormType}
467
- columnId={clientFormId}
468
- childDropdownCallback={clientChildDropdownCallback}
188
+ formSettings={form}
189
+ formType={formType}
190
+ columnId={formId}
191
+ childDropdownCallback={childDropdownCallback}
469
192
  />
470
193
  </Popup>
471
194
  </>
@@ -1,7 +1,7 @@
1
1
  import '../../styles/global.css';
2
2
 
3
3
  import React, { useCallback, useEffect, useRef, useState } from 'react';
4
- import { Outlet } from 'react-router-dom';
4
+ import { useParams, Outlet } from 'react-router-dom';
5
5
  import { toast } from 'react-toastify';
6
6
  import { saveAs } from 'file-saver';
7
7
  import _ from 'lodash';
@@ -182,7 +182,9 @@ function GenericIndex({
182
182
  userProfile,
183
183
  }) {
184
184
  const gridRef = useRef(null);
185
+ const params = useParams();
185
186
  const [rowsSelected, setRowsSelected] = useState({});
187
+ const [tableInfo, setTableInfo] = useState({});
186
188
  const [total, setTotal] = useState(0);
187
189
  const windowHeight = useWindowHeight();
188
190
  const { config, setConfig, subnav, setSubnav } = useConfig(
@@ -370,6 +372,28 @@ function GenericIndex({
370
372
  }
371
373
  }, [layout, subnav, setSubnav, setConfig, renderTable]);
372
374
 
375
+ useEffect(() => {
376
+ if (setting.page?.tableInfo?.hasOwnProperty('url')) {
377
+ const fetchTableInfo = async () => {
378
+ try {
379
+ const res = await CustomFetch(
380
+ setting.page.tableInfo.url,
381
+ 'POST',
382
+ {}
383
+ );
384
+
385
+ if (res.data) {
386
+ setTableInfo(res.data);
387
+ }
388
+ } catch (error) {
389
+ console.error(error);
390
+ }
391
+ };
392
+
393
+ fetchTableInfo();
394
+ }
395
+ }, [setting]);
396
+
373
397
  useEffect(() => {
374
398
  if (setActiveNav) {
375
399
  setActiveNav();
@@ -389,6 +413,16 @@ function GenericIndex({
389
413
  >
390
414
  <h1>{setting.page?.title}</h1>
391
415
  <div className={styles.titleInfo}>
416
+ {setting.page?.tableInfo?.hasOwnProperty(
417
+ 'heading'
418
+ ) &&
419
+ setting.page.tableInfo.heading.length > 0 &&
420
+ setting.page.tableInfo.heading.map((h) => (
421
+ <span>
422
+ [<strong>{tableInfo[h.id]}</strong>{' '}
423
+ {h.label}]{' '}
424
+ </span>
425
+ ))}
392
426
  <span>
393
427
  [<strong>{total}</strong> Total{' '}
394
428
  {setting.page?.title}]
@@ -6,6 +6,7 @@ import { Routes, Route, Navigate } from 'react-router-dom';
6
6
  import Call from '../Call';
7
7
  import Loader from '../Loader';
8
8
  import Navigation from '../Navigation';
9
+ import QuickAction from '../QuickAction';
9
10
 
10
11
  import styles from './styles/GenericMain.module.scss';
11
12
 
@@ -85,6 +86,9 @@ const GenericMain = ({
85
86
  userProfile={userProfile}
86
87
  />
87
88
  </header>
89
+ {navigation.hasOwnProperty('quickActions') && (
90
+ <QuickAction setting={navigation.quickActions} />
91
+ )}
88
92
  <main className={styles.content}>
89
93
  <div className={styles['content-main']} id="app">
90
94
  <div className={styles.padding}>
@@ -240,7 +240,7 @@ input[type='file'] {
240
240
  position: relative;
241
241
  padding: 0.75rem;
242
242
  max-height: 80vh;
243
- // overflow-y: auto;
243
+ overflow-y: auto;
244
244
 
245
245
  .btn {
246
246
  background: var(--primary-color);
@@ -61,7 +61,7 @@
61
61
 
62
62
  a,
63
63
  span {
64
- font-size: 1rem;
64
+ font-size: 0.9rem;
65
65
  display: flex;
66
66
  flex-wrap: nowrap;
67
67
  align-items: center;
@@ -260,7 +260,8 @@
260
260
  display: flex;
261
261
  align-items: center;
262
262
 
263
- &:hover {
263
+ &:hover,
264
+ &.active {
264
265
  background: var(--primary-color);
265
266
 
266
267
  svg {
@@ -283,7 +284,8 @@
283
284
  outline: none;
284
285
  appearance: none;
285
286
 
286
- &:hover {
287
+ &:hover,
288
+ &.active {
287
289
  background: var(--primary-color);
288
290
 
289
291
  svg {
@@ -0,0 +1,126 @@
1
+ .quickActions {
2
+ width: 100%;
3
+ background: rgba(var(--bg-rgb), 0.95);
4
+ padding: var(--padding);
5
+ border-top: solid 4px rgba(var(--bg-rgb), 1.05);
6
+ border-bottom: solid 1px rgba(var(--primary-rgb), 0.25);
7
+ color: var(--paragraph-color);
8
+ display: flex;
9
+ flex-wrap: nowrap;
10
+ align-items: center;
11
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
12
+
13
+ > span {
14
+ width: max-content;
15
+ display: block;
16
+ font-weight: var(--title-weight);
17
+ padding: 0 1rem 0 0;
18
+ }
19
+
20
+ > button {
21
+ appearance: none;
22
+ border: 1px solid rgba(var(--primary-rgb), 0.25);
23
+ border-radius: var(--br);
24
+ background: transparent;
25
+ width: max-content;
26
+ color: var(--paragraph-color);
27
+ padding: 0.35rem 0.65rem;
28
+ margin: 0 0.25rem;
29
+ transition: all 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
30
+
31
+ &:hover {
32
+ background: var(--tertiary-color);
33
+ transform: translateY(-2px);
34
+ }
35
+ }
36
+
37
+ .searchBar {
38
+ display: flex;
39
+ align-items: center;
40
+ background: transparent;
41
+ margin: 0 0.25rem;
42
+ flex-grow: 1;
43
+ position: relative;
44
+
45
+ input[type='text'] {
46
+ flex-grow: 1;
47
+ border: none;
48
+ outline: none;
49
+ padding: 4px 0.65rem;
50
+ padding-right: 30px;
51
+ border-radius: var(--br);
52
+ height: 32px;
53
+ font-size: 14px;
54
+ width: 100%;
55
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.05);
56
+
57
+ &:focus {
58
+ box-shadow: 0 0 8px rgba(var(--primary-rgb), 0.5);
59
+ }
60
+ }
61
+
62
+ .iconContainer {
63
+ position: absolute;
64
+ right: 10px;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ }
69
+
70
+ .searchIcon {
71
+ width: 20px;
72
+ height: 20px;
73
+ }
74
+ }
75
+
76
+ .searchResults {
77
+ position: absolute;
78
+ top: 100%;
79
+ width: 100%;
80
+ background: var(--third-color);
81
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
82
+ z-index: 100;
83
+ max-height: 400px;
84
+ overflow-y: auto;
85
+ border-radius: var(--br);
86
+ margin-top: 5px;
87
+ }
88
+
89
+ .resultItem {
90
+ padding: 0.5rem 1rem;
91
+ cursor: pointer;
92
+ transition: background-color 0.3s ease, transform 0.2s ease;
93
+
94
+ &:hover {
95
+ background-color: #f2f2f2;
96
+ transform: translateX(5px);
97
+ }
98
+
99
+ &:last-child {
100
+ border-bottom: none;
101
+ }
102
+
103
+ h3 {
104
+ margin: 0;
105
+ font-weight: var(--title-weight);
106
+ color: var(--header-color);
107
+ font-size: 14px;
108
+ }
109
+
110
+ p {
111
+ margin: 0.25rem 0 0;
112
+ color: var(--paragraph-color);
113
+ font-size: 13px;
114
+ }
115
+
116
+ &.selected {
117
+ background-color: rgba(var(--primary-rgb), 0.1);
118
+ }
119
+ }
120
+
121
+ .resultItemDivider {
122
+ height: 1px;
123
+ background-color: var(--border-color);
124
+ margin: 0.25rem 0;
125
+ }
126
+ }