@visns-studio/visns-components 5.14.0 → 5.14.2

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.
@@ -8,7 +8,7 @@ import Popup from 'reactjs-popup';
8
8
  import dayjs from 'dayjs';
9
9
  import parse from 'html-react-parser';
10
10
  import { createSwapy } from 'swapy';
11
- import { X, Minus, Plus, Star, Trash2 } from 'lucide-react';
11
+ import { X, Minus, Plus, Star, Trash2, Eye, RotateCcw, Clock, Database, BarChart3, AlertCircle } from 'lucide-react';
12
12
  import { confirmDialog } from '../utils/ConfirmDialog';
13
13
 
14
14
  import CustomFetch from '../Fetch';
@@ -18,6 +18,34 @@ import styles from '../styles/GenericDashboard.module.scss';
18
18
 
19
19
  const toSnakeCase = (str) => str.replace(/[\s-]+/g, '_').toLowerCase();
20
20
 
21
+ // Helper function to format dates for display in Australian format
22
+ const formatDateForDisplay = (dateString) => {
23
+ if (!dateString || dateString === 'Invalid Date' || dateString === 'null' || dateString === 'undefined') {
24
+ return '';
25
+ }
26
+
27
+ // Try to parse the date
28
+ const date = dayjs(dateString);
29
+
30
+ // Check if the parsed date is valid
31
+ if (!date.isValid()) {
32
+ // If it's already in DD/MM/YYYY format, return as is
33
+ if (typeof dateString === 'string' && /^\d{2}\/\d{2}\/\d{4}/.test(dateString)) {
34
+ return dateString;
35
+ }
36
+ // Return original value if we can't parse it
37
+ return dateString;
38
+ }
39
+
40
+ // Format with time if the original string includes time
41
+ if (typeof dateString === 'string' && dateString.includes(':')) {
42
+ return date.format('DD/MM/YYYY hh:mm A');
43
+ }
44
+
45
+ // Default to date only
46
+ return date.format('DD/MM/YYYY');
47
+ };
48
+
21
49
  const renderProgressBar = (value, options = {}) => {
22
50
  const percentage = parseFloat(value) || 0;
23
51
  const { showPercentage = true, color = 'primary' } = options;
@@ -83,13 +111,27 @@ const renderStatusMessage = (status, messages) => {
83
111
  padding: '4px 8px',
84
112
  backgroundColor: '#f8f9fa',
85
113
  borderRadius: '4px',
86
- border: '1px solid #e9ecef'
114
+ border: '1px solid #e9ecef',
115
+ fontFamily: 'inherit'
87
116
  }}>
88
117
  {message}
89
118
  </div>
90
119
  );
91
120
  };
92
121
 
122
+ const renderStatusText = (status) => {
123
+ // Simple text rendering without any special styling to avoid progress bar appearance
124
+ return (
125
+ <span style={{
126
+ fontFamily: 'inherit',
127
+ fontSize: '14px',
128
+ color: '#333'
129
+ }}>
130
+ {status || 'Unknown'}
131
+ </span>
132
+ );
133
+ };
134
+
93
135
  const checkCondition = (rowData, condition) => {
94
136
  if (!condition || !rowData) return false;
95
137
 
@@ -112,6 +154,106 @@ const renderCellContent = (value, column, widget, rowData = {}) => {
112
154
  const columnFormatting = widget.columnFormatting || {};
113
155
  const formatting = columnFormatting[column];
114
156
 
157
+ // Handle status column FIRST - render as plain text instead of progress bar
158
+ if (column === 'status' || column === 'processing_status' || column.toLowerCase().includes('status')) {
159
+ return renderStatusText(value);
160
+ }
161
+
162
+ // Handle action buttons with icons and CSS tooltips
163
+ if (column === 'actions' && Array.isArray(value)) {
164
+ return (
165
+ <div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
166
+ {value.map((action, index) => {
167
+ const IconComponent = getActionIcon(action.action);
168
+ return (
169
+ <div
170
+ key={index}
171
+ className={`tooltip-container-${index}`}
172
+ style={{ position: 'relative', display: 'inline-block' }}
173
+ onMouseEnter={(e) => {
174
+ const tooltip = e.currentTarget.querySelector('.tooltip-text');
175
+ if (tooltip) {
176
+ tooltip.style.opacity = '1';
177
+ tooltip.style.visibility = 'visible';
178
+ }
179
+ const button = e.currentTarget.querySelector('button');
180
+ if (button) {
181
+ button.style.backgroundColor = '#f8f9fa';
182
+ button.style.transform = 'translateY(-1px)';
183
+ button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
184
+ }
185
+ }}
186
+ onMouseLeave={(e) => {
187
+ const tooltip = e.currentTarget.querySelector('.tooltip-text');
188
+ if (tooltip) {
189
+ tooltip.style.opacity = '0';
190
+ tooltip.style.visibility = 'hidden';
191
+ }
192
+ const button = e.currentTarget.querySelector('button');
193
+ if (button) {
194
+ button.style.backgroundColor = '#fff';
195
+ button.style.transform = 'translateY(0)';
196
+ button.style.boxShadow = 'none';
197
+ }
198
+ }}
199
+ >
200
+ <button
201
+ onClick={() => handleActionClick(action, rowData)}
202
+ style={{
203
+ padding: '6px',
204
+ borderRadius: '4px',
205
+ border: '1px solid #dee2e6',
206
+ cursor: 'pointer',
207
+ backgroundColor: '#fff',
208
+ color: getActionIconColor(action.action),
209
+ display: 'flex',
210
+ alignItems: 'center',
211
+ justifyContent: 'center',
212
+ fontSize: '14px',
213
+ fontFamily: 'inherit',
214
+ transition: 'all 0.2s ease'
215
+ }}
216
+ >
217
+ <IconComponent size={14} />
218
+ </button>
219
+ <div className="tooltip-text" style={{
220
+ position: 'absolute',
221
+ bottom: '125%',
222
+ left: '50%',
223
+ transform: 'translateX(-50%)',
224
+ backgroundColor: '#333',
225
+ color: '#fff',
226
+ padding: '6px 8px',
227
+ borderRadius: '4px',
228
+ fontSize: '12px',
229
+ fontFamily: 'inherit',
230
+ whiteSpace: 'nowrap',
231
+ opacity: '0',
232
+ visibility: 'hidden',
233
+ transition: 'opacity 0.3s, visibility 0.3s',
234
+ zIndex: '1000',
235
+ pointerEvents: 'none'
236
+ }}>
237
+ {action.label}
238
+ <div style={{
239
+ position: 'absolute',
240
+ top: '100%',
241
+ left: '50%',
242
+ transform: 'translateX(-50%)',
243
+ width: 0,
244
+ height: 0,
245
+ borderLeft: '4px solid transparent',
246
+ borderRight: '4px solid transparent',
247
+ borderTop: '4px solid #333'
248
+ }} />
249
+ </div>
250
+ </div>
251
+ );
252
+ })}
253
+ </div>
254
+ );
255
+ }
256
+
115
257
  if (formatting && formatting.type === 'progressBar') {
116
258
  return renderProgressBar(value, formatting);
117
259
  }
@@ -127,9 +269,280 @@ const renderCellContent = (value, column, widget, rowData = {}) => {
127
269
  }
128
270
  }
129
271
 
272
+ // Handle date columns - format for Australian display
273
+ const isDateColumn = column.toLowerCase().includes('date') ||
274
+ column.toLowerCase().includes('_at') ||
275
+ column === 'processed_at' ||
276
+ column === 'failed_at' ||
277
+ column === 'timestamp';
278
+
279
+ if (isDateColumn && value && value !== 'Invalid Date') {
280
+ const formattedDate = formatDateForDisplay(value);
281
+ return <span style={{ fontFamily: 'inherit' }}>{formattedDate}</span>;
282
+ }
283
+
284
+ // Ensure consistent font family for all cell content
285
+ if (typeof value === 'string' || typeof value === 'number') {
286
+ return <span style={{ fontFamily: 'inherit' }}>{value}</span>;
287
+ }
288
+
130
289
  return value;
131
290
  };
132
291
 
292
+ const getActionButtonColor = (className) => {
293
+ if (className?.includes('btn-info')) return '#17a2b8';
294
+ if (className?.includes('btn-warning')) return '#ffc107';
295
+ if (className?.includes('btn-danger')) return '#dc3545';
296
+ if (className?.includes('btn-success')) return '#28a745';
297
+ return '#6c757d'; // default
298
+ };
299
+
300
+ const getActionIcon = (actionType) => {
301
+ switch (actionType) {
302
+ case 'viewLogs':
303
+ return Eye;
304
+ case 'retry':
305
+ return RotateCcw;
306
+ default:
307
+ return Eye; // default icon
308
+ }
309
+ };
310
+
311
+ const getActionIconColor = (actionType) => {
312
+ switch (actionType) {
313
+ case 'viewLogs':
314
+ return '#17a2b8'; // info color
315
+ case 'retry':
316
+ return '#ffc107'; // warning color
317
+ default:
318
+ return '#6c757d'; // default
319
+ }
320
+ };
321
+
322
+ const handleActionClick = (action, rowData) => {
323
+ switch (action.action) {
324
+ case 'viewLogs':
325
+ // Open logs in a modal
326
+ openLogsModal(action.url, rowData);
327
+ break;
328
+ case 'retry':
329
+ // Handle retry action
330
+ if (confirm('Are you sure you want to retry processing this document?')) {
331
+ fetch(action.url, {
332
+ method: action.method || 'POST',
333
+ headers: {
334
+ 'Content-Type': 'application/json',
335
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
336
+ }
337
+ })
338
+ .then(response => response.json())
339
+ .then(data => {
340
+ alert(data.message || 'Action completed');
341
+ // Optionally refresh the widget data
342
+ window.location.reload();
343
+ })
344
+ .catch(error => {
345
+ console.error('Action failed:', error);
346
+ alert('Action failed. Please try again.');
347
+ });
348
+ }
349
+ break;
350
+ default:
351
+ console.log('Unknown action:', action.action);
352
+ }
353
+ };
354
+
355
+ const openLogsModal = async (url, rowData) => {
356
+ try {
357
+ const response = await fetch(url, {
358
+ method: 'GET',
359
+ headers: {
360
+ 'Content-Type': 'application/json',
361
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
362
+ }
363
+ });
364
+
365
+ const data = await response.json();
366
+
367
+ // Create and open logs modal
368
+ const modalHtml = createLogsModalContent(data, rowData);
369
+ showModal('Document Processing Logs', modalHtml);
370
+
371
+ } catch (error) {
372
+ console.error('Failed to fetch logs:', error);
373
+ alert('Failed to load logs. Please try again.');
374
+ }
375
+ };
376
+
377
+ const createLogsModalContent = (data, rowData) => {
378
+ const { document, logs, summary } = data;
379
+
380
+ return `
381
+ <div style="max-height: 70vh; overflow-y: auto;">
382
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
383
+ <h4 style="margin: 0 0 10px 0; color: #333;">Document Information</h4>
384
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; font-size: 14px;">
385
+ <div><strong>File:</strong> ${document.filename}</div>
386
+ <div><strong>Status:</strong> <span style="color: ${getStatusColor(document.status)}">${document.status}</span></div>
387
+ <div><strong>Template:</strong> ${document.template}</div>
388
+ <div><strong>Processing Time:</strong> ${summary.processing_time}</div>
389
+ </div>
390
+ </div>
391
+
392
+ <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 15px; font-size: 14px;">
393
+ <strong>Summary:</strong> ${summary.total_logs} logs |
394
+ <span style="color: #dc3545;">${summary.errors} errors</span> |
395
+ <span style="color: #fd7e14;">${summary.warnings} warnings</span>
396
+ </div>
397
+
398
+ <div style="max-height: 400px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 6px;">
399
+ ${logs.map(log => `
400
+ <div style="padding: 12px; border-bottom: 1px solid #eee; ${getLogBackgroundColor(log.level)}">
401
+ <div style="display: flex; justify-content: between; align-items: flex-start; margin-bottom: 8px;">
402
+ <div style="flex: 1;">
403
+ <span style="background: ${getLogLevelColor(log.level)}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; text-transform: uppercase;">${log.level}</span>
404
+ <span style="color: #6c757d; font-size: 12px; margin-left: 10px;">${log.stage}</span>
405
+ </div>
406
+ <div style="font-size: 11px; color: #6c757d;">${log.timestamp}</div>
407
+ </div>
408
+ <div style="font-size: 14px; margin-bottom: 5px;">${log.message}</div>
409
+ ${log.execution_time_ms ? `<div style="font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 8px;">
410
+ <span style="display: inline-flex; align-items: center; gap: 4px;">
411
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
412
+ <circle cx="12" cy="12" r="10"/>
413
+ <polyline points="12,6 12,12 16,14"/>
414
+ </svg>
415
+ ${log.execution_time_ms}ms
416
+ </span>
417
+ <span style="display: inline-flex; align-items: center; gap: 4px;">
418
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
419
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
420
+ <polyline points="3.29,7 12,12 20.71,7"/>
421
+ <line x1="12" y1="22" x2="12" y2="12"/>
422
+ </svg>
423
+ ${log.memory_usage_mb}MB
424
+ </span>
425
+ </div>` : ''}
426
+ ${log.details && Object.keys(log.details).length > 0 ? `
427
+ <details style="margin-top: 8px;">
428
+ <summary style="font-size: 12px; color: #007bff; cursor: pointer;">View Details</summary>
429
+ <pre style="background: #f8f9fa; padding: 8px; border-radius: 4px; font-size: 11px; margin-top: 5px; white-space: pre-wrap;">${JSON.stringify(log.details, null, 2)}</pre>
430
+ </details>
431
+ ` : ''}
432
+ ${log.stack_trace ? `
433
+ <details style="margin-top: 8px;">
434
+ <summary style="font-size: 12px; color: #dc3545; cursor: pointer;">Stack Trace</summary>
435
+ <pre style="background: #f8d7da; padding: 8px; border-radius: 4px; font-size: 11px; margin-top: 5px; white-space: pre-wrap;">${log.stack_trace}</pre>
436
+ </details>
437
+ ` : ''}
438
+ </div>
439
+ `).join('')}
440
+ </div>
441
+ </div>
442
+ `;
443
+ };
444
+
445
+ const getStatusColor = (status) => {
446
+ switch (status) {
447
+ case 'completed': return '#28a745';
448
+ case 'failed': return '#dc3545';
449
+ case 'processing': return '#007bff';
450
+ case 'pending': return '#6c757d';
451
+ default: return '#6c757d';
452
+ }
453
+ };
454
+
455
+ const getLogLevelColor = (level) => {
456
+ switch (level.toLowerCase()) {
457
+ case 'error': return '#dc3545';
458
+ case 'warning': return '#fd7e14';
459
+ case 'info': return '#007bff';
460
+ case 'debug': return '#6c757d';
461
+ default: return '#6c757d';
462
+ }
463
+ };
464
+
465
+ const getLogBackgroundColor = (level) => {
466
+ switch (level.toLowerCase()) {
467
+ case 'error': return 'background-color: #f8d7da;';
468
+ case 'warning': return 'background-color: #fff3cd;';
469
+ default: return '';
470
+ }
471
+ };
472
+
473
+ const showModal = (title, content) => {
474
+ // Remove any existing modal
475
+ const existingModal = document.getElementById('logs-modal');
476
+ if (existingModal) {
477
+ existingModal.remove();
478
+ }
479
+
480
+ // Create modal HTML
481
+ const modalHtml = `
482
+ <div id="logs-modal" style="
483
+ position: fixed;
484
+ top: 0;
485
+ left: 0;
486
+ width: 100%;
487
+ height: 100%;
488
+ background: rgba(0,0,0,0.5);
489
+ z-index: 10000;
490
+ display: flex;
491
+ align-items: center;
492
+ justify-content: center;
493
+ ">
494
+ <div style="
495
+ background: white;
496
+ border-radius: 8px;
497
+ max-width: 90vw;
498
+ max-height: 90vh;
499
+ width: 1000px;
500
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
501
+ display: flex;
502
+ flex-direction: column;
503
+ ">
504
+ <div style="
505
+ padding: 20px;
506
+ border-bottom: 1px solid #dee2e6;
507
+ display: flex;
508
+ justify-content: space-between;
509
+ align-items: center;
510
+ flex-shrink: 0;
511
+ ">
512
+ <h2 style="margin: 0; color: #333; font-size: 18px;">${title}</h2>
513
+ <button onclick="document.getElementById('logs-modal').remove()" style="
514
+ background: none;
515
+ border: none;
516
+ font-size: 24px;
517
+ cursor: pointer;
518
+ color: #6c757d;
519
+ padding: 0;
520
+ width: 30px;
521
+ height: 30px;
522
+ display: flex;
523
+ align-items: center;
524
+ justify-content: center;
525
+ ">&times;</button>
526
+ </div>
527
+ <div style="padding: 20px; overflow: hidden; flex: 1;">
528
+ ${content}
529
+ </div>
530
+ </div>
531
+ </div>
532
+ `;
533
+
534
+ // Add modal to page
535
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
536
+
537
+ // Close modal when clicking outside
538
+ document.getElementById('logs-modal').addEventListener('click', (e) => {
539
+ if (e.target.id === 'logs-modal') {
540
+ e.target.remove();
541
+ }
542
+ });
543
+ };
544
+
545
+
133
546
  function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
134
547
  const swapy = useRef(null);
135
548
  const container = useRef(null);
@@ -141,8 +554,6 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
141
554
  const [data, setData] = useState({});
142
555
  const [dropdowns, setDropdowns] = useState({});
143
556
  const [filters, setFilters] = useState({});
144
- const [modalShow, setModalShow] = useState(false);
145
- const [modalContent, setModalContent] = useState(null);
146
557
  const [editMode, setEditMode] = useState(false);
147
558
  const [showAddWidgetModal, setShowAddWidgetModal] = useState(false);
148
559
  const [selectedAddWidgetId, setSelectedAddWidgetId] = useState('');
@@ -165,15 +576,15 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
165
576
  const getDefaultDate = (defaultValue) => {
166
577
  switch (defaultValue) {
167
578
  case 'startOfMonth':
168
- return dayjs().startOf('month').format('YYYY-MM-DD');
579
+ return dayjs().startOf('month').format('YYYY-MM-DD'); // Keep YYYY-MM-DD for HTML date inputs
169
580
  case 'endOfMonth':
170
- return dayjs().endOf('month').format('YYYY-MM-DD');
581
+ return dayjs().endOf('month').format('YYYY-MM-DD'); // Keep YYYY-MM-DD for HTML date inputs
171
582
  default:
172
583
  return '';
173
584
  }
174
585
  };
175
586
 
176
- // Initialize filters based on each widgets filters.
587
+ // Initialize filters based on each widget's filters.
177
588
  useEffect(() => {
178
589
  const initialFilters = {};
179
590
  dashboardSetting.widgets.forEach((widget) => {
@@ -352,14 +763,6 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
352
763
  }
353
764
  }, [dashboardSetting]);
354
765
 
355
- const openModal = (widgetId) => {
356
- setModalContent(widgetId);
357
- setModalShow(true);
358
- };
359
-
360
- const closeModal = () => {
361
- setModalShow(false);
362
- };
363
766
 
364
767
  const handleFilterChange = (widgetId, key, value) => {
365
768
  setFilters((prevFilters) => ({
@@ -1252,6 +1655,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
1252
1655
  ) => (
1253
1656
  <td
1254
1657
  key={`table-data-${rowIndex}-${columnIndex}`}
1658
+ style={{ fontFamily: 'inherit' }}
1255
1659
  >
1256
1660
  {renderCellContent(
1257
1661
  row[column],
@@ -1493,17 +1897,6 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
1493
1897
  </div>
1494
1898
  {renderFilters(widget)}
1495
1899
  <div>{renderWidget(widget)}</div>
1496
- {widget.modal && (
1497
- <div className={styles.widgetAction}>
1498
- <button
1499
- className={`${styles.btn} ${styles.dmore}`}
1500
- onClick={() => openModal(widget.id)}
1501
- >
1502
- View all{' '}
1503
- <span>({data[widget.id]?.length || 0})</span>
1504
- </button>
1505
- </div>
1506
- )}
1507
1900
  {editMode && (
1508
1901
  <div className={styles.widgetTools}>
1509
1902
  <div className={styles.resizeTools}>
@@ -1568,35 +1961,6 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
1568
1961
  )}
1569
1962
  <div className={styles.grid} ref={container}>
1570
1963
  {renderWidgets()}
1571
- <Popup open={modalShow} onClose={closeModal} closeOnDocumentClick={false}>
1572
- <div className="modalwrap top--modal modalWide">
1573
- <div className="modal">
1574
- <div className="modal__header">
1575
- <h1>
1576
- {
1577
- dashboardSetting.modalTitles?.[
1578
- modalContent
1579
- ]
1580
- }
1581
- </h1>
1582
- <button
1583
- className="modal__close"
1584
- onClick={closeModal}
1585
- >
1586
- <X strokeWidth={1} size={24} />
1587
- </button>
1588
- </div>
1589
- <div className="modal__content">
1590
- {modalContent &&
1591
- renderWidget(
1592
- dashboardSetting.widgets.find(
1593
- (w) => w.id === modalContent
1594
- )
1595
- )}
1596
- </div>
1597
- </div>
1598
- </div>
1599
- </Popup>
1600
1964
  <Popup
1601
1965
  open={showAddWidgetModal}
1602
1966
  onClose={() => setShowAddWidgetModal(false)}
@@ -64,6 +64,7 @@ import {
64
64
  X as CircleX,
65
65
  } from 'lucide-react';
66
66
  import { confirmDialog } from '../utils/ConfirmDialog';
67
+ import OcrTemplateEnhanced from './OcrTemplateEnhanced';
67
68
 
68
69
  import 'react-big-calendar/lib/css/react-big-calendar.css';
69
70
  import 'react-big-calendar/lib/addons/dragAndDrop/styles.css';
@@ -1435,7 +1436,16 @@ function GenericDetail({
1435
1436
  case 'link':
1436
1437
  return renderLink();
1437
1438
  case 'ocrTemplate':
1438
- return renderOcrTemplate(id);
1439
+ return (
1440
+ <OcrTemplateEnhanced
1441
+ id={id}
1442
+ data={data}
1443
+ setData={setData}
1444
+ routeParams={routeParams}
1445
+ urlParam={urlParam}
1446
+ onSave={handleOcrTemplateSave}
1447
+ />
1448
+ );
1439
1449
  case 'options':
1440
1450
  return renderOptions();
1441
1451
  case 'password':
@@ -2613,15 +2623,19 @@ function GenericDetail({
2613
2623
  className={
2614
2624
  item.size ===
2615
2625
  'full'
2616
- ? `${styles['fw-grid-item']} ${styles.notecolor}`
2626
+ ? item.type === 'ocrTemplate'
2627
+ ? styles['fw-grid-item']
2628
+ : `${styles['fw-grid-item']} ${styles.notecolor}`
2617
2629
  : null
2618
2630
  }
2619
2631
  >
2620
- <strong>
2621
- {item.label
2622
- ? `${item.label}:`
2623
- : null}
2624
- </strong>{' '}
2632
+ {item.type !== 'ocrTemplate' && (
2633
+ <strong>
2634
+ {item.label
2635
+ ? `${item.label}:`
2636
+ : null}
2637
+ </strong>
2638
+ )}{item.type !== 'ocrTemplate' ? ' ' : ''}
2625
2639
  {item
2626
2640
  .href
2627
2641
  ?.url &&